Unstable 0.16.0.0

This commit is contained in:
Markus Isberg
2022-01-14 01:28:24 +09:00
parent d9baeaa2e1
commit 7d6421a548
237 changed files with 6430 additions and 2205 deletions
@@ -150,7 +150,7 @@ namespace Barotrauma
private CoroutineHandle disableTailCoroutine;
private readonly IEnumerable<Body> myBodies;
private readonly List<Body> myBodies;
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
@@ -306,7 +306,8 @@ namespace Barotrauma
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
myBodies.Add(Character.AnimController.Collider.FarseerBody);
}
private CharacterParams.AIParams _aiParams;
@@ -1837,7 +1838,7 @@ namespace Barotrauma
if (!attack.IsValidTarget(target)) { return false; }
if (target is ISerializableEntity se && target is Character)
{
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; }
}
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { return false; }
if (attack.Ranged)
@@ -2182,10 +2183,22 @@ namespace Barotrauma
float margin = MathHelper.PiOver4 * distanceFactor;
if (angle < margin)
{
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(weapon.SimPosition, target.SimPosition, myBodies, collisionCategories, allowInsideFixture: true);
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true);
if (pickedBody != null)
{
if (target is MapEntity)
{
if (pickedBody.UserData is Submarine sub && sub == target.Submarine)
{
return true;
}
else if (target == pickedBody.UserData)
{
return true;
}
}
Character t = null;
if (pickedBody.UserData is Character c)
{
@@ -832,6 +832,8 @@ namespace Barotrauma
if (container == null) { return 0; }
if (!container.HasAccess(character)) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
var rootContainer = container.Item.GetRootContainer();
if (rootContainer?.GetComponent<Fabricator>() != null || rootContainer?.GetComponent<Fabricator>() != null) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
@@ -1088,6 +1090,7 @@ namespace Barotrauma
private void RespondToAttack(Character attacker, AttackResult attackResult)
{
float minorDamageThreshold = 10;
float healAmount = 0.0f;
if (attacker != null)
{
@@ -1135,6 +1138,7 @@ namespace Barotrauma
// Don't react to attackers that are outside of the sub (e.g. AoE attacks)
return;
}
bool isAttackerInfected = false;
bool isAttackerFightingEnemy = false;
if (IsFriendly(attacker))
{
@@ -1155,10 +1159,14 @@ namespace Barotrauma
}
else
{
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0;
// Inform other NPCs
if (cumulativeDamage > 1 || totalDamage >= 10)
if (isAttackerInfected || cumulativeDamage > 1 || totalDamage >= minorDamageThreshold)
{
InformOtherNPCs(cumulativeDamage);
if (GameMain.IsMultiplayer || !attacker.IsPlayer || Character.TeamID != attacker.TeamID)
{
InformOtherNPCs(cumulativeDamage);
}
}
if (Character.IsBot)
{
@@ -1167,7 +1175,7 @@ namespace Barotrauma
{
if (Character.IsSecurity)
{
if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10)
if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > minorDamageThreshold)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f);
}
@@ -1181,26 +1189,8 @@ namespace Barotrauma
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f);
}
}
if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID)
{
// If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay.
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
}
else
{
// Don't react to minor (accidental) dmg done by characters that are in the same team
if (cumulativeDamage < 10)
{
if (!Character.IsSecurity && cumulativeDamage > 1)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
}
else
{
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 50), attacker, GetReactionTime() * 2);
}
}
// If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay.
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
}
if (!isAttackerFightingEnemy)
{
@@ -1242,13 +1232,13 @@ namespace Barotrauma
continue;
}
}
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10);
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing);
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
}
}
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, bool isWitnessing = false, float dmgThreshold = 10, bool allowOffensive = true)
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, bool isWitnessing = false)
{
if (!IsFriendly(attacker))
{
@@ -1268,6 +1258,17 @@ namespace Barotrauma
}
else
{
float dmgThreshold = attacker.TeamID == Character.TeamID ? 50 : minorDamageThreshold;
if (isAttackerInfected)
{
cumulativeDamage = 100;
}
if (GameMain.IsSingleplayer && attacker.IsPlayer && Character.TeamID == attacker.TeamID)
{
// Bots in the player team never act aggressively in single player when attacked by the player
dmgThreshold = minorDamageThreshold;
return cumulativeDamage > dmgThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
}
if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Outside or attacked from an unconnected submarine -> don't react.
@@ -1279,17 +1280,17 @@ namespace Barotrauma
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
}
else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
{
return Character.CombatAction.WitnessReaction;
}
else if (attacker.IsPlayer && FindInstigator() is Character instigator)
if (attacker.IsPlayer && FindInstigator() is Character instigator)
{
// The guards don't react when the player there's an instigator around
// The guards don't react to player's aggressions when there's an instigator around
isAttackerFightingEnemy = true;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
}
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
{
if (c.IsSecurity)
{
@@ -1307,15 +1308,11 @@ namespace Barotrauma
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
}
if (attackResult.Afflictions != null && attackResult.Afflictions.Any(a => a is AfflictionHusk))
{
cumulativeDamage = 100;
}
if (cumulativeDamage > dmgThreshold)
{
if (c.IsSecurity)
{
return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
}
else
{
@@ -1838,7 +1835,7 @@ namespace Barotrauma
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.GetActiveObjectives<AIObjectiveFightIntruders>().Any();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
@@ -51,19 +51,13 @@ namespace Barotrauma
private set;
}
/// <summary>
/// Returns true if the current or the next node is in ladders.
/// </summary>
public bool InLadders =>
currentPath != null && currentPath.CurrentNode != null &&
(currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character) ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character)));
/// <summary>
/// Returns true if any node in the path is in stairs
/// </summary>
public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character);
public bool IsNextNodeLadder => GetNextLadder() != null;
public bool IsNextLadderSameAsCurrent
@@ -99,14 +93,24 @@ namespace Barotrauma
base.Update(speed);
float step = 1.0f / 60.0f;
checkDoorsTimer -= step;
buttonPressTimer -= step;
if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen)
{
buttonPressTimer = 0;
}
else
{
buttonPressTimer -= step;
}
findPathTimer -= step;
}
public void SetPath(SteeringPath path)
{
currentPath = path;
if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
if (path.Nodes.Any())
{
currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
}
findPathTimer = Math.Min(findPathTimer, 1.0f);
IsPathDirty = false;
}
@@ -124,15 +128,9 @@ namespace Barotrauma
public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true)
{
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.state && !lastDoor.door.IsOpen)
{
// We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
Reset();
}
else
{
steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
}
// Have to use a variable here or resetting doesn't work.
Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
steering += addition;
}
/// <summary>
@@ -328,7 +326,13 @@ namespace Barotrauma
{
CheckDoorsInPath();
doorsChecked = true;
}
}
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen)
{
// We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
Reset();
return Vector2.Zero;
}
Vector2 pos = host.WorldPosition;
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
@@ -378,7 +382,7 @@ namespace Barotrauma
//at the same height as the waypoint
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
{
float heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY;
float heightFromFloor = character.AnimController.GetHeightFromFloor();
if (heightFromFloor <= 0.0f)
{
diff.Y = Math.Max(diff.Y, 100);
@@ -516,7 +520,7 @@ namespace Barotrauma
return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y));
}
private (Door door, bool state) lastDoor;
private (Door door, bool shouldBeOpen) lastDoor;
private float GetDoorCheckTime()
{
if (steering.LengthSquared() > 0)
@@ -539,7 +543,6 @@ namespace Barotrauma
WayPoint nextWaypoint = null;
Door door = null;
bool shouldBeOpen = false;
if (currentPath.Nodes.Count == 1)
{
door = currentPath.Nodes.First().ConnectedDoor;
@@ -645,7 +648,7 @@ namespace Barotrauma
});
if (canAccess)
{
bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.state != shouldBeOpen;
bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.shouldBeOpen != shouldBeOpen;
if (door.HasIntegratedButtons)
{
if (pressButton && character.CanSeeTarget(door.Item))
@@ -653,7 +656,7 @@ namespace Barotrauma
if (door.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = buttonPressCooldown;
buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0;
}
else
{
@@ -671,7 +674,7 @@ namespace Barotrauma
if (closestButton.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = buttonPressCooldown;
buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0;
}
else
{
@@ -697,7 +700,6 @@ namespace Barotrauma
// The button is on the wrong side of the door or a wall
currentPath.Unreachable = true;
}
lastDoor = (null, false);
return;
}
}
@@ -16,7 +16,6 @@ namespace Barotrauma
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool ForceOrderPriority => true;
public virtual bool PrioritizeIfSubObjectivesActive => false;
/// <summary>
@@ -85,11 +85,12 @@ namespace Barotrauma
bool equip = item.GetComponent<Holdable>() != null ||
item.AllowedSlots.Any(s => s != InvSlotType.Any) &&
item.AllowedSlots.None(s =>
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes);
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes ||
s == InvSlotType.HealthInterface);
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{
@@ -11,7 +11,7 @@ namespace Barotrauma
public override string Identifier { get; set; } = "cleanup items";
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public override bool ForceOrderPriority => false;
protected override bool ForceOrderPriority => false;
public readonly List<Item> prioritizedItems = new List<Item>();
@@ -117,7 +117,7 @@ namespace Barotrauma
private float AimSpeed => HumanAIController.AimSpeed;
private float AimAccuracy => HumanAIController.AimAccuracy;
private bool EnemyIsClose() => Enemy != null && character.CurrentHull != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
private bool EnemyIsClose() => Enemy != null && Enemy.CurrentHull != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull) && Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X) < 300;
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
: base(character, objectiveManager, priorityModifier)
@@ -366,7 +366,7 @@ namespace Barotrauma
}
}
}
bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest;
bool isAllowedToSeekWeapons = character.CurrentHull != null && !EnemyIsClose() && character.TeamID != CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest;
if (!isAllowedToSeekWeapons)
{
if (WeaponComponent == null)
@@ -1190,19 +1190,5 @@ namespace Barotrauma
}
}
}
//private float CalculateEnemyStrength()
//{
// float enemyStrength = 0;
// AttackContext currentContext = character.GetAttackContext();
// foreach (Limb limb in Enemy.AnimController.Limbs)
// {
// if (limb.attack == null) continue;
// if (!limb.attack.IsValidContext(currentContext)) { continue; }
// if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; }
// enemyStrength += limb.attack.GetTotalDamage(false);
// }
// return enemyStrength;
//}
}
}
@@ -159,7 +159,8 @@ namespace Barotrauma
{
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
@@ -40,6 +40,7 @@ namespace Barotrauma
public Func<Item, bool> RemoveExistingPredicate { get; set; }
public int? RemoveExistingMax { get; set; }
public string AbandonGetItemDialogueIdentifier { get; set; }
public Func<bool> AbandonGetItemDialogueCondition { get; set; }
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -106,6 +107,7 @@ namespace Barotrauma
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip)
{
CannotFindDialogueCondition = AbandonGetItemDialogueCondition,
CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier,
SpeakIfFails = AbandonGetItemDialogueIdentifier != null,
TakeWholeStack = this.TakeWholeStack
@@ -26,6 +26,7 @@ namespace Barotrauma
if (totalEnemies == 0) { return 0; }
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
// If there's any security officers onboard, leave fighting for them.
return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100;
}
@@ -64,6 +65,7 @@ namespace Barotrauma
if (target.CurrentHull == null) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (character.Submarine.TeamID != target.Submarine.TeamID) { return false; }
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; }
return true;
@@ -162,7 +162,10 @@ namespace Barotrauma
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
TargetName = Leak.FlowTargetHull?.DisplayName,
CheckVisibility = false
CheckVisibility = false,
requiredCondition = () => Leak.Submarine == character.Submarine,
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
SpeakCannotReachCondition = () => !CheckObjectiveSpecific()
},
onAbandon: () =>
{
@@ -59,6 +59,7 @@ namespace Barotrauma
public bool CheckPathForEachItem { get; set; }
public bool SpeakIfFails { get; set; }
public string CannotFindDialogueIdentifierOverride { get; set; }
public Func<bool> CannotFindDialogueCondition { get; set; }
private int _itemCount = 1;
public int ItemCount
@@ -560,22 +561,18 @@ namespace Barotrauma
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
#endif
}
if (SpeakIfFails)
{
SpeakCannotFind();
}
SpeakCannotFind();
}
private void SpeakCannotFind()
{
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true);
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f);
}
}
if (!SpeakIfFails) { return; }
if (!character.IsOnPlayerTeam) { return; }
if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; }
if (CannotFindDialogueCondition != null && !CannotFindDialogueCondition()) { return; }
string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true);
if (msg == null) { return; }
character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f);
}
}
}
@@ -96,6 +96,8 @@ namespace Barotrauma
public float? OverridePriority = null;
public Func<bool> SpeakCannotReachCondition { get; set; }
protected override float GetPriority()
{
bool isOrder = objectiveManager.IsOrder(this);
@@ -166,14 +168,14 @@ namespace Barotrauma
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
}
#endif
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails)
{
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character));
if (msg != null)
{
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
}
}
if (!character.IsOnPlayerTeam) { return; }
if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; }
if (DialogueIdentifier == null) { return; }
if (!SpeakIfFails) { return; }
if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; }
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character));
if (msg == null) { return; }
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
}
public void ForceAct(float deltaTime) => Act(deltaTime);
@@ -635,21 +637,27 @@ namespace Barotrauma
{
get
{
if (SteeringManager == PathSteering && PathSteering.CurrentPath?.CurrentNode?.Ladders != null)
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished && PathSteering.IsCurrentNodeLadder)
{
//don't consider the character to be close enough to the target while climbing ladders,
//UNLESS the last node in the path has been reached
//otherwise characters can let go of the ladders too soon once they're close enough to the target
if (PathSteering.CurrentPath.NextNode != null) { return false; }
// Climbing a ladder
if (Target.WorldPosition.Y > character.WorldPosition.Y)
{
// The target is still above us
return false;
}
if (!character.AnimController.IsAboveFloor)
{
// Going through a hatch
return false;
}
}
if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater)
{
float yDiff = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y);
if (yDiff > CloseEnough) { return false; }
float xDiff = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X);
return xDiff <= CloseEnough;
float yDist = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y);
if (yDist > CloseEnough) { return false; }
float xDist = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X);
return xDist <= CloseEnough;
}
Vector2 sourcePos = UseDistanceRelativeToAimSourcePos ? character.AnimController.AimSourceWorldPos : character.WorldPosition;
return Vector2.DistanceSquared(Target.WorldPosition, sourcePos) < CloseEnough * CloseEnough;
}
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
@@ -20,6 +21,8 @@ namespace Barotrauma
private Item Container { get; }
private ItemContainer ItemContainer { get; }
private ImmutableArray<string> TargetContainerTags { get; }
private ImmutableHashSet<string> ValidContainableItemIdentifiers { get; }
private static Dictionary<ItemPrefab, ImmutableHashSet<string>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<string>>();
private int itemIndex = 0;
private AIObjectiveDecontainItem decontainObjective;
@@ -47,6 +50,109 @@ namespace Barotrauma
abandonGetItemDialogueIdentifier = optionSpecificDialogueIdentifier;
}
}
ValidContainableItemIdentifiers = GetValidContainableItemIdentifiers();
if (ValidContainableItemIdentifiers.None())
{
#if DEBUG
DebugConsole.ShowError($"No valid containable item identifiers found for the Load Item objective targeting {Container}");
#endif
Abandon = true;
return;
}
}
private enum CheckStatus { Unfinished, Finished }
private ImmutableHashSet<string> GetValidContainableItemIdentifiers()
{
if (AllValidContainableItemIdentifiers.TryGetValue(Container.Prefab, out var existingIdentifiers))
{
return existingIdentifiers;
}
// Status effects are often used to alter item condition so using the Containable Item Identifiers directly can lead to unwanted results
// For example, placing welding fuel tanks inside oxygen tank shelves
bool defaultContainableItemIdentifiers = true;
var potentialContainablePrefabs = MapEntityPrefab.List
.Where(mep => mep is ItemPrefab ip && ItemContainer.ContainableItemIdentifiers.Any(i => i == ip.Identifier || ip.Tags.Contains(i)))
.Cast<ItemPrefab>();
var validContainableItemIdentifiers = new HashSet<string>();
foreach (var component in Container.Components)
{
if (CheckComponent() == CheckStatus.Finished)
{
break;
}
CheckStatus CheckComponent()
{
if (component.statusEffectLists != null)
{
foreach (var (_, statusEffects) in component.statusEffectLists)
{
if (CheckStatusEffects(statusEffects) == CheckStatus.Finished)
{
return CheckStatus.Finished;
}
}
}
if (component is ItemContainer itemContainer && itemContainer.ContainableItems != null)
{
foreach (var item in itemContainer.ContainableItems)
{
if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished)
{
return CheckStatus.Finished;
}
}
}
return CheckStatus.Unfinished;
CheckStatus CheckStatusEffects(IEnumerable<StatusEffect> statusEffects)
{
if (statusEffects == null) { return CheckStatus.Unfinished; }
foreach (var statusEffect in statusEffects)
{
if ((statusEffect.TargetIdentifiers == null || statusEffect.TargetIdentifiers.None()) && !statusEffect.HasConditions) { continue; }
switch (TargetItemCondition)
{
case AIObjectiveLoadItems.ItemCondition.Empty:
if (!statusEffect.ReducesItemCondition()) { continue; }
break;
case AIObjectiveLoadItems.ItemCondition.Full:
if (!statusEffect.IncreasesItemCondition()) { continue; }
break;
default:
continue;
}
defaultContainableItemIdentifiers = false;
if (statusEffect.TargetIdentifiers != null)
{
foreach (string target in statusEffect.TargetIdentifiers)
{
foreach (var prefab in potentialContainablePrefabs)
{
if (CheckPrefab(prefab, () => prefab.Tags.Contains(target)) == CheckStatus.Finished) { return CheckStatus.Finished; }
}
}
}
foreach (var prefab in potentialContainablePrefabs)
{
if (CheckPrefab(prefab, () => statusEffect.MatchesTagConditionals(prefab)) == CheckStatus.Finished) { return CheckStatus.Finished; }
}
CheckStatus CheckPrefab(ItemPrefab prefab, Func<bool> isValid)
{
if (validContainableItemIdentifiers.Contains(prefab.Identifier)) { return CheckStatus.Unfinished; }
if (!isValid()) { return CheckStatus.Unfinished; }
validContainableItemIdentifiers.Add(prefab.Identifier);
if (potentialContainablePrefabs.Any(p => !validContainableItemIdentifiers.Contains(p.Identifier))) { return CheckStatus.Unfinished; }
return CheckStatus.Finished;
}
}
return CheckStatus.Unfinished;
}
}
}
var newIdentifiers = defaultContainableItemIdentifiers ? ItemContainer.ContainableItemIdentifiers.ToImmutableHashSet() : validContainableItemIdentifiers.ToImmutableHashSet();
AllValidContainableItemIdentifiers.Add(Container.Prefab, newIdentifiers);
return newIdentifiers;
}
protected override float GetPriority()
@@ -116,7 +222,7 @@ namespace Barotrauma
base.Update(deltaTime);
if (targetItem == null)
{
if (character.FindItem(ref itemIndex, out Item item, identifiers: ItemContainer.ContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetConditionBasedPriority))
if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority))
{
if (item == null)
{
@@ -125,17 +231,19 @@ namespace Barotrauma
}
targetItem = item;
}
// Prefer items closer to full condition when target condition is Empty, and vice versa
float GetConditionBasedPriority(Item item)
float GetPriority(Item item)
{
try
{
return TargetItemCondition switch
// Prefer items closer to full condition when target condition is Empty, and vice versa
float conditionBasedPriority = TargetItemCondition switch
{
AIObjectiveLoadItems.ItemCondition.Full => MathUtils.InverseLerp(100.0f, 0.0f, item.ConditionPercentage),
AIObjectiveLoadItems.ItemCondition.Empty => MathUtils.InverseLerp(0.0f, 100.0f, item.ConditionPercentage),
_ => throw new NotImplementedException()
};
// Prefer items that have the same identifier as one of the already contained items
return ItemContainer.ContainsItemsWithSameIdentifier(item) ? conditionBasedPriority : conditionBasedPriority / 2;
}
catch (NotImplementedException)
{
@@ -161,10 +269,11 @@ namespace Barotrauma
TryAddSubObjective(ref decontainObjective,
constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
{
AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem),
AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier,
Equip = true,
RemoveExistingWhenNecessary = true,
RemoveExistingPredicate = (i) => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
RemoveExistingMax = 1
},
onCompleted: () =>
@@ -189,6 +298,7 @@ namespace Barotrauma
{
if (item == null) { return false; }
if (item.Removed) { return false; }
if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; }
if (ignoredItems.Contains(item)) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
var rootInventoryOwner = item.GetRootInventoryOwner();
@@ -46,6 +46,7 @@ namespace Barotrauma
public override bool AllowSubObjectiveSorting => true;
public virtual bool InverseTargetEvaluation => false;
protected virtual bool ResetWhenClearingIgnoreList => true;
protected virtual bool ForceOrderPriority => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
@@ -643,7 +643,12 @@ namespace Barotrauma
public bool IsOrder(AIObjective objective)
{
return objective == ForcedOrder || CurrentOrders.Any(o => o.Objective == objective);
if (objective == ForcedOrder) { return true; }
foreach (var order in CurrentOrders)
{
if (order.Objective == objective) { return true; }
}
return false;
}
public bool HasOrders()
@@ -33,6 +33,7 @@ namespace Barotrauma
if (pump.Item.Submarine == null) { return false; }
if (pump.Item.CurrentHull == null) { return false; }
if (pump.Item.Submarine.TeamID != character.TeamID) { return false; }
if (pump.IsAutoControlled) { return false; }
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
if (character.Submarine != null)
@@ -489,7 +489,7 @@ namespace Barotrauma
return Priority;
}
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
public static IEnumerable<Affliction> GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs);
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
@@ -374,7 +374,7 @@ namespace Barotrauma
}
if (OptionNames.Count != Options.Length)
{
DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
DebugConsole.AddWarning("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
OptionNames.Clear();
Options.ForEach(o => OptionNames.Add(o, o));
}
@@ -499,16 +499,14 @@ namespace Barotrauma
return false;
}
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", int? priority = null)
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", bool isNewOrder = true)
{
priority ??= CharacterInfo.HighestManualOrderPriority;
// If the order has a lesser priority, it means we are rearranging character orders
if (!TargetAllCharacters && priority != CharacterInfo.HighestManualOrderPriority && Identifier != "dismissed")
if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed")
{
// Use special dialogue when we're rearranging character orders
return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty;
}
string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}";
messageTag += $".{Identifier}";
string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}.{Identifier}";
if (!string.IsNullOrEmpty(orderOption))
{
if (Identifier != "dismissed")
@@ -55,7 +55,7 @@ namespace Barotrauma
CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5);
}
CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter);
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter);
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: CharacterInfo.HighestManualOrderPriority, CommandingCharacter, CommandingCharacter != OrderedCharacter);
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative"), delay: 1.0f, minDurationBetweenSimilar: 5);
}
TimeSinceLastAttempt = 0f;
@@ -75,7 +75,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
if (!Active) { return; }
if (!Active || character.IsArrested) { return; }
decisionTimer -= deltaTime;
if (decisionTimer <= 0.0f)
{
@@ -344,7 +344,6 @@ namespace Barotrauma
ShipIssueWorkers.Clear();
// could have support for multiple reactors, todo m61
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup"));
@@ -269,6 +269,11 @@ namespace Barotrauma
}
}
public float GetHeightFromFloor() => GetColliderBottom().Y - FloorY;
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
public bool IsAboveFloor => GetHeightFromFloor() > -0.1f;
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
{
useItemTimer = 0.5f;
@@ -332,7 +337,7 @@ namespace Barotrauma
aimingMelee = aimMelee;
if (character.Stun > 0.0f || character.IsIncapacitated)
{
aim = false;
aim = false;
}
//calculate the handle positions
@@ -382,51 +382,55 @@ namespace Barotrauma
mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * 2, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
mouthLimb.body.ApplyTorque(-force * 50);
}
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)
if (Character.CanEat)
{
target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false);
}
if (Rand.Value() < particleFrequency)
{
target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false);
}
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;
//keep severing joints until there is only one limb left
var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered);
if (nonSeveredJoints.None())
var jaw = GetLimb(LimbType.Jaw);
if (jaw != null)
{
//small monsters don't eat the contents of the character's inventory
if (Mass < target.AnimController.Mass)
{
target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null));
}
//only one limb left, the character is now full eaten
Entity.Spawner?.AddToRemoveQueue(target);
if (Character.AIController is EnemyAIController enemyAi)
{
enemyAi.PetBehavior?.OnEat("dead", 1.0f);
}
character.SelectedCharacter = null;
jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25);
}
else //sever a random joint
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f);
if (Rand.Value() < particleFrequency / 6)
{
target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom());
target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false);
}
}
if (Rand.Value() < particleFrequency)
{
target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false);
}
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;
//keep severing joints until there is only one limb left
var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered);
if (nonSeveredJoints.None())
{
//small monsters don't eat the contents of the character's inventory
if (Mass < target.AnimController.Mass)
{
target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null));
}
//only one limb left, the character is now full eaten
Entity.Spawner?.AddToRemoveQueue(target);
if (Character.AIController is EnemyAIController enemyAi)
{
enemyAi.PetBehavior?.OnEat("dead", 1.0f);
}
character.SelectedCharacter = null;
}
else //sever a random joint
{
target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom());
}
}
}
}
else
{
@@ -808,7 +808,8 @@ namespace Barotrauma
if (head == null) { return; }
if (torso == null) { return; }
if (currentHull != null)
//check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside
if (currentHull != null && character.CurrentHull != null)
{
float surfacePos = currentHull.Surface;
float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f);
@@ -816,7 +817,7 @@ namespace Barotrauma
//and use its water surface instead of the current hull's
if (currentHull.Rect.Y - currentHull.Surface < 5.0f)
{
GetSurfacePos(CurrentHull, ref surfacePos);
GetSurfacePos(currentHull, ref surfacePos);
void GetSurfacePos(Hull hull, ref float prevSurfacePos)
{
if (prevSurfacePos > surfaceThreshold) { return; }
@@ -834,7 +835,7 @@ namespace Barotrauma
foreach (var linkedTo in gap.linkedTo)
{
if (linkedTo is Hull otherHull && otherHull != hull)
if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull)
{
prevSurfacePos = Math.Max(surfacePos, otherHull.Surface);
GetSurfacePos(otherHull, ref prevSurfacePos);
@@ -888,7 +889,6 @@ namespace Barotrauma
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * Dir;
TargetMovement = new Vector2(0.0f, -0.1f);
float newRotation = MathUtils.VectorToAngle(diff);
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
@@ -130,6 +130,9 @@ namespace Barotrauma
set => _structureDamage = value;
}
[Serialize(true, true), Editable]
public bool EmitStructureDamageParticles { get; private set; }
private float _itemDamage;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float ItemDamage
@@ -311,7 +314,7 @@ namespace Barotrauma
return totalDamage * DamageMultiplier;
}
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f, float penetration = 0f)
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f)
{
if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null);
if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null);
@@ -682,8 +682,8 @@ namespace Barotrauma
get { return CharacterHealth.BloodlossAmount; }
set
{
if (!MathUtils.IsValid(value)) return;
CharacterHealth.BloodlossAmount = MathHelper.Clamp(value, 0.0f, 100.0f);
if (!MathUtils.IsValid(value)) { return; }
CharacterHealth.BloodlossAmount = value;
}
}
@@ -1830,7 +1830,7 @@ namespace Barotrauma
if (!attack.IsValidTarget(attackTarget)) { return false; }
if (attackTarget is ISerializableEntity se && attackTarget is Character)
{
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; }
}
}
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; }
@@ -2270,17 +2270,18 @@ namespace Barotrauma
}
}
if (SelectedConstruction?.GetComponent<RemoteController>()?.TargetItem == item ||
HeldItems.Any(it => it.GetComponent<RemoteController>()?.TargetItem == item))
{
return true;
}
if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; }
Pickable pickableComponent = item.GetComponent<Pickable>();
if (pickableComponent != null && pickableComponent.Picker != this && pickableComponent.Picker != null && !pickableComponent.Picker.IsDead) { return false; }
if (SelectedConstruction?.GetComponent<RemoteController>()?.TargetItem == item) { return true; }
//optimization: don't use HeldItems because it allocates memory and this method is executed very frequently
var heldItem1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand);
if (heldItem1?.GetComponent<RemoteController>()?.TargetItem == item) { return true; }
var heldItem2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand);
if (heldItem2?.GetComponent<RemoteController>()?.TargetItem == item) { return true; }
Vector2 characterDirection = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(AnimController.Collider.Rotation));
Vector2 upperBodyPosition = Position + (characterDirection * 20.0f);
@@ -3225,7 +3226,7 @@ namespace Barotrauma
if (orderGiver != null)
{
var abilityOrderedCharacter = new AbilityCharacter(this);
var abilityOrderedCharacter = new AbilityOrderedCharacter(this);
orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter);
if (orderGiver.LastOrderedCharacter != this)
@@ -3547,12 +3548,12 @@ namespace Barotrauma
}
#endif
// Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head)
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer);
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer, attacker: attacker);
return attackResult;
}
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading)
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, Character attacker = null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if DEBUG
@@ -3590,9 +3591,20 @@ namespace Barotrauma
if (severed)
{
Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA;
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
if (attacker != null)
{
foreach (var statusEffect in statusEffects)
{
if (statusEffect.type == ActionType.OnSevered) { statusEffect.SetUser(attacker); }
}
foreach (var statusEffect in targetLimb.StatusEffects)
{
if (statusEffect.type == ActionType.OnSevered) { statusEffect.SetUser(attacker); }
}
}
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
}
}
if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI)
@@ -3961,8 +3973,8 @@ namespace Barotrauma
causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource);
OnDeath?.Invoke(this, CauseOfDeath);
var abilityKiller = new AbilityCharacter(CauseOfDeath.Killer);
CheckTalents(AbilityEffectType.OnDieToCharacter, abilityKiller);
var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer);
CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller);
if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen)
{
@@ -4472,18 +4484,17 @@ namespace Barotrauma
if (info == null) { return false; }
info.UnlockedTalents.Add(talentPrefab.Identifier);
if (characterTalents.Any(t => t.Prefab == talentPrefab)) { return false; }
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents });
#endif
CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this);
characterTalent.ActivateTalent(addingFirstTime);
characterTalents.Add(characterTalent);
characterTalent.ActivateTalent(addingFirstTime);
characterTalent.AddedThisRound = addingFirstTime;
if (addingFirstTime)
{
OnTalentGiven(talentPrefab.Identifier);
OnTalentGiven(talentPrefab);
}
return true;
}
@@ -4493,6 +4504,24 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier);
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGetValue(Info.Job.Prefab.Identifier, out TalentTree talentTree))
{
foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees)
{
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{
if (talentOption.Talents.None(t => HasTalent(t.Identifier)))
{
return false;
}
}
}
}
return true;
}
public static IEnumerable<Character> GetFriendlyCrew(Character character)
{
if (character is null)
@@ -4552,7 +4581,7 @@ namespace Barotrauma
}
partial void OnMoneyChanged(int prevAmount, int newAmount);
partial void OnTalentGiven(string talentIdentifier);
partial void OnTalentGiven(TalentPrefab talentPrefab);
/// <summary>
/// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now.
@@ -4724,4 +4753,49 @@ namespace Barotrauma
public Character Killer { get; set; }
}
class AbilityAttackData : AbilityObject, IAbilityCharacter
{
public float DamageMultiplier { get; set; } = 1f;
public float AddedPenetration { get; set; } = 0f;
public List<Affliction> Afflictions { get; set; }
public bool ShouldImplode { get; set; } = false;
public Attack SourceAttack { get; }
public Character Character { get; set; }
public Character Attacker { get; set; }
public AbilityAttackData(Attack sourceAttack, Character character)
{
SourceAttack = sourceAttack;
Character = character;
}
}
class AbilityAttackResult : AbilityObject, IAbilityAttackResult
{
public AttackResult AttackResult { get; set; }
public AbilityAttackResult(AttackResult attackResult)
{
AttackResult = attackResult;
}
}
class AbilityCharacterKiller : AbilityObject, IAbilityCharacter
{
public AbilityCharacterKiller(Character character)
{
Character = character;
}
public Character Character { get; set; }
}
class AbilityOrderedCharacter : AbilityObject, IAbilityCharacter
{
public AbilityOrderedCharacter(Character character)
{
Character = character;
}
public Character Character { get; set; }
}
}
@@ -361,7 +361,7 @@ namespace Barotrauma
public CharacterTeamType TeamID;
private readonly NPCPersonalityTrait personalityTrait;
private NPCPersonalityTrait personalityTrait;
public const int MaxCurrentOrders = 3;
public static int HighestManualOrderPriority => MaxCurrentOrders;
@@ -568,7 +568,7 @@ namespace Barotrauma
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
HasRaces = CharacterConfigElement.GetAttributeBool("races", false);
SetGenderAndRace(randSync);
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant);
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant);
HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
@@ -584,11 +584,10 @@ namespace Barotrauma
}
else
{
name = "";
Name = GetRandomName(randSync);
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId);
SetPersonalityTrait();
Salary = CalculateSalary();
if (ragdollFileName != null)
{
@@ -597,6 +596,11 @@ namespace Barotrauma
LoadHeadAttachments();
}
private void SetPersonalityTrait()
{
personalityTrait = NPCPersonalityTrait.GetRandom(Name + HeadSpriteId);
}
public string GetRandomName(Rand.RandSync randSync)
{
string name = "";
@@ -1261,7 +1265,7 @@ namespace Barotrauma
{
int prevAmount = ExperiencePoints;
var experienceGainMultiplier = new AbilityValue(1f);
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
if (isMissionExperience)
{
Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier);
@@ -1858,18 +1862,27 @@ namespace Barotrauma
}
}
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
{
public AbilitySkillGain(float value, string abilityString, Character character, bool gainedFromAbility)
public AbilitySkillGain(float skillAmount, string skillIdentifier, Character character, bool gainedFromAbility)
{
Value = value;
String = abilityString;
Value = skillAmount;
SkillIdentifier = skillIdentifier;
Character = character;
GainedFromAbility = gainedFromAbility;
}
public Character Character { get; set; }
public float Value { get; set; }
public string String { get; set; }
public string SkillIdentifier { get; set; }
public bool GainedFromAbility { get; }
}
class AbilityExperienceGainMultiplier : AbilityObject, IAbilityValue
{
public AbilityExperienceGainMultiplier(float experienceGainMultiplier)
{
Value = experienceGainMultiplier;
}
public float Value { get; set; }
}
}
@@ -67,6 +67,9 @@ namespace Barotrauma
public Affliction(AfflictionPrefab prefab, float strength)
{
#if CLIENT
prefab?.ReloadSoundsIfNeeded();
#endif
Prefab = prefab;
PendingAdditionStrength = Prefab.GrainBurst;
_strength = strength;
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma
namespace Barotrauma
{
class AfflictionBleeding : Affliction
{
@@ -15,6 +11,10 @@ namespace Barotrauma
{
base.Update(characterHealth, targetLimb, deltaTime);
characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime;
if (Source != null)
{
characterHealth.BloodlossAffliction.Source = Source;
}
}
}
}
@@ -52,10 +52,6 @@ namespace Barotrauma
{
if (state == value) { return; }
state = value;
if (character != null && character == Character.Controlled)
{
UpdateMessages();
}
}
}
@@ -81,6 +77,9 @@ namespace Barotrauma
base.Update(characterHealth, targetLimb, deltaTime);
character = characterHealth.Character;
if (character == null) { return; }
UpdateMessages();
if (!subscribedToDeathEvent)
{
character.OnDeath += CharacterDead;
@@ -363,6 +363,7 @@ namespace Barotrauma
public readonly string Name, Description;
public readonly string TranslationOverride;
public readonly bool IsBuff;
public readonly float HealCostMultiplier;
public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription;
@@ -655,6 +656,7 @@ namespace Barotrauma
Name = TextManager.Get("AfflictionName." + translationId, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", "");
IsBuff = element.GetAttributeBool("isbuff", false);
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f);
if (element.Attribute("nameidentifier") != null)
{
@@ -677,7 +679,7 @@ namespace Barotrauma
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f));
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : 0.05f));
TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f));
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
@@ -751,6 +753,32 @@ namespace Barotrauma
}
}
#if CLIENT
public void ReloadSoundsIfNeeded()
{
foreach (var effect in effects)
{
foreach (var statusEffect in effect.StatusEffects)
{
foreach (var sound in statusEffect.Sounds)
{
if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); }
}
}
}
foreach (var periodicEffect in periodicEffects)
{
foreach (var statusEffect in periodicEffect.StatusEffects)
{
foreach (var sound in statusEffect.Sounds)
{
if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); }
}
}
}
}
#endif
public override string ToString()
{
return "AfflictionPrefab (" + Name + ")";
@@ -111,6 +111,7 @@ namespace Barotrauma
private Affliction oxygenLowAffliction;
private Affliction pressureAffliction;
private Affliction stunAffliction;
public Affliction BloodlossAffliction { get => bloodlossAffliction; }
public bool IsUnconscious
{
@@ -181,7 +182,7 @@ namespace Barotrauma
public float BloodlossAmount
{
get { return bloodlossAffliction.Strength; }
set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0.0f, 100.0f); }
set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0, bloodlossAffliction.Prefab.MaxStrength); }
}
public float Stun
@@ -324,7 +325,11 @@ namespace Barotrauma
if (kvp.Key == affliction)
{
int limbHealthIndex = limbHealths.IndexOf(kvp.Value);
return Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealthIndex);
foreach (Limb limb in Character.AnimController.Limbs)
{
if (limb.HealthIndex == limbHealthIndex) { return limb; }
}
return null;
}
}
return null;
@@ -658,7 +663,7 @@ namespace Barotrauma
newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength);
if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
existingAffliction.Strength = newStrength;
existingAffliction.Source = newAffliction.Source;
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
CalculateVitality();
if (Vitality <= MinVitality)
{
@@ -744,7 +749,6 @@ namespace Barotrauma
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed));
// maybe a bit of a hacky way to do this. should inquire if there is a better way. M61T
if (Character.InWater)
{
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed));
@@ -35,7 +35,7 @@ namespace Barotrauma
public Skill PrimarySkill { get; }
public Job(JobPrefab jobPrefab, int variant = 0)
public Job(JobPrefab jobPrefab, Rand.RandSync randSync = Rand.RandSync.Unsynced, int variant = 0)
{
prefab = jobPrefab;
Variant = variant;
@@ -43,7 +43,7 @@ namespace Barotrauma
skills = new Dictionary<string, Skill>();
foreach (SkillPrefab skillPrefab in prefab.Skills)
{
var skill = new Skill(skillPrefab);
var skill = new Skill(skillPrefab, randSync);
skills.Add(skillPrefab.Identifier, skill);
if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; }
}
@@ -79,7 +79,7 @@ namespace Barotrauma
{
var prefab = JobPrefab.Random(randSync);
var variant = Rand.Range(0, prefab.Variants, randSync);
return new Job(prefab, variant);
return new Job(prefab, randSync, variant);
}
public float GetSkillLevel(string skillIdentifier)
@@ -36,10 +36,10 @@ namespace Barotrauma
public readonly float PriceMultiplier = 1.0f;
public Skill(SkillPrefab prefab)
public Skill(SkillPrefab prefab, Rand.RandSync randSync)
{
Identifier = prefab.Identifier;
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, Rand.RandSync.Server);
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
icon = GetIcon();
PriceMultiplier = prefab.PriceMultiplier;
}
@@ -583,6 +583,8 @@ namespace Barotrauma
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
public IEnumerable<StatusEffect> StatusEffects { get { return statusEffects; } }
public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams)
{
this.ragdoll = ragdoll;
@@ -756,8 +758,8 @@ namespace Barotrauma
}
if (attacker != null)
{
var abilityAffliction = new AbilityAfflictionCharacter(newAffliction, character);
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAffliction);
var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character);
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter);
}
if (applyAffliction)
{
@@ -1309,4 +1311,16 @@ namespace Barotrauma
partial void LoadParamsProjSpecific();
}
class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter
{
public AbilityAfflictionCharacter(Affliction affliction, Character character)
{
Affliction = affliction;
Character = character;
}
public Character Character { get; set; }
public Affliction Affliction { get; set; }
}
}
@@ -612,7 +612,7 @@ namespace Barotrauma
[Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Width { get; set; }
[Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
[Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)]
public float Density { get; set; }
[Serialize(false, true), Editable]
@@ -1,5 +1,6 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -7,15 +8,19 @@ namespace Barotrauma.Abilities
{
class AbilityConditionAttackData : AbilityConditionData
{
[Flags]
private enum WeaponType
{
Any = 0,
Melee = 1,
Ranged = 2,
HandheldRanged = 3,
Turret = 4
HandheldRanged = 4,
Turret = 8,
NoWeapon = 16
};
private static readonly List<WeaponType> WeaponTypeValues = Enum.GetValues(typeof(WeaponType)).Cast<WeaponType>().ToList();
private readonly string itemIdentifier;
private readonly string[] tags;
private readonly WeaponType weapontype;
@@ -65,27 +70,39 @@ namespace Barotrauma.Abilities
if (weapontype != WeaponType.Any)
{
switch (weapontype)
foreach (WeaponType wt in WeaponTypeValues)
{
// it is possible that an item that has both a melee and a projectile component will return true
// even when not used as a melee/ranged weapon respectively
// attackdata should contain data regarding whether the attack is melee or not
case WeaponType.Melee:
return item?.GetComponent<MeleeWeapon>() != null;
case WeaponType.Ranged:
return item?.GetComponent<Projectile>() != null;
case WeaponType.HandheldRanged:
{
var projectile = item?.GetComponent<Projectile>();
return projectile?.Launcher?.GetComponent<Holdable>() != null;
}
case WeaponType.Turret:
{
var projectile = item?.GetComponent<Projectile>();
return projectile?.Launcher?.GetComponent<Turret>() != null;
}
if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; }
switch (wt)
{
// it is possible that an item that has both a melee and a projectile component will return true
// even when not used as a melee/ranged weapon respectively
// attackdata should contain data regarding whether the attack is melee or not
case WeaponType.Melee:
if (item?.GetComponent<MeleeWeapon>() != null) { return true; }
break;
case WeaponType.Ranged:
if (item?.GetComponent<Projectile>() != null) { return true; }
break;
case WeaponType.HandheldRanged:
{
var projectile = item?.GetComponent<Projectile>();
if (projectile?.Launcher?.GetComponent<Holdable>() != null) { return true; }
}
break;
case WeaponType.Turret:
{
var projectile = item?.GetComponent<Projectile>();
if (projectile?.Launcher?.GetComponent<Turret>() != null) { return true; }
}
break;
case WeaponType.NoWeapon:
if (item == null) { return true; }
break;
}
}
}
return false;
}
return true;
}
@@ -0,0 +1,38 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItemInSubmarine : AbilityConditionData
{
private readonly SubmarineType? submarineType;
public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
if (conditionElement.Attribute("submarinetype") != null)
{
submarineType = conditionElement.GetAttributeEnum<SubmarineType>("submarinetype", SubmarineType.Player);
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
if (item.Submarine == null) { return false; }
if (submarineType.HasValue)
{
return item.Submarine.Info?.Type == submarineType.Value;
}
else
{
return true;
}
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false;
}
}
}
}
@@ -1,23 +0,0 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItemOutsideSubmarine : AbilityConditionData
{
public AbilityConditionItemOutsideSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
return item.Submarine == null || item.Submarine.TeamID != character.Info.TeamID;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false;
}
}
}
}
@@ -1,23 +0,0 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItemWreck : AbilityConditionData
{
public AbilityConditionItemWreck(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
return item.Submarine?.Info?.IsWreck ?? false;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false;
}
}
}
}
@@ -18,13 +18,13 @@ namespace Barotrauma.Abilities
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityString)?.String is string skillIdentifier)
if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier)
{
return MatchesConditionSpecific(skillIdentifier);
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityString));
LogAbilityConditionError(abilityObject, typeof(IAbilitySkillIdentifier));
return false;
}
}
@@ -30,9 +30,9 @@
public Character Character { get; set; }
}
interface IAbilityString
interface IAbilitySkillIdentifier
{
public string String { get; set; }
public string SkillIdentifier { get; set; }
}
interface IAbilityAffliction
@@ -16,173 +16,4 @@ namespace Barotrauma.Abilities
public Character Character { get; set; }
}
class AbilityItem : AbilityObject, IAbilityItem
{
public AbilityItem(Item item)
{
Item = item;
}
public Item Item { get; set; }
}
class AbilityValue : AbilityObject, IAbilityValue
{
public AbilityValue(float value)
{
Value = value;
}
public float Value { get; set; }
}
class AbilityAffliction : AbilityObject, IAbilityAffliction
{
public AbilityAffliction(Affliction affliction)
{
Affliction = affliction;
}
public Affliction Affliction { get; set; }
}
class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter
{
public AbilityAfflictionCharacter(Affliction affliction, Character character)
{
Affliction = affliction;
Character = character;
}
public Character Character { get; set; }
public Affliction Affliction { get; set; }
}
class AbilityValueItem : AbilityObject, IAbilityValue, IAbilityItemPrefab
{
public AbilityValueItem(float value, ItemPrefab itemPrefab)
{
Value = value;
ItemPrefab = itemPrefab;
}
public float Value { get; set; }
public ItemPrefab ItemPrefab { get; set; }
}
class AbilityItemPrefabItem : AbilityObject, IAbilityItem, IAbilityItemPrefab
{
public AbilityItemPrefabItem(Item item, ItemPrefab itemPrefab)
{
Item = item;
ItemPrefab = itemPrefab;
}
public Item Item { get; set; }
public ItemPrefab ItemPrefab { get; set; }
}
class AbilityValueString : AbilityObject, IAbilityValue, IAbilityString
{
public AbilityValueString(float value, string abilityString)
{
Value = value;
String = abilityString;
}
public float Value { get; set; }
public string String { get; set; }
}
class AbilityStringCharacter : AbilityObject, IAbilityCharacter, IAbilityString
{
public AbilityStringCharacter(string abilityString, Character character)
{
String = abilityString;
Character = character;
}
public Character Character { get; set; }
public string String { get; set; }
}
class AbilityValueAffliction : AbilityObject, IAbilityValue, IAbilityAffliction
{
public AbilityValueAffliction(float value, Affliction affliction)
{
Value = value;
Affliction = affliction;
}
public float Value { get; set; }
public Affliction Affliction { get; set; }
}
class AbilityValueMission : AbilityObject, IAbilityValue, IAbilityMission
{
public AbilityValueMission(float value, Mission mission)
{
Value = value;
Mission = mission;
}
public float Value { get; set; }
public Mission Mission { get; set; }
}
class AbilityLocation : AbilityObject, IAbilityLocation
{
public AbilityLocation(Location location)
{
Location = location;
}
public Location Location { get; set; }
}
// this is an exception class that should only be passed in this form, so classes that use it should cast into it directly
class AbilityAttackData : AbilityObject, IAbilityCharacter
{
public float DamageMultiplier { get; set; } = 1f;
public float AddedPenetration { get; set; } = 0f;
public List<Affliction> Afflictions { get; set; }
public bool ShouldImplode { get; set; } = false;
public Attack SourceAttack { get; }
public Character Character { get; set; }
public Character Attacker { get; set; }
public AbilityAttackData(Attack sourceAttack, Character character)
{
SourceAttack = sourceAttack;
Character = character;
}
}
class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem
{
public Character Character { get; set; }
public Character User { get; set; }
public Item Item { get; set; }
public AbilityApplyTreatment(Character user, Character target, Item item)
{
Character = target;
User = user;
Item = item;
}
}
class AbilityAttackResult : AbilityObject, IAbilityAttackResult
{
public AttackResult AttackResult { get; set; }
public AbilityAttackResult(AttackResult attackResult)
{
AttackResult = attackResult;
}
}
class AbilityCharacterSubmarine : AbilityObject, IAbilityCharacter, IAbilitySubmarine
{
public AbilityCharacterSubmarine(Character character, Submarine submarine)
{
Character = character;
Submarine = submarine;
}
public Character Character { get; set; }
public Submarine Submarine { get; set; }
}
}
@@ -87,7 +87,7 @@ namespace Barotrauma.Abilities
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
}
protected void LogabilityObjectMismatch()
protected void LogAbilityObjectMismatch()
{
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}");
}
@@ -23,7 +23,7 @@ namespace Barotrauma.Abilities
}
else
{
LogabilityObjectMismatch();
LogAbilityObjectMismatch();
}
}
}
@@ -29,7 +29,7 @@ namespace Barotrauma.Abilities
}
else
{
LogabilityObjectMismatch();
LogAbilityObjectMismatch();
}
}
}
@@ -41,7 +41,7 @@ namespace Barotrauma.Abilities
}
else
{
LogabilityObjectMismatch();
LogAbilityObjectMismatch();
}
}
}
@@ -37,7 +37,7 @@ namespace Barotrauma.Abilities
}
else
{
LogabilityObjectMismatch();
LogAbilityObjectMismatch();
}
}
}
@@ -34,7 +34,7 @@ namespace Barotrauma.Abilities
}
else
{
LogabilityObjectMismatch();
LogAbilityObjectMismatch();
}
}
}
@@ -1,6 +1,4 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -18,7 +16,7 @@ namespace Barotrauma.Abilities
if (abilityObject is AbilitySkillGain abilitySkillGain && abilitySkillGain.Character != Character)
{
if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; }
Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, gainedFromAbility: true);
Character.Info?.IncreaseSkillLevel(abilitySkillGain.SkillIdentifier, 1.0f, gainedFromAbility: true);
}
}
}
@@ -13,7 +13,7 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityString)?.String is string skillIdentifier)
if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier)
{
if (skillIdentifier != lastSkillIdentifier)
{
@@ -9,7 +9,7 @@ namespace Barotrauma.Abilities
class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly
{
// this should just be its own class, misleading to inherit here
private string tag;
private readonly string tag;
public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
tag = abilityElement.GetAttributeString("tag", "");
@@ -20,7 +20,7 @@ namespace Barotrauma.Abilities
if (Character.SelectedConstruction == null || !Character.SelectedConstruction.HasTag(tag)) { return; }
Character closestCharacter = null;
float closestDistance = float.MaxValue;
float closestDistance = squaredMaxDistance;
foreach (Character crewCharacter in Character.GetFriendlyCrew(Character))
{
@@ -984,8 +984,8 @@ namespace Barotrauma
commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) =>
{
if (Submarine.MainSub == null || Level.Loaded == null) return;
if (Level.Loaded.Type == LevelData.LevelType.Outpost)
if (Submarine.MainSub == null) { return; }
if (Level.Loaded?.Type == LevelData.LevelType.Outpost && GameMain.GameSession != null)
{
NewMessage("The teleportsub command is unavailable in outpost levels!", Color.Red);
return;
@@ -1001,6 +1001,11 @@ namespace Barotrauma
}
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
{
if (Level.Loaded == null)
{
NewMessage("Can't teleport the sub to the start of the level (no level loaded).", Color.Red);
return;
}
Vector2 pos = Level.Loaded.StartPosition;
if (Level.Loaded.StartOutpost != null)
{
@@ -1010,6 +1015,11 @@ namespace Barotrauma
}
else
{
if (Level.Loaded == null)
{
NewMessage("Can't teleport the sub to the end of the level (no level loaded).", Color.Red);
return;
}
Vector2 pos = Level.Loaded.EndPosition;
if (Level.Loaded.EndOutpost != null)
{
@@ -1189,6 +1199,7 @@ namespace Barotrauma
{
foreach (Item it in Item.ItemList)
{
if (it.GetComponent<GeneticMaterial>() != null) { continue; }
it.Condition = it.MaxCondition;
}
}, null, true));
@@ -355,7 +355,7 @@ namespace Barotrauma
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters();
// use multipliers here so that we can easily add them together without introducing multiplicative XP stacking
var experienceGainMultiplier = new AbilityValue(1f);
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier));
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
@@ -374,11 +374,11 @@ namespace Barotrauma
#endif
// apply money gains afterwards to prevent them from affecting XP gains
var moneyGainMission = new AbilityValueMission(1f, this);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, moneyGainMission));
crewCharacters.ForEach(c => moneyGainMission.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
campaign.Money += (int)(reward * moneyGainMission.Value);
campaign.Money += (int)(reward * missionMoneyGainMultiplier.Value);
foreach (Character character in crewCharacters)
{
@@ -534,4 +534,16 @@ namespace Barotrauma
cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2);
}
}
class AbilityMissionMoneyGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission
{
public AbilityMissionMoneyGainMultiplier(Mission mission, float moneyGainMultiplier)
{
Value = moneyGainMultiplier;
Mission = mission;
}
public float Value { get; set; }
public Mission Mission { get; set; }
}
}
@@ -15,6 +15,7 @@ namespace Barotrauma
private readonly float scatter;
private readonly float offset;
private readonly float delayBetweenSpawns;
private Vector2? spawnPos;
@@ -92,6 +93,7 @@ namespace Barotrauma
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f);
if (GameMain.NetworkMember != null)
{
@@ -538,7 +540,7 @@ namespace Barotrauma
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true);
}
}, Rand.Range(0f, amount / 2f));
}, delayBetweenSpawns * i);
}
}
@@ -45,6 +45,8 @@ namespace Barotrauma
requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null);
RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false);
GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start");
}
public void AddTarget(string tag, Entity target)
@@ -229,5 +231,11 @@ namespace Barotrauma
}
return false;
}
public override void Finished()
{
base.Finished();
GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Finished:{CurrentActionIndex}");
}
}
}
@@ -133,7 +133,7 @@ namespace Barotrauma.Extensions
return source.Count(predicate) > 1;
}
}
public static IEnumerable<T> ToEnumerable<T>(this T item)
{
yield return item;
@@ -196,5 +196,28 @@ namespace Barotrauma.Extensions
}
return -1;
}
/// <summary>
/// Same as FirstOrDefault but will always return null instead of default(T) when no element is found
/// </summary>
public static T? FirstOrNull<T>(this IEnumerable<T> source, Func<T, bool> predicate) where T : struct
{
if (source.FirstOrDefault(predicate) is var first && !first.Equals(default(T)))
{
return first;
}
return null;
}
public static T? FirstOrNull<T>(this IEnumerable<T> source) where T : struct
{
if (source.FirstOrDefault() is var first && !first.Equals(default(T)))
{
return first;
}
return null;
}
}
}
@@ -149,6 +149,13 @@ namespace Barotrauma
SetConsent(Consent.Error);
}
if (!SteamManager.IsInitialized)
{
DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam authentication ticket (not connected to Steam).");
SetConsent(Consent.Error);
return;
}
string authTicketStr;
try
{
@@ -183,7 +190,7 @@ namespace Barotrauma
return;
}
var response = ((Task<IRestResponse>)t).Result;
if (!t.TryGetResult(out IRestResponse response)) { return; }
if (!CheckResponse(response))
{
SetConsent(Consent.Error);
@@ -367,7 +367,6 @@ namespace Barotrauma
+ GameMain.Version.ToString()
+ exeName + ":"
+ ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash) + ":"
+ AssemblyInfo.GitBranch + ":"
+ AssemblyInfo.GitRevision + ":"
+ buildConfiguration);
}
@@ -47,7 +47,9 @@ namespace Barotrauma
{
if (order.TargetEntity == null)
{
DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace());
string message = $"Attempted to add a \"{order.Name}\" order with no target entity to CrewManager!\n{Environment.StackTrace.CleanupStackTrace()}";
DebugConsole.AddWarning(message);
GameAnalyticsManager.AddErrorEventOnce("CrewManager.AddOrder:OrderTargetEntityNull", GameAnalyticsManager.ErrorSeverity.Error, message);
return false;
}
@@ -185,6 +187,10 @@ namespace Barotrauma
public void InitRound()
{
#if CLIENT
GUIContextMenu.CurrentContextMenu = null;
#endif
characters.Clear();
List<WayPoint> spawnWaypoints = null;
@@ -437,17 +443,17 @@ namespace Barotrauma
return filteredCharacters
// 1. Prioritize those who are on the same submarine than the controlled character
.OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine)
// 2. Prioritize those who have been given the same maintenance or operate order as now issued
.ThenByDescending(c => c.CurrentOrders.Any(o =>
o.Order != null && o.Order.Identifier == order.Identifier &&
(order.Category == OrderCategory.Maintenance || order.Category == OrderCategory.Operate)))
// 2. Prioritize those who are already ordered to operate the device
.ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.Any(o => o.Order != null && o.Order.Identifier == order.Identifier && o.Order.TargetEntity == order.TargetEntity))
// 3. Prioritize those with the appropriate job for the order
.ThenByDescending(c => order.HasAppropriateJob(c))
// 4. Prioritize bots over player controlled characters
// 4. Prioritize those who don't yet have another Operate order of the same kind (which allows quick-assigning multiple Operate orders to different characters)
.ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.None(o => o.Order != null && o.Order.Identifier == order.Identifier))
// 5. Prioritize bots over player controlled characters
.ThenByDescending(c => c.IsBot)
// 5. Use the priority value of the current objective
// 6. Use the priority value of the current objective
.ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0)
// 6. Prioritize those with the best skill for the order
// 7. Prioritize those with the best skill for the order
.ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill));
}
@@ -78,10 +78,11 @@ namespace Barotrauma
//there can be no events before this time has passed during the 1st campaign round
const float FirstRoundEventDelay = 0.0f;
public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub }
public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub, MedicalClinic }
public readonly CargoManager CargoManager;
public UpgradeManager UpgradeManager;
public MedicalClinic MedicalClinic;
public List<Faction> Factions;
@@ -176,6 +177,7 @@ namespace Barotrauma
{
Money = InitialMoney;
CargoManager = new CargoManager(this);
MedicalClinic = new MedicalClinic(this);
}
/// <summary>
@@ -192,6 +192,37 @@ namespace Barotrauma
}
#endif
}
public static List<SubmarineInfo> GetCampaignSubs()
{
bool isSubmarineVisible(SubmarineInfo s)
=> !GameMain.NetworkMember.ServerSettings.HiddenSubs.Any(h
=> s.Name.Equals(h, StringComparison.OrdinalIgnoreCase));
List<SubmarineInfo> availableSubs =
SubmarineInfo.SavedSubmarines
.Where(s =>
s.IsCampaignCompatible
&& isSubmarineVisible(s))
.ToList();
if (!availableSubs.Any())
{
//None of the available subs were marked as campaign-compatible, just include all visible subs
availableSubs.AddRange(
SubmarineInfo.SavedSubmarines
.Where(isSubmarineVisible));
}
if (!availableSubs.Any())
{
//No subs are visible at all! Just make the selected one available
availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub);
}
return availableSubs;
}
}
}
@@ -411,9 +411,9 @@ namespace Barotrauma
GameAnalyticsManager.ProgressionStatus.Start,
GameMode?.Name ?? "none");
string eventId = "StartRound:GameMode:" + (GameMode?.Name ?? "none") + ":";
string eventId = "StartRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":";
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0));
foreach (Mission mission in missions)
{
@@ -421,6 +421,17 @@ namespace Barotrauma
}
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"));
if (GameMode is CampaignMode campaignMode)
{
if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled)
{
GameAnalyticsManager.AddDesignEvent(eventId + "RadiationEnabled");
}
else
{
GameAnalyticsManager.AddDesignEvent(eventId + "RadiationDisabled");
}
}
#if CLIENT
if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
@@ -457,6 +468,8 @@ namespace Barotrauma
}
}
ReadyCheck.ReadyCheckCooldown = DateTime.MinValue;
GUI.PreventPauseMenuToggle = false;
HintManager.OnRoundStarted();
@@ -895,14 +908,7 @@ namespace Barotrauma
((CampaignMode)GameMode).Save(doc.Root);
try
{
doc.SaveSafe(filePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving gamesession to \"" + filePath + "\" failed!", e);
}
doc.SaveSafe(filePath, throwExceptions: true);
}
/*public void Load(XElement saveElement)
@@ -0,0 +1,348 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
internal partial class MedicalClinic
{
public enum NetworkHeader
{
REQUEST_AFFLICTIONS,
REQUEST_PENDING,
ADD_PENDING,
REMOVE_PENDING,
CLEAR_PENDING,
HEAL_PENDING
}
public enum AfflictionSeverity
{
Low,
Medium,
High
}
public enum MessageFlag
{
Response, // responding to your request
Announce // responding to someone else's request
}
public enum HealRequestResult
{
Unknown, // everything is not ok
Success, // everything ok
InsufficientFunds, // not enough money
Refused // the outpost has refused to provide medical assistance
}
[NetworkSerialize]
public struct NetHealRequest : INetSerializableStruct
{
public HealRequestResult Result;
}
[NetworkSerialize]
public struct NetRemovedAffliction : INetSerializableStruct
{
public NetCrewMember CrewMember;
public NetAffliction Affliction;
}
public struct NetPendingCrew : INetSerializableStruct
{
[NetworkSerialize(ArrayMaxSize = CrewManager.MaxCrewSize)]
public NetCrewMember[] CrewMembers;
}
public struct NetAffliction : INetSerializableStruct
{
[NetworkSerialize]
public string Identifier;
[NetworkSerialize]
public ushort Strength;
[NetworkSerialize]
public ushort Price;
public AfflictionSeverity AfflictionSeverity
{
get
{
if (Prefab is null) { return AfflictionSeverity.Low; }
float normalizedStrength = Strength / Prefab.MaxStrength;
// lesser than 0.1
if (normalizedStrength <= 0.1)
{
return AfflictionSeverity.Low;
}
// between 0.1 and 0.5
if (normalizedStrength > 0.1f && normalizedStrength < 0.5f)
{
return AfflictionSeverity.Medium;
}
// greater than 0.5
return AfflictionSeverity.High;
}
}
public Affliction Affliction
{
set
{
Identifier = value.Identifier;
Strength = (ushort)Math.Ceiling(value.Strength);
Price = (ushort)(Strength * value.Prefab.HealCostMultiplier);
}
}
private AfflictionPrefab? cachedPrefab;
public AfflictionPrefab? Prefab
{
get
{
if (cachedPrefab is { } cached) { return cached; }
foreach (AfflictionPrefab prefab in AfflictionPrefab.List)
{
if (prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase))
{
cachedPrefab = prefab;
return prefab;
}
}
return null;
}
set
{
cachedPrefab = value;
Identifier = value?.Identifier ?? string.Empty;
Strength = 0;
Price = 0;
}
}
public readonly bool AfflictionEquals(AfflictionPrefab prefab)
{
return prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase);
}
public readonly bool AfflictionEquals(NetAffliction affliction)
{
return affliction.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase);
}
}
public struct NetCrewMember : INetSerializableStruct
{
[NetworkSerialize]
public int CharacterInfoID;
[NetworkSerialize]
public NetAffliction[] Afflictions;
public CharacterInfo CharacterInfo
{
set => CharacterInfoID = value.GetIdentifierUsingOriginalName();
}
public readonly CharacterInfo? FindCharacterInfo(ImmutableArray<CharacterInfo> crew)
{
foreach (CharacterInfo info in crew)
{
if (info.GetIdentifierUsingOriginalName() == CharacterInfoID)
{
return info;
}
}
return null;
}
public readonly bool CharacterEquals(NetCrewMember crewMember)
{
return crewMember.CharacterInfoID == CharacterInfoID;
}
}
private readonly CampaignMode? campaign;
public MedicalClinic(CampaignMode campaign)
{
this.campaign = campaign;
}
public readonly List<NetCrewMember> PendingHeals = new List<NetCrewMember>();
public Action? OnUpdate;
private static bool IsOutpostInCombat()
{
if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; }
IEnumerable<Character> crew = GetCrewCharacters().Where(c => c.Character != null).Select(c => c.Character).ToImmutableHashSet();
foreach (Character npc in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.FriendlyNPC))
{
bool isInCombatWithCrew = !npc.IsInstigator && npc.AIController is HumanAIController { ObjectiveManager: { CurrentObjective: AIObjectiveCombat combatObjective } } && crew.Contains(combatObjective.Enemy);
if (isInCombatWithCrew) { return true; }
}
return false;
}
private HealRequestResult HealAllPending(bool force = false)
{
int totalCost = GetTotalCost();
if (!force)
{
if (GetMoney() < totalCost) { return HealRequestResult.InsufficientFunds; }
if (IsOutpostInCombat()) { return HealRequestResult.Refused; }
}
ImmutableArray<CharacterInfo> crew = GetCrewCharacters();
foreach (NetCrewMember crewMember in PendingHeals)
{
CharacterInfo? targetCharacter = crewMember.FindCharacterInfo(crew);
if (!(targetCharacter?.Character is { CharacterHealth: { } health })) { continue; }
foreach (NetAffliction affliction in crewMember.Afflictions)
{
health.ReduceAffliction(null, affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength);
}
}
if (campaign != null)
{
campaign.Money -= totalCost;
}
ClearPendingHeals();
return HealRequestResult.Success;
}
private void ClearPendingHeals()
{
PendingHeals.Clear();
}
private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction)
{
foreach (NetCrewMember listMember in PendingHeals.ToList())
{
PendingHeals.Remove(listMember);
NetCrewMember pendingMember = listMember;
if (pendingMember.CharacterEquals(crewMember))
{
List<NetAffliction> newAfflictions = new List<NetAffliction>();
foreach (NetAffliction pendingAffliction in pendingMember.Afflictions)
{
if (pendingAffliction.AfflictionEquals(affliction)) { continue; }
newAfflictions.Add(pendingAffliction);
}
pendingMember.Afflictions = newAfflictions.ToArray();
}
if (!pendingMember.Afflictions.Any()) { continue; }
PendingHeals.Add(pendingMember);
}
}
private void InsertPendingCrewMember(NetCrewMember crewMember)
{
if (PendingHeals.FirstOrNull(m => m.CharacterEquals(crewMember)) is { } foundHeal)
{
PendingHeals.Remove(foundHeal);
}
PendingHeals.Add(crewMember);
}
private NetAffliction[] GetAllAfflictions(CharacterHealth health)
{
IEnumerable<Affliction> rawAfflictions = health.GetAllAfflictions().Where(a => !a.Prefab.IsBuff && a.Strength > GetShowTreshold(a));
List<NetAffliction> afflictions = new List<NetAffliction>();
foreach (Affliction affliction in rawAfflictions)
{
NetAffliction newAffliction;
if (afflictions.FirstOrNull(netAffliction => netAffliction.AfflictionEquals(affliction.Prefab)) is { } foundAffliction)
{
afflictions.Remove(foundAffliction);
foundAffliction.Strength += (ushort)affliction.Strength;
foundAffliction.Price += (ushort)GetAdjustedPrice((int)(affliction.Prefab.HealCostMultiplier * affliction.Strength));
newAffliction = foundAffliction;
}
else
{
newAffliction = new NetAffliction { Affliction = affliction };
newAffliction.Price = (ushort)GetAdjustedPrice(newAffliction.Price);
}
afflictions.Add(newAffliction);
}
return afflictions.ToArray();
static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold));
}
public int GetTotalCost() => PendingHeals.SelectMany(h => h.Afflictions).Aggregate(0, (current, affliction) => current + affliction.Price);
private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue;
public int GetMoney() => campaign?.Money ?? 0;
public static ImmutableArray<CharacterInfo> GetCrewCharacters()
{
#if DEBUG && CLIENT
if (Screen.Selected is TestScreen)
{
return TestInfos.ToImmutableArray();
}
#endif
return Character.CharacterList.Where(c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(c => c.Info).ToImmutableArray();
}
#if DEBUG && CLIENT
private static readonly CharacterInfo[] TestInfos =
{
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human")
};
private static readonly NetAffliction[] TestAfflictions =
{
new NetAffliction { Identifier = "internaldamage", Strength = 80, Price = 10 },
new NetAffliction { Identifier = "blunttrauma", Strength = 50, Price = 10 },
new NetAffliction { Identifier = "lacerations", Strength = 20, Price = 10 },
new NetAffliction { Identifier = "burn", Strength = 10, Price = 10 }
};
#endif
}
}
@@ -144,16 +144,6 @@ namespace Barotrauma.Items.Components
if (linkedGap == null)
{
Rectangle rect = item.Rect;
if (IsHorizontal)
{
rect.Y += 5;
rect.Height += 10;
}
else
{
rect.X -= 5;
rect.Width += 10;
}
linkedGap = new Gap(rect, !IsHorizontal, Item.Submarine)
{
Submarine = item.Submarine
@@ -55,17 +55,22 @@ namespace Barotrauma.Items.Components
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize("1,3", true, "Minumum and maximum amount of items or creatures to spawn in one attempt")]
public Vector2 SpawnAmountRange { get; set; }
[Editable(MinValueInt = int.MinValue, MaxValueInt = int.MaxValue), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned")]
[Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, true, "Total maximum amount of items or creatures that can be spawned. 0 = unrestricted.")]
public int MaximumAmount { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")]
[Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned. 0 = unrestricted.")]
public int MaximumAmountInArea { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")]
public float MaximumAmountRangePadding { get; set; }
[Serialize(true, true, "")]
public bool CanSpawn { get; set; } = true;
private float SpawnTimer;
private float? SpawnTimerGoal;
private float spawnTimer;
private float? spawnTimerGoal;
private int spawnedAmount = 0;
public EntitySpawnerComponent(Item item, XElement element) : base(item, element)
{
@@ -115,15 +120,15 @@ namespace Barotrauma.Items.Components
if (minTime < 0 && maxTime < 0) { return; }
SpawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced);
spawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced);
SpawnTimer += deltaTime;
spawnTimer += deltaTime;
if (SpawnTimer > SpawnTimerGoal)
if (spawnTimer > spawnTimerGoal)
{
Spawn();
SpawnTimerGoal = null;
SpawnTimer = 0;
spawnTimerGoal = null;
spawnTimer = 0;
}
}
@@ -149,12 +154,12 @@ namespace Barotrauma.Items.Components
private RectangleF GetAreaRectangle(Vector2 size, Vector2 offset, bool draw)
{
Vector2 pos = item.WorldPosition;
pos += offset;
if (draw)
{
pos.Y = -pos.Y;
}
pos += offset;
RectangleF rect = new RectangleF(pos.X - size.X / 2f, pos.Y - size.Y / 2f, size.X, size.Y);
return rect;
}
@@ -162,6 +167,7 @@ namespace Barotrauma.Items.Components
private bool CanSpawnMore()
{
if (!CanSpawn) { return false; }
if (MaximumAmount > 0 && spawnedAmount >= MaximumAmount) { return false; }
if (OnlySpawnWhenCrewInRange)
{
@@ -171,10 +177,9 @@ namespace Barotrauma.Items.Components
}
}
if (MaximumAmount < 0) { return true; }
if (MaximumAmountInArea <= 0) { return true; }
int amount;
if (!string.IsNullOrWhiteSpace(SpeciesName))
{
amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName.Equals(SpeciesName, StringComparison.OrdinalIgnoreCase) && IsInRange(c.WorldPosition, crewArea: false, rangePad: true));
@@ -188,13 +193,12 @@ namespace Barotrauma.Items.Components
return false;
}
return amount < MaximumAmount;
return amount < MaximumAmountInArea;
}
private bool IsInRange(Vector2 worldPos, bool crewArea = false, bool rangePad = false)
{
Vector2 offset = crewArea ? CrewAreaOffset : SpawnAreaOffset;
offset.Y = -offset.Y;
switch (crewArea ? CrewAreaShape : SpawnAreaShape)
{
case AreaShape.Circle:
@@ -269,6 +273,7 @@ namespace Barotrauma.Items.Components
string[] allSpecies = SpeciesName.Split(',');
string species = allSpecies.GetRandom().Trim();
Entity.Spawner?.AddToSpawnQueue(species, pos);
spawnedAmount++;
}
else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
{
@@ -283,6 +288,7 @@ namespace Barotrauma.Items.Components
}
Entity.Spawner?.AddToSpawnQueue(prefab, pos, item.Submarine);
spawnedAmount++;
}
}
}
@@ -74,6 +74,7 @@ namespace Barotrauma.Items.Components
a.Identifier.Equals(Effect, StringComparison.OrdinalIgnoreCase) ||
a.AfflictionType.Equals(Effect, StringComparison.OrdinalIgnoreCase)).GetRandom();
}
Tainted = true;
}
[Serialize(3.0f, false)]
@@ -94,37 +95,28 @@ namespace Barotrauma.Items.Components
if (targetCharacter != null) { return; }
if (tainted)
{
if (selectedTaintedEffect != null)
{
float selectedTaintedEffectStrength = item.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength;
character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength));
var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (existingAffliction != null)
{
existingAffliction.Strength = selectedTaintedEffectStrength;
}
targetCharacter = character;
#if SERVER
item.CreateServerEvent(this);
#endif
}
}
if (selectedEffect != null)
{
ApplyStatusEffects(ActionType.OnWearing, 1.0f);
float selectedEffectStrength = item.ConditionPercentage / 100.0f * selectedEffect.MaxStrength;
character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength));
var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (existingAffliction != null)
{
existingAffliction.Strength = selectedEffectStrength;
}
targetCharacter = character;
ApplyStatusEffects(ActionType.OnWearing, 1.0f);
float selectedEffectStrength = GetCombinedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null) { affliction.Strength = selectedEffectStrength; }
#if SERVER
item.CreateServerEvent(this);
#endif
}
if (tainted && selectedTaintedEffect != null)
{
float selectedTaintedEffectStrength = GetCombinedTaintedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (affliction != null) { affliction.Strength = selectedTaintedEffectStrength; }
targetCharacter = character;
#if SERVER
item.CreateServerEvent(this);
#endif
}
foreach (Item containedItem in item.ContainedItems)
{
@@ -142,13 +134,14 @@ namespace Barotrauma.Items.Components
(rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface)))
{
item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter);
targetCharacter.CharacterHealth.ReduceAffliction(null, selectedEffect.Identifier, selectedEffect.MaxStrength);
if (tainted)
{
targetCharacter.CharacterHealth.ReduceAffliction(null, selectedTaintedEffect.Identifier, selectedTaintedEffect.MaxStrength);
}
targetCharacter = null;
IsActive = false;
var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null) { affliction.Strength = GetCombinedEffectStrength(); }
var taintedAffliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (taintedAffliction != null) { taintedAffliction.Strength = GetCombinedTaintedEffectStrength(); }
targetCharacter = null;
}
}
}
@@ -184,6 +177,36 @@ namespace Barotrauma.Items.Components
}
}
private float GetCombinedEffectStrength()
{
float effectStrength = 0.0f;
foreach (Item otherItem in targetCharacter.Inventory.FindAllItems(recursive: true))
{
var geneticMaterial = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial == null || !geneticMaterial.IsActive) { continue; }
if (geneticMaterial.selectedEffect == selectedEffect)
{
effectStrength += otherItem.ConditionPercentage / 100.0f * selectedEffect.MaxStrength;
}
}
return effectStrength;
}
private float GetCombinedTaintedEffectStrength()
{
float taintedEffectStrength = 0.0f;
foreach (Item otherItem in targetCharacter.Inventory.FindAllItems(recursive: true))
{
var geneticMaterial = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial == null || !geneticMaterial.IsActive) { continue; }
if (selectedTaintedEffect != null && geneticMaterial.selectedTaintedEffect == selectedTaintedEffect)
{
taintedEffectStrength += otherItem.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength;
}
}
return taintedEffectStrength;
}
private float GetTaintedProbabilityOnRefine(Character user)
{
if (user == null) { return 1.0f; }
@@ -278,7 +278,7 @@ namespace Barotrauma.Items.Components
for (int i = 0, j = 0; i < maxSides; i++)
{
if (!occupiedSides.IsBitSet((TileSide) (1 << i)))
if (!occupiedSides.HasFlag((TileSide) (1 << i)))
{
pool[j] = i;
j++;
@@ -303,7 +303,7 @@ namespace Barotrauma.Items.Components
public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4;
public bool IsSideBlocked(TileSide side) => BlockedSides.IsBitSet(side) || Sides.IsBitSet(side);
public bool IsSideBlocked(TileSide side) => BlockedSides.HasFlag(side) || Sides.HasFlag(side);
public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int) pos.X - Size / 2, (int) pos.Y + Size / 2, Size, Size);
}
@@ -774,7 +774,7 @@ namespace Barotrauma.Items.Components
TileSide oppositeSide = connectingSide.GetOppositeSide();
if (otherVine.BlockedSides.IsBitSet(connectingSide))
if (otherVine.BlockedSides.HasFlag(connectingSide))
{
newVine.BlockedSides |= oppositeSide;
continue;
@@ -166,7 +166,7 @@ namespace Barotrauma.Items.Components
Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, item.body.Density)
{
BodyType = BodyType.Dynamic,
CollidesWith = Physics.CollisionCharacter,
CollidesWith = Physics.CollisionCharacter | Physics.CollisionProjectile,
CollisionCategories = Physics.CollisionItemBlocking,
Enabled = false,
UserData = this
@@ -604,7 +604,14 @@ namespace Barotrauma.Items.Components
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int currentlyAttachedCount = Item.ItemList.Count(
i => i.Submarine == attachTarget?.Submarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier);
if (currentlyAttachedCount >= maxAttachableCount)
if (maxAttachableCount == 0)
{
#if CLIENT
GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red);
#endif
return false;
}
else if (currentlyAttachedCount >= maxAttachableCount)
{
#if CLIENT
GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
@@ -801,7 +808,7 @@ namespace Barotrauma.Items.Components
equipLimb = picker.AnimController.GetLimb(LimbType.Torso);
}
if (equipLimb != null)
if (equipLimb != null && !equipLimb.Removed)
{
float itemAngle = (equipLimb.Rotation + holdAngle * picker.AnimController.Dir);
@@ -814,6 +821,11 @@ namespace Barotrauma.Items.Components
}
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
//do nothing
}
public override void FlipX(bool relativeToSub)
{
handlePos[0].X = -handlePos[0].X;
@@ -115,7 +115,7 @@ namespace Barotrauma.Items.Components
reloadTimer /= (1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier));
item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall;
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking;
item.body.FarseerBody.OnCollision += OnCollision;
item.body.FarseerBody.IsBullet = true;
item.body.PhysEnabled = true;
@@ -361,6 +361,10 @@ namespace Barotrauma.Items.Components
}
hitTargets.Add(targetItem);
}
else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
{
hitTargets.Add(holdable.Item);
}
else
{
return false;
@@ -411,6 +415,14 @@ namespace Barotrauma.Items.Components
if (targetItem.Removed) { return; }
Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
}
else if (target.UserData is Holdable holdable && holdable.CanPush)
{
if (holdable.Item.Removed) { return; }
Attack.DoDamage(User, holdable.Item, item.WorldPosition, 1.0f);
RestoreCollision();
hitting = false;
User = null;
}
else
{
return;
@@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components
if (PickingTime > 0.0f)
{
var abilityPickingTime = new AbilityValueItem(PickingTime, item.Prefab);
var abilityPickingTime = new AbilityItemPickingTime(PickingTime, item.Prefab);
picker.CheckTalents(AbilityEffectType.OnItemPicked, abilityPickingTime);
if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped))
@@ -300,4 +300,15 @@ namespace Barotrauma.Items.Components
}
}
}
class AbilityItemPickingTime : AbilityObject, IAbilityValue, IAbilityItemPrefab
{
public AbilityItemPickingTime(float pickingTime, ItemPrefab itemPrefab)
{
Value = pickingTime;
ItemPrefab = itemPrefab;
}
public float Value { get; set; }
public ItemPrefab ItemPrefab { get; set; }
}
}
@@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components
return MathHelper.ToRadians(spread);
}
private readonly List<Body> limbBodies = new List<Body>();
private readonly List<Body> ignoredBodies = new List<Body>();
public override bool Use(float deltaTime, Character character = null)
{
tryingToCharge = true;
@@ -172,8 +172,8 @@ namespace Barotrauma.Items.Components
if (character != null)
{
var abilityItem = new AbilityItem(item);
character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityItem);
var abilityRangedWeapon = new AbilityRangedWeapon(item);
character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon);
}
if (item.AiTarget != null)
@@ -182,11 +182,20 @@ namespace Barotrauma.Items.Components
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
}
limbBodies.Clear();
ignoredBodies.Clear();
foreach (Limb l in character.AnimController.Limbs)
{
if (l.IsSevered) { continue; }
limbBodies.Add(l.body.FarseerBody);
ignoredBodies.Add(l.body.FarseerBody);
}
foreach (Item heldItem in character.HeldItems)
{
var holdable = heldItem.GetComponent<Holdable>();
if (holdable?.Pusher != null)
{
ignoredBodies.Add(holdable.Pusher.FarseerBody);
}
}
float degreeOfFailure = 1.0f - DegreeOfSuccess(character);
@@ -211,7 +220,7 @@ namespace Barotrauma.Items.Components
}
float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier);
projectile.Launcher = item;
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier);
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier);
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
if (i == 0)
{
@@ -270,4 +279,12 @@ namespace Barotrauma.Items.Components
partial void LaunchProjSpecific();
}
class AbilityRangedWeapon : AbilityObject, IAbilityItem
{
public AbilityRangedWeapon(Item item)
{
Item = item;
}
public Item Item { get; set; }
}
}
@@ -521,7 +521,7 @@ namespace Barotrauma.Items.Components
if (!fixableEntities.Contains("structure") && !fixableEntities.Contains(targetStructure.Prefab.Identifier)) { return true; }
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetStructure });
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure);
FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex);
float structureFixAmount = StructureFixAmount;
@@ -589,8 +589,7 @@ namespace Barotrauma.Items.Components
closestLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
}
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse,
closestLimb == null ? new ISerializableEntity[] { targetCharacter } : new ISerializableEntity[] { targetCharacter, closestLimb });
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetCharacter, limb: closestLimb);
FixCharacterProjSpecific(user, deltaTime, targetCharacter);
return true;
}
@@ -606,7 +605,7 @@ namespace Barotrauma.Items.Components
}
targetLimb.character.LastDamageSource = item;
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetLimb.character, targetLimb });
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetLimb.character, limb: targetLimb);
FixCharacterProjSpecific(user, deltaTime, targetLimb.character);
return true;
}
@@ -645,7 +644,7 @@ namespace Barotrauma.Items.Components
targetItem.IsHighlighted = true;
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects);
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem);
if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f))
{
@@ -682,7 +681,7 @@ namespace Barotrauma.Items.Components
Reset();
return true;
}
if (leak.Submarine == null)
if (leak.Submarine == null || leak.Submarine != character.Submarine)
{
Reset();
return true;
@@ -836,32 +835,44 @@ namespace Barotrauma.Items.Components
}
}
private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, IEnumerable<ISerializableEntity> targets)
private static List<ISerializableEntity> currentTargets = new List<ISerializableEntity>();
private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, Item targetItem = null, Character character = null, Limb limb = null, Structure structure = null)
{
if (statusEffectLists == null) { return; }
if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffects)) { return; }
currentTargets.Clear();
foreach (StatusEffect effect in statusEffects)
{
effect.SetUser(user);
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
effect.Apply(actionType, deltaTime, item, targets);
if (targetItem != null)
{
currentTargets.AddRange(targetItem.AllPropertyObjects);
}
if (structure != null)
{
currentTargets.Add(structure);
}
effect.Apply(actionType, deltaTime, item, currentTargets);
}
else if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Character));
currentTargets.Add(character);
effect.Apply(actionType, deltaTime, item, currentTargets);
}
else if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{
effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Limb));
currentTargets.Add(limb);
effect.Apply(actionType, deltaTime, item, currentTargets);
}
#if CLIENT
if (user == null) { return; }
// Hard-coded progress bars for welding doors stuck.
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
foreach (ISerializableEntity target in targets)
foreach (ISerializableEntity target in currentTargets)
{
if (!(target is Door door)) { continue; }
if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; }
@@ -284,6 +284,43 @@ namespace Barotrauma.Items.Components
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
ParseMsg();
string inheritRequiredSkillsFrom = element.GetAttributeString("inheritrequiredskillsfrom", "");
if (!string.IsNullOrEmpty(inheritRequiredSkillsFrom))
{
var component = item.Components.Find(ic => ic.Name.Equals(inheritRequiredSkillsFrom, StringComparison.OrdinalIgnoreCase));
if (component == null)
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found.");
}
else
{
requiredSkills = component.requiredSkills;
}
}
string inheritStatusEffectsFrom = element.GetAttributeString("inheritstatuseffectsfrom", "");
if (!string.IsNullOrEmpty(inheritStatusEffectsFrom))
{
var component = item.Components.Find(ic => ic.Name.Equals(inheritStatusEffectsFrom, StringComparison.OrdinalIgnoreCase));
if (component == null)
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found.");
}
else if (component.statusEffectLists != null)
{
statusEffectLists ??= new Dictionary<ActionType, List<StatusEffect>>();
foreach (KeyValuePair<ActionType, List<StatusEffect>> kvp in component.statusEffectLists)
{
if (!statusEffectLists.TryGetValue(kvp.Key, out List<StatusEffect> effectList))
{
effectList = new List<StatusEffect>();
statusEffectLists.Add(kvp.Key, effectList);
}
effectList.AddRange(kvp.Value);
}
}
}
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -315,19 +352,8 @@ namespace Barotrauma.Items.Components
requiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0)));
break;
case "statuseffect":
var statusEffect = StatusEffect.Load(subElement, item.Name);
if (statusEffectLists == null) statusEffectLists = new Dictionary<ActionType, List<StatusEffect>>();
List<StatusEffect> effectList;
if (!statusEffectLists.TryGetValue(statusEffect.type, out effectList))
{
effectList = new List<StatusEffect>();
statusEffectLists.Add(statusEffect.type, effectList);
}
effectList.Add(statusEffect);
statusEffectLists ??= new Dictionary<ActionType, List<StatusEffect>>();
LoadStatusEffect(subElement);
break;
default:
if (LoadElemProjSpecific(subElement)) { break; }
@@ -342,6 +368,17 @@ namespace Barotrauma.Items.Components
break;
}
}
void LoadStatusEffect(XElement subElement)
{
var statusEffect = StatusEffect.Load(subElement, item.Name);
if (!statusEffectLists.TryGetValue(statusEffect.type, out List<StatusEffect> effectList))
{
effectList = new List<StatusEffect>();
statusEffectLists.Add(statusEffect.type, effectList);
}
effectList.Add(statusEffect);
}
}
private void SetActiveState(bool isActive)
@@ -399,6 +436,8 @@ namespace Barotrauma.Items.Components
return false;
}
public virtual bool UpdateWhenInactive => false;
//called when isActive is true and condition > 0.0f
public virtual void Update(float deltaTime, Camera cam)
{
@@ -798,7 +837,10 @@ namespace Barotrauma.Items.Components
foreach (ItemComponent ic in item.Components)
{
if (ic.statusEffectLists == null || !ic.statusEffectLists.TryGetValue(ActionType.OnBroken, out List<StatusEffect> brokenEffects)) { continue; }
brokenEffects.ForEach(e => e.SetUser(user));
foreach (var brokenEffect in brokenEffects)
{
brokenEffect.SetUser(user);
}
}
}
@@ -1007,7 +1049,8 @@ namespace Barotrauma.Items.Components
return 0.0f;
}
}
return 1.0f;
// Prefer items with the same identifier as the contained items'
return container.ContainsItemsWithSameIdentifier(i) ? 1.0f : 0.5f;
}
};
containObjective.Abandoned += () => aiController.IgnoredItems.Add(container.Item);
@@ -208,12 +208,13 @@ namespace Barotrauma.Items.Components
public override bool RecreateGUIOnResolutionChange => true;
public List<RelatedItem> ContainableItems { get; }
public ItemContainer(Item item, XElement element)
: base(item, element)
{
int totalCapacity = capacity;
List<RelatedItem> containableItems = null;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -225,8 +226,8 @@ namespace Barotrauma.Items.Components
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers.");
continue;
}
containableItems ??= new List<RelatedItem>();
containableItems.Add(containable);
ContainableItems ??= new List<RelatedItem>();
ContainableItems.Add(containable);
break;
case "subcontainer":
totalCapacity += subElement.GetAttributeInt("capacity", 1);
@@ -237,7 +238,7 @@ namespace Barotrauma.Items.Components
slotRestrictions = new SlotRestrictions[totalCapacity];
for (int i = 0; i < capacity; i++)
{
slotRestrictions[i] = new SlotRestrictions(maxStackSize, containableItems);
slotRestrictions[i] = new SlotRestrictions(maxStackSize, ContainableItems);
}
int subContainerIndex = capacity;
@@ -344,6 +345,19 @@ namespace Barotrauma.Items.Components
return slotRestrictions[index].MatchesItem(itemPrefab);
}
public bool ContainsItemsWithSameIdentifier(Item item)
{
if (item == null) { return false; }
foreach (var containedItem in Inventory.AllItems)
{
if (containedItem.Prefab.Identifier == item.Prefab.Identifier)
{
return true;
}
}
return false;
}
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public override void Update(float deltaTime, Camera cam)
@@ -432,7 +446,7 @@ namespace Barotrauma.Items.Components
}
}
}
var abilityItem = new AbilityItem(item);
var abilityItem = new AbilityItemContainer(item);
character.CheckTalents(AbilityEffectType.OnOpenItemContainer, abilityItem);
return base.Select(character);
@@ -494,6 +508,21 @@ namespace Barotrauma.Items.Components
IsActive = true;
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
switch (connection.Name)
{
case "activate":
case "use":
case "trigger_in":
if (signal.value != "0")
{
item.Use(1.0f, signal.sender);
}
break;
}
}
public void SetContainedItemPositions()
{
Vector2 transformedItemPos = ItemPos * item.Scale;
@@ -689,7 +718,6 @@ namespace Barotrauma.Items.Components
}
}
protected override void ShallowRemoveComponentSpecific()
{
}
@@ -743,4 +771,13 @@ namespace Barotrauma.Items.Components
return componentElement;
}
}
class AbilityItemContainer : AbilityObject, IAbilityItem
{
public AbilityItemContainer(Item item)
{
Item = item;
}
public Item Item { get; set; }
}
}
@@ -370,18 +370,29 @@ namespace Barotrauma.Items.Components
public Item GetFocusTarget()
{
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), "position_out");
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
Item focusTarget = null;
for (int c = 0; c < 2; c++)
{
if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; }
if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected)
//try finding the item to focus on using trigger_out, and if that fails, using position_out
string connectionName = c == 0 ? "trigger_out" : "position_out";
string signal = c == 0 ? "0" : MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture);
if (!item.SendSignal(new Signal(signal, sender: user), connectionName) || focusTarget != null)
{
return item.LastSentSignalRecipients[i].Item;
continue;
}
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
{
if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; }
if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected)
{
focusTarget = item.LastSentSignalRecipients[i].Item;
break;
}
}
}
return null;
return focusTarget;
}
public override bool Pick(Character picker)
@@ -170,7 +170,7 @@ namespace Barotrauma.Items.Components
character.CheckTalents(AbilityEffectType.OnItemDeconstructedByAlly, abilityTargetItem);
}
var itemCreationMultiplier = new AbilityValueItem(amountMultiplier, targetItem.Prefab);
var itemCreationMultiplier = new AbilityItemCreationMultiplier(targetItem.Prefab, amountMultiplier);
user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, itemCreationMultiplier);
amountMultiplier = (int)itemCreationMultiplier.Value;
}
@@ -261,8 +261,8 @@ namespace Barotrauma.Items.Components
if (user != null && !user.Removed)
{
// used to spawn items directly into the deconstructor
var itemContainer = new AbilityItemPrefabItem(item, targetItem.Prefab);
user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemContainer);
var itemDeconstructedInventory = new AbilityItemDeconstructedInventory(targetItem.Prefab, item);
user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemDeconstructedInventory);
}
int amount = (int)amountMultiplier;
@@ -333,7 +333,7 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < outputContainer.Capacity; i++)
{
var containedItem = outputContainer.Inventory.GetItemAt(i);
if (containedItem?.OwnInventory != null && containedItem.OwnInventory.TryPutItem(item, user: null))
if (containedItem?.OwnInventory != null && containedItem.GetComponent<GeneticMaterial>() == null && containedItem.OwnInventory.TryPutItem(item, user: null))
{
return;
}
@@ -454,4 +454,26 @@ namespace Barotrauma.Items.Components
public Character Character { get; set; }
}
class AbilityItemCreationMultiplier : AbilityObject, IAbilityValue, IAbilityItemPrefab
{
public AbilityItemCreationMultiplier(ItemPrefab itemPrefab, float itemAmountMultiplier)
{
ItemPrefab = itemPrefab;
Value = itemAmountMultiplier;
}
public ItemPrefab ItemPrefab { get; set; }
public float Value { get; set; }
}
class AbilityItemDeconstructedInventory : AbilityObject, IAbilityItem, IAbilityItemPrefab
{
public AbilityItemDeconstructedInventory(ItemPrefab itemPrefab, Item item)
{
ItemPrefab = itemPrefab;
Item = item;
}
public ItemPrefab ItemPrefab { get; set; }
public Item Item { get; set; }
}
}
@@ -179,8 +179,6 @@ namespace Barotrauma.Items.Components
if (selectedItem == null) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
RefreshAvailableIngredients();
#if CLIENT
itemList.Enabled = false;
activateButton.Text = TextManager.Get("FabricatorCancel");
@@ -189,7 +187,13 @@ namespace Barotrauma.Items.Components
IsActive = true;
this.user = user;
fabricatedItem = selectedItem;
MoveIngredientsToInputContainer(selectedItem);
RefreshAvailableIngredients();
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
if (!isClient)
{
MoveIngredientsToInputContainer(selectedItem);
}
requiredTime = GetRequiredTime(fabricatedItem, user);
timeUntilReady = requiredTime;
@@ -230,21 +234,19 @@ namespace Barotrauma.Items.Components
}
if (fabricatedItem == null) { return; }
fabricatedItem = null;
#if CLIENT
#if SERVER
if (user != null)
{
GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction);
}
#elif CLIENT
itemList.Enabled = true;
if (activateButton != null)
{
activateButton.Text = TextManager.Get("FabricatorCreate");
}
#endif
#if SERVER
if (user != null)
{
GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction);
}
#endif
fabricatedItem = null;
}
public override void Update(float deltaTime, Camera cam)
@@ -256,15 +258,20 @@ namespace Barotrauma.Items.Components
}
refreshIngredientsTimer -= deltaTime;
if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user))
bool isClient = GameMain.NetworkMember?.IsClient ?? false;
if (!isClient)
{
CancelFabricating();
return;
if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user))
{
CancelFabricating();
return;
}
}
progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime;
if (GameMain.NetworkMember?.IsClient ?? false)
if (isClient)
{
hasPower = State != FabricatorState.Paused;
if (!hasPower)
@@ -365,28 +372,29 @@ namespace Barotrauma.Items.Components
availableItems.Remove(availableItem);
Entity.Spawner.AddToRemoveQueue(availableItem);
inputContainer.Inventory.RemoveItem(availableItem);
break;
}
}
});
int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health);
var fabricationValueItem = new AbilityValueItem(fabricatedItem.Amount, fabricatedItem.TargetItem);
var fabricationitemAmount = new AbilityFabricationItemAmount(fabricatedItem.TargetItem, fabricatedItem.Amount);
int quality = 0;
if (user?.Info != null)
{
foreach (Character character in Character.GetFriendlyCrew(user))
{
character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationValueItem);
character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount);
}
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationValueItem);
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount);
quality = GetFabricatedItemQuality(fabricatedItem, user);
}
var tempUser = user;
for (int i = 0; i < (int)fabricationValueItem.Value; i++)
for (int i = 0; i < (int)fabricationitemAmount.Value; i++)
{
float outCondition = fabricatedItem.OutCondition;
if (i < amountFittingContainer)
@@ -433,7 +441,7 @@ namespace Barotrauma.Items.Components
{
float userSkill = user.GetSkillLevel(skill.Identifier);
float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f);
var addedSkillValue = new AbilityValueString(addedSkill, skill.Identifier);
var addedSkillValue = new AbilityFabricatorSkillGain(skill.Identifier, addedSkill);
user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue);
user.Info.IncreaseSkillLevel(
@@ -542,6 +550,11 @@ namespace Barotrauma.Items.Components
private void RefreshAvailableIngredients()
{
Character user = this.user;
#if CLIENT
user ??= Character.Controlled;
#endif
List<Item> itemList = new List<Item>();
itemList.AddRange(inputContainer.Inventory.AllItems);
foreach (MapEntity linkedTo in item.linkedTo)
@@ -550,6 +563,10 @@ namespace Barotrauma.Items.Components
{
var itemContainer = linkedItem.GetComponent<ItemContainer>();
if (itemContainer == null) { continue; }
if (user != null)
{
if (!itemContainer.HasRequiredItems(user, addMessage: false)) { continue; }
}
var deconstructor = linkedItem.GetComponent<Deconstructor>();
if (deconstructor != null)
@@ -568,17 +585,10 @@ namespace Barotrauma.Items.Components
itemList.AddRange(container.Inventory.AllItems);
}
}
#if CLIENT
if (Character.Controlled?.Inventory != null)
{
itemList.AddRange(Character.Controlled.Inventory.AllItems);
}
#else
if (user?.Inventory != null)
{
itemList.AddRange(user.Inventory.AllItems);
}
#endif
availableIngredients.Clear();
foreach (Item item in itemList)
{
@@ -600,8 +610,6 @@ namespace Barotrauma.Items.Components
//required ingredients that are already present in the input container
List<Item> usedItems = new List<Item>();
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
targetItem.RequiredItems.ForEach(requiredItem => {
for (int i = 0; i < requiredItem.Amount; i++)
{
@@ -630,10 +638,11 @@ namespace Barotrauma.Items.Components
if (!inputContainer.Inventory.CanBePut(availablePrefab))
{
var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it));
unneededItem?.Drop(null, createNetworkEvent: !isClient);
unneededItem?.Drop(null);
}
inputContainer.Inventory.TryPutItem(availablePrefab, user: null, createNetworkEvent: !isClient);
inputContainer.Inventory.TryPutItem(availablePrefab, user: null);
}
break;
}
}
});
@@ -684,5 +693,26 @@ namespace Barotrauma.Items.Components
}
savedFabricatedItem = null;
}
class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier
{
public AbilityFabricatorSkillGain(string skillIdentifier, float skillAmount)
{
SkillIdentifier = skillIdentifier;
Value = skillAmount;
}
public float Value { get; set; }
public string SkillIdentifier { get; set; }
}
class AbilityFabricationItemAmount : AbilityObject, IAbilityValue, IAbilityItemPrefab
{
public AbilityFabricationItemAmount(ItemPrefab itemPrefab, float itemAmount)
{
ItemPrefab = itemPrefab;
Value = itemAmount;
}
public float Value { get; set; }
public ItemPrefab ItemPrefab { get; set; }
}
}
}
@@ -29,6 +29,15 @@ namespace Barotrauma.Items.Components
}
}
public float CurrentBrokenVolume
{
get
{
if (item.ConditionPercentage > 10.0f || !IsActive) { return 0.0f; }
return (1.0f - item.ConditionPercentage / 10.0f) * 100.0f;
}
}
private float pumpSpeedLockTimer, isActiveLockTimer;
[Serialize(0.0f, true, description: "How fast the item is currently pumping water (-100 = full speed out, 100 = full speed in). Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
@@ -72,6 +81,8 @@ namespace Barotrauma.Items.Components
private const float TinkeringSpeedIncrease = 4.0f;
public override bool UpdateWhenInactive => true;
public Pump(Item item, XElement element)
: base(item, element)
{
@@ -82,11 +93,15 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
pumpSpeedLockTimer -= deltaTime;
isActiveLockTimer -= deltaTime;
if (!IsActive) { return; }
currFlow = 0.0f;
if (TargetLevel != null)
{
pumpSpeedLockTimer -= deltaTime;
float hullPercentage = 0.0f;
if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; }
FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f;
@@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components
{
if (lastUser == value) { return; }
lastUser = value;
degreeOfSuccess = lastUser == null ? 0.0f : DegreeOfSuccess(lastUser);
degreeOfSuccess = lastUser == null ? 0.0f : Math.Min(DegreeOfSuccess(lastUser), 1.0f);
LastUserWasPlayer = lastUser.IsPlayer;
}
}
@@ -601,7 +601,7 @@ namespace Barotrauma.Items.Components
if (!shutDown)
{
float degreeOfSuccess = DegreeOfSuccess(character);
float degreeOfSuccess = Math.Min(DegreeOfSuccess(character), 1.0f);
float refuelLimit = 0.3f;
//characters with insufficient skill levels don't refuel the reactor
if (degreeOfSuccess > refuelLimit)
@@ -106,6 +106,13 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize(false, false, description: "Should the sonar view be centered on the transducers or the submarine's center of mass. Only has an effect if UseTransducers is enabled.")]
public bool CenterOnTransducers
{
get;
set;
}
[Editable, Serialize(false, false, description: "Does the sonar have mineral scanning mode. " +
"Only available in-game when the Item has no Steering component.")]
public bool HasMineralScanner { get; set; }
@@ -318,7 +325,7 @@ namespace Barotrauma.Items.Components
Vector2 transducerPosSum = Vector2.Zero;
foreach (ConnectedTransducer transducer in connectedTransducers)
{
if (transducer.Transducer.Item.Submarine != null)
if (transducer.Transducer.Item.Submarine != null && CenterOnTransducers)
{
return transducer.Transducer.Item.Submarine.WorldPosition;
}
@@ -311,7 +311,7 @@ namespace Barotrauma.Items.Components
}
// override autopilot pathing while the AI rams, and go full speed ahead
if (AIRamTimer > 0f)
if (AIRamTimer > 0f && controlledSub != null)
{
AIRamTimer -= deltaTime;
TargetVelocity = GetSteeringVelocity(AITacticalTarget, 0f);
@@ -1,7 +1,6 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
@@ -111,6 +110,17 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(false, true, description: "If true, the recharge speed (and power consumption) of the device goes up exponentially as the recharge rate is increased.")]
public bool ExponentialRechargeSpeed { get; set; }
private float efficiency;
[Editable(minValue: 0.0f, maxValue: 1.0f, decimals: 2), Serialize(0.95f, true, description: "The amount of power you can get out of a item relative to the amount of power that's put into it.")]
public float Efficiency
{
get { return efficiency; }
set { efficiency = MathHelper.Clamp(value, 0.0f, 1.0f); }
}
public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed;
public const float aiRechargeTargetRatio = 0.5f;
@@ -170,7 +180,7 @@ namespace Barotrauma.Items.Components
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
}
if (charge >= capacity)
{
//rechargeVoltage = 0.0f;
@@ -181,13 +191,17 @@ namespace Barotrauma.Items.Components
{
float missingCharge = capacity - charge;
float targetRechargeSpeed = rechargeSpeed;
if (ExponentialRechargeSpeed)
{
targetRechargeSpeed = MathF.Pow(rechargeSpeed / maxRechargeSpeed, 2) * maxRechargeSpeed;
}
if (missingCharge < 1.0f)
{
targetRechargeSpeed *= missingCharge;
}
currPowerConsumption = MathHelper.Lerp(currPowerConsumption, targetRechargeSpeed, 0.05f);
Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f;
}
Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f * efficiency;
}
if (charge <= 0.0f)
{
@@ -10,6 +10,8 @@ namespace Barotrauma.Items.Components
{
public List<Connection> PowerConnections { get; private set; }
private readonly HashSet<Connection> signalConnections = new HashSet<Connection>();
private readonly Dictionary<Connection, bool> connectionDirty = new Dictionary<Connection, bool>();
//a list of connections a given connection is connected to, either directly or via other power transfer components
@@ -121,6 +123,7 @@ namespace Barotrauma.Items.Components
partial void InitProjectSpecific(XElement element);
private static readonly HashSet<PowerTransfer> recipientsToRefresh = new HashSet<PowerTransfer>();
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
@@ -132,7 +135,8 @@ namespace Barotrauma.Items.Components
powerLoad = 0.0f;
currPowerConsumption = 0.0f;
SetAllConnectionsDirty();
foreach (HashSet<Connection> recipientList in connectedRecipients.Values.ToList())
recipientsToRefresh.Clear();
foreach (HashSet<Connection> recipientList in connectedRecipients.Values)
{
foreach (Connection c in recipientList)
{
@@ -140,16 +144,26 @@ namespace Barotrauma.Items.Components
var recipientPowerTransfer = c.Item.GetComponent<PowerTransfer>();
if (recipientPowerTransfer != null)
{
recipientPowerTransfer.SetAllConnectionsDirty();
recipientPowerTransfer.RefreshConnections();
recipientsToRefresh.Add(recipientPowerTransfer);
}
}
}
foreach (PowerTransfer recipientPowerTransfer in recipientsToRefresh)
{
recipientPowerTransfer.SetAllConnectionsDirty();
recipientPowerTransfer.RefreshConnections();
}
RefreshConnections();
isBroken = true;
}
}
private int prevSentPowerValue;
private string powerSignal;
private int prevSentLoadValue;
private string loadSignal;
public override void Update(float deltaTime, Camera cam)
{
RefreshConnections();
@@ -172,6 +186,19 @@ namespace Barotrauma.Items.Components
//if the item can't be fixed, don't allow it to break
if (!item.Repairables.Any() || !CanBeOverloaded) { return; }
if (prevSentPowerValue != (int)-CurrPowerConsumption || powerSignal == null)
{
prevSentPowerValue = (int)Math.Round(-CurrPowerConsumption);
powerSignal = prevSentPowerValue.ToString();
}
if (prevSentLoadValue != (int)powerLoad || loadSignal == null)
{
prevSentLoadValue = (int)Math.Round(powerLoad);
loadSignal = prevSentLoadValue.ToString();
}
item.SendSignal(powerSignal, "power_value_out");
item.SendSignal(loadSignal, "load_value_out");
float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f);
Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage;
if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
@@ -217,6 +244,7 @@ namespace Barotrauma.Items.Components
return picker != null;
}
private static readonly HashSet<Connection> tempConnected = new HashSet<Connection>();
protected void RefreshConnections()
{
var connections = item.Connections;
@@ -229,15 +257,15 @@ namespace Barotrauma.Items.Components
else if (!connectionDirty[c])
{
continue;
}
}
//find all connections that are connected to this one (directly or via another PowerTransfer)
HashSet<Connection> connected = new HashSet<Connection>();
tempConnected.Clear();
if (item.Condition > 0.0f)
{
if (!connectedRecipients.ContainsKey(c))
{
connectedRecipients.Add(c, connected);
connectedRecipients.Add(c, tempConnected);
}
else
{
@@ -249,24 +277,22 @@ namespace Barotrauma.Items.Components
}
}
connected.Add(c);
GetConnected(c, connected);
tempConnected.Add(c);
GetConnected(c, tempConnected);
}
connectedRecipients[c] = connected;
connectedRecipients[c] = tempConnected;
//go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated
//(no need to go through the recursive GetConnected method again)
foreach (Connection recipient in connected)
foreach (Connection recipient in tempConnected)
{
if (recipient == c) { continue; }
var recipientPowerTransfer = recipient.Item.GetComponent<PowerTransfer>();
if (recipientPowerTransfer == null) continue;
if (recipientPowerTransfer == null) { continue; }
if (!connectedRecipients.ContainsKey(recipient))
{
connectedRecipients.Add(recipient, connected);
connectedRecipients.Add(recipient, tempConnected);
}
recipientPowerTransfer.connectedRecipients[recipient] = connected;
recipientPowerTransfer.connectionDirty[recipient] = false;
}
}
@@ -296,7 +322,7 @@ namespace Barotrauma.Items.Components
public void SetAllConnectionsDirty()
{
if (item.Connections == null) return;
if (item.Connections == null) { return; }
foreach (Connection c in item.Connections)
{
connectionDirty[c] = true;
@@ -321,6 +347,14 @@ namespace Barotrauma.Items.Components
return;
}
foreach (Connection c in connections)
{
if (c.Name.Length > 5 && c.Name.Substring(0, 6) == "signal")
{
signalConnections.Add(c);
}
}
if (!(this is RelayComponent))
{
if (PowerConnections.Any(p => !p.IsOutput) && PowerConnections.Any(p => p.IsOutput))
@@ -356,29 +390,30 @@ namespace Barotrauma.Items.Components
{
if (item.Condition <= 0.0f || connection.IsPower) { return; }
if (!connectedRecipients.ContainsKey(connection)) { return; }
if (!signalConnections.Contains(connection)) { return; }
if (connection.Name.Length > 5 && connection.Name.Substring(0, 6) == "signal")
foreach (Connection recipient in connectedRecipients[connection])
{
foreach (Connection recipient in connectedRecipients[connection])
if (recipient.Item == item || recipient.Item == signal.source) { continue; }
signal.source?.LastSentSignalRecipients.Add(recipient);
foreach (ItemComponent ic in recipient.Item.Components)
{
if (recipient.Item == item || recipient.Item == signal.source) { continue; }
signal.source?.LastSentSignalRecipients.Add(recipient);
foreach (ItemComponent ic in recipient.Item.Components)
{
//other junction boxes don't need to receive the signal in the pass-through signal connections
//because we relay it straight to the connected items without going through the whole chain of junction boxes
if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; }
ic.ReceiveSignal(signal, recipient);
}
//other junction boxes don't need to receive the signal in the pass-through signal connections
//because we relay it straight to the connected items without going through the whole chain of junction boxes
if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; }
ic.ReceiveSignal(signal, recipient);
}
if (recipient.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value))
{
foreach (StatusEffect effect in recipient.Effects)
{
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f);
}
}
}
}
}
protected override void RemoveComponentSpecific()
@@ -272,7 +272,7 @@ namespace Barotrauma.Items.Components
powered.voltage = -pt1.CurrPowerConsumption / Math.Max(pt1.PowerLoad, 1.0f);
continue;
}
if (powered.powerConsumption <= 0.0f && !(powered is PowerContainer))
if ((powered.powerConsumption <= 0.0f || (powered.Item.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && repairable.TinkeringPowersDevices)) && !(powered is PowerContainer))
{
powered.voltage = 1.0f;
continue;
@@ -268,7 +268,8 @@ namespace Barotrauma.Items.Components
IgnoredBodies = ignoredBodies;
Vector2 projectilePos = weaponPos;
//make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel
if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null)
if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
customPredicate: (Fixture f) => { return !IgnoredBodies.Contains(f.Body); }) == null)
{
//no obstacles -> we can spawn the projectile at the barrel
projectilePos = spawnPos;
@@ -359,7 +360,7 @@ namespace Barotrauma.Items.Components
item.body.FarseerBody.IsBullet = true;
item.body.CollisionCategories = Physics.CollisionProjectile;
item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking;
IsActive = true;
@@ -16,6 +16,9 @@ namespace Barotrauma.Items.Components
private float deteriorationTimer;
private float deteriorateAlwaysResetTimer;
private int prevSentConditionValue;
private string conditionSignal;
bool wasBroken;
bool wasGoodCondition;
@@ -113,6 +116,9 @@ namespace Barotrauma.Items.Components
public float TinkeringStrength => tinkeringStrength;
private bool tinkeringPowersDevices;
public bool TinkeringPowersDevices => tinkeringPowersDevices;
public bool IsBelowRepairThreshold => item.ConditionPercentage <= RepairThreshold;
public bool IsBelowRepairIconThreshold => item.ConditionPercentage <= RepairThreshold / 2;
@@ -266,6 +272,7 @@ namespace Barotrauma.Items.Components
if (action == FixActions.Tinker)
{
tinkeringStrength = 1f + CurrentFixer.GetStatValue(StatTypes.TinkeringStrength);
tinkeringPowersDevices = CurrentFixer.HasAbilityFlag(AbilityFlags.TinkeringPowersDevices);
if (character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors) && item.GetComponent<Deconstructor>() != null || item.GetComponent<Fabricator>() != null)
{
@@ -346,7 +353,13 @@ namespace Barotrauma.Items.Components
UpdateProjSpecific(deltaTime);
IsTinkering = false;
item.SendSignal($"{(int) item.ConditionPercentage}", "condition_out");
if (prevSentConditionValue != (int)item.ConditionPercentage || conditionSignal == null)
{
prevSentConditionValue = (int)item.ConditionPercentage;
conditionSignal = prevSentConditionValue.ToString();
}
item.SendSignal(conditionSignal, "condition_out");
if (CurrentFixer == null)
{
@@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
protected readonly Character[] signalSender = new Character[2];
[InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.", alwaysUseInstanceValues: true)]
public float TimeFrame
@@ -80,14 +82,14 @@ namespace Barotrauma.Items.Components
bool sendOutput = true;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] > timeFrame) sendOutput = false;
if (timeSinceReceived[i] > timeFrame) { sendOutput = false; }
timeSinceReceived[i] += deltaTime;
}
string signalOut = sendOutput ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut)) return;
if (string.IsNullOrEmpty(signalOut)) { return; }
item.SendSignal(signalOut, "signal_out");
item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out");
}
public override void ReceiveSignal(Signal signal, Connection connection)
@@ -95,12 +97,14 @@ namespace Barotrauma.Items.Components
switch (connection.Name)
{
case "signal_in1":
if (signal.value == "0") return;
if (signal.value == "0") { return; }
timeSinceReceived[0] = 0.0f;
signalSender[0] = signal.sender;
break;
case "signal_in2":
if (signal.value == "0") return;
if (signal.value == "0") { return; }
timeSinceReceived[1] = 0.0f;
signalSender[1] = signal.sender;
break;
case "set_output":
output = signal.value;
@@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components
private HashSet<ItemPrefab> ActivatingItemPrefabs { get; set; } = new HashSet<ItemPrefab>();
private bool AllowUsingButtons => ActivatingItemPrefabs.None() || Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab));
private bool AllowUsingButtons => ActivatingItemPrefabs.None() || (Container != null && Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab)));
public ButtonTerminal(Item item, XElement element) : base(item, element)
{
@@ -101,12 +101,12 @@ namespace Barotrauma.Items.Components
partial void OnItemLoadedProjSpecific();
private bool SendSignal(int signalIndex, bool isServerMessage = false)
private bool SendSignal(int signalIndex, Character sender, bool isServerMessage = false)
{
if (!isServerMessage && !AllowUsingButtons) { return false; }
string signal = Signals[signalIndex];
string connectionName = $"signal_out{signalIndex + 1}";
item.SendSignal(signal, connectionName);
item.SendSignal(new Signal(signal, sender: sender), connectionName);
return true;
}
@@ -17,6 +17,12 @@ namespace Barotrauma.Items.Components
}
}
[Editable, Serialize("", false)]
public string Separator
{
get;
set;
}
public ConcatComponent(Item item, XElement element)
: base(item, element)
@@ -25,7 +31,15 @@ namespace Barotrauma.Items.Components
protected override string Calculate(string signal1, string signal2)
{
string output = signal1 + signal2;
string output;
if (string.IsNullOrEmpty(Separator))
{
output = signal1 + signal2;
}
else
{
output = signal1 + Separator + signal2;
}
return output.Length <= maxOutputLength ? output : output.Substring(0, MaxOutputLength);
}
}
@@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components
get { return wires; }
}
private Item item;
private readonly Item item;
public readonly bool IsOutput;
@@ -142,7 +142,6 @@ namespace Barotrauma.Items.Components
IsPower = Name == "power_in" || Name == "power" || Name == "power_out";
Effects = new List<StatusEffect>();
wireId = new ushort[MaxWires];
@@ -164,6 +163,7 @@ namespace Barotrauma.Items.Components
break;
case "statuseffect":
Effects ??= new List<StatusEffect>();
Effects.Add(StatusEffect.Load(subElement, item.Name + ", connection " + Name));
break;
}
@@ -272,7 +272,7 @@ namespace Barotrauma.Items.Components
ic.ReceiveSignal(signal, connection);
}
if (signal.value != "0")
if (recipient.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value))
{
foreach (StatusEffect effect in recipient.Effects)
{
@@ -24,7 +24,19 @@ namespace Barotrauma.Items.Components
/// </summary>
public bool AlwaysAllowRewiring
{
get { return item.Submarine?.Info.Type == SubmarineType.BeaconStation; }
get
{
if (item.Submarine == null) { return true; }
switch (item.Submarine.Info.Type)
{
case SubmarineType.Wreck:
case SubmarineType.BeaconStation:
case SubmarineType.EnemySubmarine:
case SubmarineType.Ruin:
return true;
}
return false;
}
}
[Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)]
@@ -301,7 +301,6 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
UpdateProjSpecific();
foreach (CustomInterfaceElement ciElement in customInterfaceElementList)
{
if (!ciElement.ContinuousSignal) { continue; }
@@ -318,8 +317,6 @@ namespace Barotrauma.Items.Components
}
}
partial void UpdateProjSpecific();
public override XElement Save(XElement parentElement)
{
labels = customInterfaceElementList.Select(ci => ci.Label).ToArray();
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
@@ -24,7 +25,7 @@ namespace Barotrauma.Items.Components
private int signalQueueSize;
private int delayTicks;
private readonly Queue<DelayedSignal> signalQueue;
private readonly Queue<DelayedSignal> signalQueue = new Queue<DelayedSignal>();
private DelayedSignal prevQueuedSignal;
@@ -39,6 +40,7 @@ namespace Barotrauma.Items.Components
delay = value;
delayTicks = (int)(delay / Timing.Step);
signalQueueSize = Math.Max(delayTicks, 1) * 2;
signalQueue.Clear();
}
}
@@ -59,7 +61,6 @@ namespace Barotrauma.Items.Components
public DelayComponent(Item item, XElement element)
: base (item, element)
{
signalQueue = new Queue<DelayedSignal>();
IsActive = true;
}
@@ -74,7 +75,7 @@ namespace Barotrauma.Items.Components
{
var signalOut = signalQueue.Peek();
signalOut.SendDuration -= 1;
item.SendSignal(new Signal(signalOut.Signal.value, strength: signalOut.Signal.strength), "signal_out");
item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out");
if (signalOut.SendDuration <= 0)
{
signalQueue.Dequeue();
@@ -115,7 +116,7 @@ namespace Barotrauma.Items.Components
signalQueue.Enqueue(prevQueuedSignal);
break;
case "set_delay":
if (float.TryParse(signal.value, out float newDelay))
if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float newDelay))
{
newDelay = MathHelper.Clamp(newDelay, 0, 60);
if (signalQueue.Count > 0 && newDelay != Delay)
@@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components
protected string[] receivedSignal;
private readonly Character[] signalSender = new Character[2];
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
@@ -90,9 +92,8 @@ namespace Barotrauma.Items.Components
if (sendOutput)
{
string signalOut = receivedSignal[0] == receivedSignal[1] ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut)) return;
item.SendSignal(signalOut, "signal_out");
if (string.IsNullOrEmpty(signalOut)) { return; }
item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out");
}
}
@@ -103,10 +104,15 @@ namespace Barotrauma.Items.Components
case "signal_in1":
receivedSignal[0] = signal.value;
timeSinceReceived[0] = 0.0f;
signalSender[0] = signal.sender;
break;
case "signal_in2":
receivedSignal[1] = signal.value;
timeSinceReceived[1] = 0.0f;
signalSender[1] = signal.sender;
break;
case "set_output":
output = signal.value;
break;
}
}
@@ -32,10 +32,22 @@ namespace Barotrauma.Items.Components
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
base.ReceiveSignal(signal, connection);
float.TryParse(receivedSignal[0], NumberStyles.Float, CultureInfo.InvariantCulture, out val1);
float.TryParse(receivedSignal[1], NumberStyles.Float, CultureInfo.InvariantCulture, out val2);
{
//base.ReceiveSignal(signal, connection);
switch (connection.Name)
{
case "signal_in1":
float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out val1);
timeSinceReceived[0] = 0.0f;
break;
case "signal_in2":
float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out val2);
timeSinceReceived[1] = 0.0f;
break;
case "set_output":
output = signal.value;
break;
}
}
}
}
@@ -50,7 +50,7 @@ namespace Barotrauma.Items.Components
set
{
rotation = value;
SetLightSourceTransform();
SetLightSourceTransformProjSpecific();
}
}
@@ -256,7 +256,7 @@ namespace Barotrauma.Items.Components
return;
}
SetLightSourceTransform();
SetLightSourceTransformProjSpecific();
PhysicsBody body = ParentBody ?? item.body;
if (body != null && !body.Enabled)
@@ -338,7 +338,11 @@ namespace Barotrauma.Items.Components
partial void SetLightSourceState(bool enabled, float brightness);
partial void SetLightSourceTransform();
public void SetLightSourceTransform()
{
SetLightSourceTransformProjSpecific();
}
partial void SetLightSourceTransformProjSpecific();
}
}
@@ -74,6 +74,17 @@ namespace Barotrauma.Items.Components
}
}
public Vector2 TransformedDetectOffset
{
get
{
Vector2 transformedDetectOffset = detectOffset;
if (item.FlippedX) { transformedDetectOffset.X = -transformedDetectOffset.X; }
if (item.FlippedY) { transformedDetectOffset.Y = -transformedDetectOffset.Y; }
return transformedDetectOffset;
}
}
[Editable(MinValueFloat = 0.1f, MaxValueFloat = 100.0f, DecimalCount = 2), Serialize(0.1f, true, description: "How often the sensor checks if there's something moving near it. Higher values are better for performance.", alwaysUseInstanceValues: true)]
public float UpdateInterval
{
@@ -184,15 +195,15 @@ namespace Barotrauma.Items.Components
}
}
Vector2 detectPos = item.WorldPosition + detectOffset;
Vector2 detectPos = item.WorldPosition + TransformedDetectOffset;
Rectangle detectRect = new Rectangle((int)(detectPos.X - rangeX), (int)(detectPos.Y - rangeY), (int)(rangeX * 2), (int)(rangeY * 2));
float broadRangeX = Math.Max(rangeX * 2, 500);
float broadRangeY = Math.Max(rangeY * 2, 500);
if (item.CurrentHull == null && item.Submarine != null && Level.Loaded != null &&
if (item.CurrentHull == null && item.Submarine != null &&
(Target == TargetType.Wall || Target == TargetType.Any))
{
if (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity)
if (Level.Loaded != null && (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity))
{
var cells = Level.Loaded.GetCells(item.WorldPosition, 1);
foreach (var cell in cells)
@@ -268,7 +279,7 @@ namespace Barotrauma.Items.Components
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) { continue; }
if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; }
if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect))
{
MotionDetected = true;
@@ -276,23 +287,12 @@ namespace Barotrauma.Items.Components
}
}
}
}
}
}
public override void FlipX(bool relativeToSub)
{
detectOffset.X = -detectOffset.X;
}
public override void FlipY(bool relativeToSub)
{
detectOffset.Y = -detectOffset.Y;
}
public override XElement Save(XElement parentElement)
{
Vector2 prevDetectOffset = detectOffset;
//undo flipping before saving
if (item.FlippedX) { detectOffset.X = -detectOffset.X; }
if (item.FlippedY) { detectOffset.Y = -detectOffset.Y; }
XElement element = base.Save(parentElement);
detectOffset = prevDetectOffset;
return element;
@@ -15,14 +15,14 @@ namespace Barotrauma.Items.Components
bool sendOutput = false;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] <= timeFrame) sendOutput = true;
if (timeSinceReceived[i] <= timeFrame) { sendOutput = true; }
timeSinceReceived[i] += deltaTime;
}
string signalOut = sendOutput ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut)) return;
if (string.IsNullOrEmpty(signalOut)) { return; }
item.SendSignal(signalOut, "signal_out");
item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out");
}
}
}
@@ -4,6 +4,9 @@ namespace Barotrauma.Items.Components
{
class OxygenDetector : ItemComponent
{
private int prevSentOxygenValue;
private string oxygenSignal;
public OxygenDetector(Item item, XElement element)
: base (item, element)
{
@@ -12,9 +15,15 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (item.CurrentHull == null) return;
if (item.CurrentHull == null) { return; }
item.SendSignal(((int)item.CurrentHull.OxygenPercentage).ToString(), "signal_out");
if (prevSentOxygenValue != (int)item.CurrentHull.OxygenPercentage || oxygenSignal == null)
{
prevSentOxygenValue = (int)item.CurrentHull.OxygenPercentage;
oxygenSignal = prevSentOxygenValue.ToString();
}
item.SendSignal(oxygenSignal, "signal_out");
}
}

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