v1.6.17.0 (Unto the Breach update)

This commit is contained in:
Regalis11
2024-10-22 17:29:04 +03:00
parent e74b3cdb17
commit 6e6c17e100
417 changed files with 17166 additions and 5870 deletions
@@ -335,7 +335,7 @@ namespace Barotrauma.Items.Components
item.CreateServerEvent(this);
}
#else
if (GameMain.Client != null && GameMain.Client.MidRoundSyncing &&
if (GameMain.Client != null && GameMain.Client.MidRoundSyncing && Submarine.MainSub != null &&
(item.Submarine == Submarine.MainSub || DockingTarget.item.Submarine == Submarine.MainSub))
{
Screen.Selected.Cam.Position = Submarine.MainSub.WorldPosition;
@@ -1224,11 +1224,10 @@ namespace Barotrauma.Items.Components
#if CLIENT
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
}
if (GameMain.GameSession?.Campaign != null && !CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap))
{
return;
if (GameMain.GameSession?.Campaign != null && !CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap))
{
return;
}
}
#endif
@@ -1254,9 +1253,12 @@ namespace Barotrauma.Items.Components
if (newDockedState != wasDocked)
{
bool tryingToToggleOutpostDocking = docked ?
DockingTarget?.Item?.Submarine?.Info?.IsOutpost ?? false :
FindAdjacentPort()?.Item?.Submarine?.Info?.IsOutpost ?? false;
var targetPort = docked ? DockingTarget : FindAdjacentPort();
bool tryingToToggleOutpostDocking =
item.Submarine is { Info.IsOutpost: false } &&
//check that the "parent submarine of the submarine" is not an outpost (that this isn't a shuttle/elevator that's part of an outpost)
item.Submarine?.Submarine is not { Info.IsOutpost: true } &&
targetPort is { item.Submarine.Info.IsOutpost: true };
//trying to dock/undock from an outpost and the signal was sent by some automated system instead of a character
// -> ask if the player really wants to dock/undock to prevent a softlock if someone's wired the docking port
// in a way that makes always makes it dock/undock immediately at the start of the roun
@@ -59,6 +59,8 @@ namespace Barotrauma.Items.Components
}
}
public bool IgnoreSignals { get; private set; }
//how much "less stuck" partially doors get when opened
const float StuckReductionOnOpen = 30.0f;
@@ -350,6 +352,7 @@ namespace Barotrauma.Items.Components
if (!HasAccess(picker))
{
ToggleState(ActionType.OnPicked, picker);
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
}
return false;
}
@@ -707,6 +710,7 @@ namespace Barotrauma.Items.Components
foreach (Character c in Character.CharacterList)
{
if (!c.Enabled) { continue; }
if (c.SelectedItem?.GetComponent<Controller>() is { } controller && controller.IsAttachedUser(c)) { continue; }
if (!MathUtils.IsValid(c.SimPosition))
{
if (!characterPosErrorShown.Contains(c))
@@ -797,23 +801,20 @@ namespace Barotrauma.Items.Components
}
partial void OnFailedToOpen();
public override bool HasAccess(Character character)
{
if (!item.IsInteractable(character)) { return false; }
if (HasIntegratedButtons)
{
return base.HasAccess(character);
}
else
{
return base.HasAccess(character) && Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character));
}
if (!base.HasAccess(character)) { return false; }
if (HasIntegratedButtons) { return true; }
var buttons = Item.GetConnectedComponents<Controller>(recursive: true);
// If there are no buttons, and we can access the door, treat it accessible. Might be controlled by some mechanism, such as motion sensor.
return buttons.None() || buttons.Any(b => b.HasAccess(character));
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (IsStuck || IsJammed) { return; }
if (IsStuck || IsJammed || IgnoreSignals) { return; }
bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value;
@@ -172,7 +172,7 @@ namespace Barotrauma.Items.Components
if (item.Connections == null)
{
//no connections and can't be wired = must be powered by something like batteries
hasPower = Voltage > MinVoltage;
hasPower = HasPower;
}
else
{
@@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components
}
else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
{
Identifier[] allItems = ItemIdentifier.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray();
Identifier[] allItems = ItemIdentifier.ToIdentifiers().ToArray();
Identifier itemIdentifier = allItems.GetRandomUnsynced();
ItemPrefab? prefab = ItemPrefab.Find(null, itemIdentifier);
if (prefab is null) { return; }
@@ -320,7 +320,7 @@ namespace Barotrauma.Items.Components
{
if (!string.IsNullOrWhiteSpace(SpeciesName))
{
Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray();
Identifier[] allSpecies = SpeciesName.ToIdentifiers().ToArray();
Identifier species = allSpecies.GetRandomUnsynced();
Entity.Spawner?.AddCharacterToSpawnQueue(species, pos, onSpawn);
}
@@ -2,6 +2,7 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -15,47 +16,57 @@ namespace Barotrauma.Items.Components
private AfflictionPrefab selectedEffect, selectedTaintedEffect;
[Serialize("", IsPropertySaveable.Yes)]
public string Effect
{
get;
set;
}
public string Effect { get; set; }
[Serialize("geneticmaterialdebuff", IsPropertySaveable.Yes)]
public Identifier TaintedEffect
{
get;
set;
}
[Serialize("geneticmaterialdebuff", IsPropertySaveable.Yes, description: "Either the identifier or the type for the tainted effect prefab")]
public Identifier TaintedEffect { get; set; }
private bool tainted;
[Serialize(false, IsPropertySaveable.Yes)]
public bool Tainted
{
get { return tainted; }
private set
set
{
if (!value) { return; }
tainted = true;
item.AllowDeconstruct = false;
if (!TaintedEffect.IsEmpty)
tainted = value;
if (tainted)
{
selectedTaintedEffect = AfflictionPrefab.Prefabs.Where(a =>
a.Identifier == TaintedEffect ||
a.AfflictionType == TaintedEffect).GetRandomUnsynced();
if (!TaintedEffect.IsEmpty)
{
selectedTaintedEffect = AfflictionPrefab.Prefabs.Where(a =>
a.Identifier == TaintedEffect ||
a.AfflictionType == TaintedEffect).GetRandomUnsynced();
}
}
else
{
if (targetCharacter != null)
{
var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null)
{
affliction.Strength = 0;
}
}
selectedTaintedEffect = null;
}
}
}
[Serialize(false, IsPropertySaveable.Yes)]
public bool SetTaintedOnDeath { get; private set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool CanBeUntainted { get; private set; }
//only for saving the selected tainted effect
[Serialize("", IsPropertySaveable.Yes)]
public Identifier SelectedTaintedEffect
{
get { return selectedTaintedEffect?.Identifier ?? Identifier.Empty; }
private set
{
selectedTaintedEffect = !value.IsEmpty ? AfflictionPrefab.Prefabs.Find(a => a.Identifier == value) : null;
}
private set { selectedTaintedEffect = !value.IsEmpty ? AfflictionPrefab.Prefabs.Find(a => a.Identifier == value) : null; }
}
public GeneticMaterial(Item item, ContentXElement element)
@@ -66,6 +77,7 @@ namespace Barotrauma.Items.Components
{
materialName = TextManager.Get(nameId);
}
if (!string.IsNullOrEmpty(Effect))
{
selectedEffect = AfflictionPrefab.Prefabs.Where(a =>
@@ -74,20 +86,103 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(3.0f, IsPropertySaveable.No)]
public float ConditionIncreaseOnCombineMin { get; set; }
[Serialize(0.0f, IsPropertySaveable.No)]
public float ConditionIncreaseOnCombineMin { get; set; }
[Serialize(8.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float ConditionIncreaseOnCombineMax { get; set; }
[Serialize(3.0f, IsPropertySaveable.No, description: "When refining, min value for condition increase bonus based on the quality of the worse gene.")]
public float ConditionIncreaseOnLowQualityCombine { get; set; }
[Serialize(25.0f, IsPropertySaveable.No, description: "When refining, max value for condition increase bonus based on the quality of the worse gene.")]
public float ConditionIncreaseOnHighQualityCombine { get; set; }
private bool SharesTypeWith(GeneticMaterial otherGeneticMaterial)
{
return GetSharedTypeOrDefault(otherGeneticMaterial) != null;
}
private ItemPrefab GetSharedTypeOrDefault(GeneticMaterial otherGeneticMaterial)
{
if (otherGeneticMaterial == null) { return default; }
return AllMaterialTypes.FirstOrDefault(materialType => otherGeneticMaterial.AllMaterialTypes.Contains(materialType));
}
private IEnumerable<ItemPrefab> AllMaterialTypes
{
get
{
yield return item.Prefab;
if (IsCombined) { yield return NestedMaterial.item.Prefab; }
}
}
private GeneticMaterial NestedMaterial
{
get
{
if (item == null || item.OwnInventory == null) { return null; }
var nestedItemWithGeneticMaterial = item.OwnInventory.AllItems.FirstOrDefault(it => it.GetComponent<GeneticMaterial>() != null);
if (nestedItemWithGeneticMaterial == null) { return null; }
return nestedItemWithGeneticMaterial.GetComponent<GeneticMaterial>();
}
}
private bool IsCombined
{
get
{
if (NestedMaterial != null) { return true; }
// check if this is the nested material
if (item.ParentInventory != null &&
item.ParentInventory.Owner is Item parentItem &&
parentItem.GetComponent<GeneticMaterial>()?.NestedMaterial == this)
{
return true;
}
return false;
}
}
private CombineResult GetCombineRefineResult(GeneticMaterial otherGeneticMaterial)
{
if (otherGeneticMaterial == null)
{
return CombineResult.None;
}
// both are combined, no more processing is possible
if (IsCombined && otherGeneticMaterial.IsCombined)
{
return CombineResult.None;
}
// neither is combined, can be either refined or combined
if (!IsCombined && !otherGeneticMaterial.IsCombined)
{
return SharesTypeWith(otherGeneticMaterial) ? CombineResult.Refined : CombineResult.Combined;
}
// one is combined, if they share a type, they can be refined
return SharesTypeWith(otherGeneticMaterial) ? CombineResult.Refined : CombineResult.None;
}
public bool CanBeCombinedWith(GeneticMaterial otherGeneticMaterial)
{
return !tainted && otherGeneticMaterial != null && !otherGeneticMaterial.tainted && item.AllowDeconstruct && otherGeneticMaterial.item.AllowDeconstruct;
return GetCombineRefineResult(otherGeneticMaterial) != CombineResult.None;
}
public override void Equip(Character character)
{
if (character == null) { return; }
IsActive = true;
if (targetCharacter != null) { return; }
@@ -99,7 +194,7 @@ namespace Barotrauma.Items.Components
float selectedEffectStrength = GetCombinedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null)
if (affliction != null)
{
affliction.Strength = selectedEffectStrength;
//force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing
@@ -107,24 +202,27 @@ namespace Barotrauma.Items.Components
}
#if SERVER
item.CreateServerEvent(this);
#endif
#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)
{
if (affliction != null)
{
affliction.Strength = selectedTaintedEffectStrength;
//force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing
affliction.SetStrength(selectedTaintedEffectStrength);
}
targetCharacter = character;
#if SERVER
item.CreateServerEvent(this);
#endif
}
foreach (Item containedItem in item.ContainedItems)
{
containedItem.GetComponent<GeneticMaterial>()?.Equip(character);
@@ -136,17 +234,29 @@ namespace Barotrauma.Items.Components
base.Update(deltaTime, cam);
if (targetCharacter != null)
{
if (SetTaintedOnDeath && targetCharacter.IsDead && !tainted)
{
SetTainted(true);
}
var rootContainer = item.RootContainer;
if (!targetCharacter.HasEquippedItem(item) &&
if (!targetCharacter.HasEquippedItem(item) &&
(rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface)))
{
item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter);
//deactivate so the material is no longer updated or considered to be "in effect" in GetCombinedEffectStrength
IsActive = false;
var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null) { affliction.Strength = GetCombinedEffectStrength(); }
if (affliction != null)
{
affliction.Strength = GetCombinedEffectStrength();
}
var taintedAffliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (taintedAffliction != null) { taintedAffliction.Strength = GetCombinedTaintedEffectStrength(); }
if (taintedAffliction != null)
{
taintedAffliction.Strength = GetCombinedTaintedEffectStrength();
}
targetCharacter = null;
}
@@ -160,35 +270,85 @@ namespace Barotrauma.Items.Components
Combined
}
public CombineResult Combine(GeneticMaterial otherGeneticMaterial, Character user)
public CombineResult Combine(GeneticMaterial otherGeneticMaterial, Character user, out Item itemToDestroy)
{
if (!CanBeCombinedWith(otherGeneticMaterial)) { return CombineResult.None; }
var combineRefineResult = GetCombineRefineResult(otherGeneticMaterial);
float conditionIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax);
conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f;
if (item.Prefab == otherGeneticMaterial.item.Prefab)
float randomQualityIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax);
float talentIncrease = user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f;
bool perfectQuality = item.IsFullCondition || otherGeneticMaterial.item.IsFullCondition;
itemToDestroy = otherGeneticMaterial.item;
if (combineRefineResult == CombineResult.Refined)
{
float taintedProbability = GetTaintedProbabilityOnRefine(otherGeneticMaterial, user);
item.Condition = Math.Max(item.Condition, otherGeneticMaterial.item.Condition) + conditionIncrease;
if (taintedProbability >= Rand.Range(0.0f, 1.0f))
float maxQuality = Math.Max(item.Condition, otherGeneticMaterial.item.Condition);
float minQuality = Math.Min(item.Condition, otherGeneticMaterial.item.Condition);
bool oneIsCombined = IsCombined || otherGeneticMaterial.IsCombined;
float minQualityProportionalIncreaseBonus = MathHelper.Lerp(ConditionIncreaseOnLowQualityCombine, ConditionIncreaseOnHighQualityCombine, Math.Clamp(minQuality / 80.0f, 0f, 1f));
float totalQualityIncrease = minQualityProportionalIncreaseBonus + randomQualityIncrease + talentIncrease;
if (oneIsCombined) { totalQualityIncrease /= 2f; }
float newQuality = maxQuality + totalQualityIncrease;
// the deconstructor wants to remove and delete the item for otherGeneticMaterial,
// we want to keep the type that's not being refined here, so we move around the items
if (!IsCombined && otherGeneticMaterial.IsCombined)
{
MakeTainted();
if (item.Prefab == otherGeneticMaterial.item.Prefab)
{
// main items share type, nest the non-shared item
item.OwnInventory?.TryPutItem(otherGeneticMaterial.NestedMaterial.item, user: null);
}
else
{
// the non-shared item is the main item in otherGeneticMaterial,
// we need to nest it...
item.OwnInventory?.TryPutItem(otherGeneticMaterial.item, user: null);
// ...and get rid of the now triple-nested item inside it
var otherNestedItem = otherGeneticMaterial.NestedMaterial.item;
otherGeneticMaterial.item.OwnInventory?.RemoveItem(otherNestedItem);
itemToDestroy = otherNestedItem;
}
}
// note: the condition of combined items represents the quality of both materials,
// and the condition of the nested item should equal that of the housing item
item.Condition = newQuality;
// this can become combined as a result of the item shuffling above
if (IsCombined) { NestedMaterial.item.Condition = newQuality; }
// if one item is 100% condition, remove taint when refining
if (CanBeUntainted && perfectQuality)
{
SetTainted(false, affectsNestedGene: true);
}
else if (GetTaintedProbabilityOnRefine(otherGeneticMaterial, user) >= Rand.Range(0.0f, 1.0f))
{
SetTainted(true);
}
return CombineResult.Refined;
}
else
else if (combineRefineResult == CombineResult.Combined)
{
item.Condition = otherGeneticMaterial.Item.Condition =
(item.Condition + otherGeneticMaterial.Item.Condition) / 2.0f + conditionIncrease;
float averageQuality = (item.Condition + otherGeneticMaterial.Item.Condition) / 2.0f;
item.Condition = otherGeneticMaterial.Item.Condition = averageQuality + randomQualityIncrease + talentIncrease;
item.OwnInventory?.TryPutItem(otherGeneticMaterial.Item, user: null);
item.AllowDeconstruct = false;
otherGeneticMaterial.Item.AllowDeconstruct = false;
if (GetTaintedProbabilityOnCombine(user) >= Rand.Range(0.0f, 1.0f))
// if one item is 100% condition, remove taint when combining
if (CanBeUntainted && perfectQuality)
{
MakeTainted();
SetTainted(false, affectsNestedGene: true);
}
else if (GetTaintedProbabilityOnCombine(user) >= Rand.Range(0.0f, 1.0f))
{
SetTainted(true);
}
return CombineResult.Combined;
}
return combineRefineResult;
}
private float GetCombinedEffectStrength()
@@ -198,11 +358,13 @@ namespace Barotrauma.Items.Components
{
var geneticMaterial = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial == null || !geneticMaterial.IsActive) { continue; }
if (geneticMaterial.selectedEffect == selectedEffect)
{
effectStrength += otherItem.ConditionPercentage / 100.0f * selectedEffect.MaxStrength;
}
}
return effectStrength;
}
@@ -213,17 +375,20 @@ namespace Barotrauma.Items.Components
{
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(GeneticMaterial otherGeneticMaterial, Character user)
{
if (user == null) { return 1.0f; }
float probability = MathHelper.Lerp(0.0f, 0.99f, Math.Max(item.Condition, otherGeneticMaterial.Item.Condition) / 100.0f);
probability *= MathHelper.Lerp(1.0f, 0.25f, DegreeOfSuccess(user));
return MathHelper.Clamp(probability, 0.0f, 1.0f);
@@ -232,17 +397,24 @@ namespace Barotrauma.Items.Components
private static float GetTaintedProbabilityOnCombine(Character user)
{
if (user == null) { return 1.0f; }
float probability = 1.0f - user.GetStatValue(StatTypes.GeneticMaterialTaintedProbabilityReductionOnCombine);
return MathHelper.Clamp(probability, 0.0f, 1.0f);
}
private void MakeTainted()
public void SetTainted(bool newValue, bool affectsNestedGene = false)
{
if (GameMain.NetworkMember?.IsClient ?? false) { return; }
Tainted = true;
Tainted = newValue;
#if SERVER
item.CreateServerEvent(this);
#endif
#endif
if (affectsNestedGene && NestedMaterial != null)
{
NestedMaterial.SetTainted(newValue);
}
}
public static LocalizedString TryCreateName(ItemPrefab prefab, XElement element)
@@ -258,7 +430,8 @@ namespace Barotrauma.Items.Components
}
}
}
return prefab.Name;
}
}
}
}
@@ -556,7 +556,7 @@ namespace Barotrauma.Items.Components
if (spawnProduct || spawnSeed)
{
VineTile vine = Vines.GetRandomUnsynced();
VineTile vine = Vines.GetRandomUnsynced()!;
spawnPos = vine.GetWorldPosition(planter, slot.Offset);
}
else
@@ -1,4 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System.Collections.Immutable;
namespace Barotrauma.Items.Components
@@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components
public string OwnerTags
{
get => string.Join(',', OwnerTagSet);
set => OwnerTagSet = value.Split(',').ToIdentifiers().ToImmutableHashSet();
set => OwnerTagSet = value.ToIdentifiers().ToImmutableHashSet();
}
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
@@ -95,6 +95,10 @@ namespace Barotrauma.Items.Components
{
item.AddTag(s);
}
if (GameMain.GameSession?.GameMode is PvPMode)
{
item.AddTag($"id_{character.TeamID}".ToIdentifier());
}
if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc))
{
item.Description = Description = spawnPoint.IdCardDesc;
@@ -174,9 +174,11 @@ namespace Barotrauma.Items.Components
public override void Drop(Character dropper, bool setTransform = true)
{
//end hit first (which sets the weapon to the "held" state, with disabled physics and no special collision detection)
EndHit();
//ensure the physics body is enabled
item.body.PhysEnabled = true;
base.Drop(dropper, setTransform);
hitting = false;
hitPos = 0.0f;
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -251,10 +253,7 @@ namespace Barotrauma.Items.Components
}
if (hitPos < -MathHelper.Pi)
{
RestoreCollision();
hitting = false;
hitTargets.Clear();
hitPos = 0;
EndHit();
}
}
}
@@ -291,6 +290,14 @@ namespace Barotrauma.Items.Components
User = character;
}
private void EndHit()
{
RestoreCollision();
hitting = false;
hitTargets.Clear();
hitPos = 0;
}
private void RestoreCollision()
{
impactQueue.Clear();
@@ -380,6 +387,7 @@ namespace Barotrauma.Items.Components
}
else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
{
if (holdable.Item.GetRootInventoryOwner() == User) { return false; }
hitTargets.Add(holdable.Item);
}
}
@@ -475,8 +483,8 @@ namespace Barotrauma.Items.Components
}
if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: this, targetCharacter, targetLimb, useTarget: targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: this, targetCharacter, targetLimb, useTarget: targetEntity));
serverLogger ??= new System.Text.StringBuilder();
serverLogger.Clear();
serverLogger.Append($"{picker?.LogName} used {item.Name}");
@@ -243,7 +243,7 @@ namespace Barotrauma.Items.Components
IsActive = true;
float baseReloadTime = reload;
float weaponSkill = character.GetSkillLevel("weapons");
float weaponSkill = character.GetSkillLevel(Tags.WeaponsSkill);
bool applyReloadFailure = ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement;
if (applyReloadFailure)
@@ -291,6 +291,9 @@ namespace Barotrauma.Items.Components
var lastProjectile = LastProjectile;
if (lastProjectile != projectile)
{
//Note that we always snap the rope here, unlike when firing a rope from a turret.
//That's because handheld RangedWeapons have some special logic for handling the rope,
//which doesn't support multiple attached ropes (see Holdable.GetRope and the references to it)
lastProjectile?.Item.GetComponent<Rope>()?.Snap();
}
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier;
@@ -764,7 +764,7 @@ namespace Barotrauma.Items.Components
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController humanAnim &&
Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f)
{
humanAnim.Crouching = true;
humanAnim.Crouch();
}
}
if (!character.IsClimbing)
@@ -661,6 +661,10 @@ namespace Barotrauma.Items.Components
protected virtual void RemoveComponentSpecific()
{
#if CLIENT
HUDOverlay?.Remove();
HUDOverlay = null;
#endif
}
protected string GetTextureDirectory(ContentXElement subElement)
@@ -794,12 +798,25 @@ namespace Barotrauma.Items.Components
/// </summary>
private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard)
{
if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
if (item.Submarine is { IsRespawnShuttle: false })
{
//id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
{
return false;
if (GameMain.GameSession?.GameMode is PvPMode)
{
if (item.Submarine.TeamID != CharacterTeamType.FriendlyNPC && item.Submarine.TeamID != CharacterTeamType.None)
{
// In PvP, always allow access also to FriendlyNPC and None -> restrict access only to the enemy sub.
{
return false;
}
}
}
else
{
return false;
}
}
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
{
@@ -139,13 +139,24 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No)]
private ImmutableHashSet<Identifier> autoInteractWithContainedTags = ImmutableHashSet<Identifier>.Empty;
[Serialize("", IsPropertySaveable.Yes, description: $"Interacting with this container will autointeract with contained items that have one of these tags. Only valid if {nameof(AutoInteractWithContained)} is set to true.")]
public string AutoInteractWithContainedTags
{
get { return autoInteractWithContainedTags.ConvertToString(); }
set
{
autoInteractWithContainedTags = value.ToIdentifiers().ToImmutableHashSet();
}
}
[Serialize(true, IsPropertySaveable.No, description: "Is the container accessible in general.")]
public bool AllowAccess { get; set; }
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description: "Is the container only accessible when it's broken. Doesn't apply to editors.")]
public bool AccessOnlyWhenBroken { get; set; }
[Serialize(true, IsPropertySaveable.No)]
[Serialize(true, IsPropertySaveable.No, description: "Is the container accessible when dropped.")]
public bool AllowAccessWhenDropped { get; set; }
[Serialize(5, IsPropertySaveable.No, description: "How many inventory slots the inventory has per row.")]
@@ -421,12 +432,11 @@ namespace Barotrauma.Items.Components
relatedItem ??= containableItem;
foreach (StatusEffect effect in containableItem.StatusEffects)
{
activeContainedItems.Add(new ActiveContainedItem(
containedItem,
effect,
containableItem.ExcludeBroken,
containableItem.ExcludeFullCondition,
containableItem.BlameEquipperForDeath));
ActiveContainedItem activeContainedItem = new(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition, containableItem.BlameEquipperForDeath);
activeContainedItems.Add(activeContainedItem);
if (!ShouldApplyEffects(activeContainedItem)) { continue; }
activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets);
}
}
}
@@ -492,6 +502,12 @@ namespace Barotrauma.Items.Components
public void OnItemRemoved(Item containedItem)
{
foreach (ActiveContainedItem activeContainedItem in activeContainedItems)
{
if (activeContainedItem.Item != containedItem || !ShouldApplyEffects(activeContainedItem)) { continue; }
activeContainedItem.StatusEffect.Apply(ActionType.OnRemoved, deltaTime: 1, item, targets);
}
activeContainedItems.RemoveAll(i => i.Item == containedItem);
containedItems.RemoveAll(i => i.Item == containedItem);
item.SetContainedItemPositions();
@@ -657,39 +673,47 @@ namespace Barotrauma.Items.Components
return;
}
foreach (var activeContainedItem in activeContainedItems)
foreach (ActiveContainedItem activeContainedItem in activeContainedItems)
{
Item contained = activeContainedItem.Item;
if (!ShouldApplyEffects(activeContainedItem)) continue;
if (activeContainedItem.ExcludeBroken && contained.Condition <= 0.0f) { continue; }
if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; }
StatusEffect effect = activeContainedItem.StatusEffect;
targets.Clear();
bool wearing = item.GetComponent<Wearable>() is Wearable { IsActive: true };
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
targets.AddRange(item.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.AddRange(contained.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
{
targets.Add(character);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
effect.AddNearbyTargets(item.WorldPosition, targets);
}
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
effect.Apply(ActionType.OnContaining, deltaTime, item, targets);
if (wearing) { effect.Apply(ActionType.OnWearing, deltaTime, item, targets); }
if (item.GetComponent<Wearable>() is Wearable { IsActive: true })
{
effect.Apply(ActionType.OnWearing, deltaTime, item, targets);
}
}
}
private bool ShouldApplyEffects(ActiveContainedItem activeContainedItem)
{
Item contained = activeContainedItem.Item;
if (activeContainedItem.ExcludeBroken && contained.Condition <= 0) { return false; }
if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { return false; }
StatusEffect effect = activeContainedItem.StatusEffect;
targets.Clear();
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
targets.AddRange(item.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.AddRange(contained.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
{
targets.Add(character);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
effect.AddNearbyTargets(item.WorldPosition, targets);
}
return true;
}
/// <summary>
/// Set the positions of the contained items if this item has moved/rotated enough
/// </summary>
@@ -715,25 +739,37 @@ namespace Barotrauma.Items.Components
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
return IsAccessible() && base.HasRequiredItems(character, addMessage, msg);
}
/// <summary>
/// Is the container currently accessible. Use this method for checking the accessibility logic, instead of using custom logic on the properties.
/// Use <see cref="HasRequiredItems"/> instead to do a transitive check, where the items of the character are also checked.
/// </summary>
public bool IsAccessible()
{
if (!AllowAccess) { return false; }
if (AccessOnlyWhenBroken)
{
if (Screen.Selected is { IsEditor: true })
{
// AccessOnlyWhenBroken doesn't apply to editors.
return true;
}
return item.Condition <= 0;
}
return true;
}
public override bool Select(Character character)
{
if (!AllowAccess) { return false; }
if (item.Container != null) { return false; }
if (AccessOnlyWhenBroken)
{
if (item.Condition > 0)
{
return false;
}
}
if (!IsAccessible()) { return false; }
if (AutoInteractWithContained && character.SelectedItem == null && Screen.Selected is not { IsEditor: true })
{
foreach (Item contained in Inventory.AllItems)
{
if (contained.TryInteract(character))
if (CanAutoInteractWithContained(contained) && contained.TryInteract(character))
{
character.FocusedItem = contained;
return false;
@@ -756,19 +792,12 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (!AllowAccess) { return false; }
if (AccessOnlyWhenBroken)
{
if (item.Condition > 0)
{
return false;
}
}
if (!IsAccessible()) { return false; }
if (AutoInteractWithContained && Screen.Selected is not { IsEditor: true })
{
foreach (Item contained in Inventory.AllItems)
{
if (contained.TryInteract(picker))
if (CanAutoInteractWithContained(contained) && contained.TryInteract(picker))
{
picker.FocusedItem = contained;
return true;
@@ -819,6 +848,11 @@ namespace Barotrauma.Items.Components
}
}
private bool CanAutoInteractWithContained(Item containedItem)
{
return AutoInteractWithContained && autoInteractWithContainedTags.Any(t => containedItem.HasTag(t));
}
private void SetContainedActive(bool active)
{
if ((ContainableItems == null || !ContainableItems.Any(c => c.SetActive)) &&
@@ -888,58 +922,14 @@ namespace Barotrauma.Items.Components
#warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods.
public void SetContainedItemPositions()
{
Vector2 transformedItemPos = ItemPos * item.Scale;
Vector2 transformedItemInterval = ItemInterval * item.Scale;
Vector2 transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
Vector2 transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
var rootBody = item.RootContainer?.body ?? item.body;
if (ItemPos == Vector2.Zero && ItemInterval == Vector2.Zero)
{
transformedItemPos = item.Position;
}
else
{
if (item.body == null)
{
if (item.FlippedX)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemPos.X += item.Rect.Width;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
if (item.FlippedY)
{
transformedItemPos.Y = -transformedItemPos.Y;
transformedItemPos.Y -= item.Rect.Height;
transformedItemInterval.Y = -transformedItemInterval.Y;
transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
}
transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (Math.Abs(item.Rotation) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(-item.RotationRad);
transformedItemPos = Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position;
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
}
}
else
{
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
if (item.body.Dir == -1.0f)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemPos += item.Position;
}
}
Vector2 transformedItemPos = GetContainedPosition(
drawPosition: false,
out Vector2 transformedItemIntervalHorizontal,
out Vector2 transformedItemIntervalVertical,
out bool flippedX,
out bool flippedY);
int i = 0;
Vector2 currentItemPos = transformedItemPos;
@@ -952,19 +942,19 @@ namespace Barotrauma.Items.Components
if (item.body != null)
{
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
pos.X *= item.body.Dir;
pos.X *= rootBody.Dir;
itemPos = Vector2.Transform(pos, transform) + item.body.Position;
}
else
{
itemPos = pos;
// This code is aped based on above. Not tested.
if (item.FlippedX)
if (flippedX)
{
itemPos.X = -itemPos.X;
itemPos.X += item.Rect.Width;
}
if (item.FlippedY)
if (flippedY)
{
itemPos.Y = -itemPos.Y;
itemPos.Y -= item.Rect.Height;
@@ -990,7 +980,7 @@ namespace Barotrauma.Items.Components
}
if (item.body != null)
{
rotation *= item.body.Dir;
rotation *= rootBody.Dir;
rotation += item.body.Rotation;
}
else
@@ -1034,11 +1024,82 @@ namespace Barotrauma.Items.Components
}
else
{
currentItemPos += transformedItemInterval;
currentItemPos += transformedItemIntervalHorizontal + transformedItemIntervalVertical;
}
}
}
private Vector2 GetContainedPosition(bool drawPosition,
out Vector2 transformedItemIntervalHorizontal, out Vector2 transformedItemIntervalVertical,
out bool flippedX, out bool flippedY)
{
Vector2 transformedItemPos = ItemPos * item.Scale;
Vector2 transformedItemInterval = ItemInterval * item.Scale;
transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
flippedX = item.RootContainer?.FlippedX ?? item.FlippedX;
flippedY = item.RootContainer?.FlippedY ?? item.FlippedY;
var rootBody = item.RootContainer?.body ?? item.body;
bool bodyFlipped = rootBody is { Dir: -1 };
if (ItemPos == Vector2.Zero && ItemInterval == Vector2.Zero && !drawPosition)
{
transformedItemPos = item.Position;
}
else
{
if (item.body == null)
{
if (flippedX)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemPos.X += item.Rect.Width;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
if (flippedY)
{
transformedItemPos.Y = -transformedItemPos.Y;
transformedItemPos.Y -= item.Rect.Height;
transformedItemInterval.Y = -transformedItemInterval.Y;
transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
}
transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (drawPosition)
{
if (item.Submarine != null) { transformedItemPos += item.Submarine.DrawPosition; }
}
if (Math.Abs(item.RotationRad) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(-item.RotationRad);
transformedItemPos =
drawPosition ?
Vector2.Transform(transformedItemPos - item.DrawPosition, transform) + item.DrawPosition :
Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position;
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
}
}
else
{
Matrix transform = Matrix.CreateRotationZ(drawPosition ? item.body.DrawRotation : item.body.Rotation);
if (bodyFlipped)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemPos += drawPosition ? item.body.DrawPosition : item.body.Position;
}
}
return transformedItemPos;
}
public override void OnItemLoaded()
{
Inventory.AllowSwappingContainedItems = AllowSwappingContainedItems;
@@ -1086,6 +1147,8 @@ namespace Barotrauma.Items.Components
{
SpawnAlwaysContainedItems();
}
SetContainedItemPositions();
}
private void SpawnAlwaysContainedItems()
@@ -18,7 +18,8 @@ namespace Barotrauma.Items.Components
public override bool Select(Character character)
{
if (character == null || character.LockHands || character.Removed || !(character.AnimController is HumanoidAnimController)) return false;
if (character == null || character.LockHands || character.Removed ) { return false; }
if (!character.CanClimb) { return false; }
character.AnimController.StartClimbing();
return true;
}
@@ -183,6 +183,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Does the Controller require power to function (= to send signals and move the camera focus to a connected item)?")]
public bool RequirePower
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "If true, other items can be used simultaneously.")]
public bool IsSecondaryItem
{
@@ -190,6 +197,13 @@ namespace Barotrauma.Items.Components
private set;
}
[Serialize(false, IsPropertySaveable.No, description: "If enabled, the user sticks to the position of this item even if the item moves.")]
public bool ForceUserToStayAttached
{
get;
set;
}
public Controller(Item item, ContentXElement element)
: base(item, element)
{
@@ -199,22 +213,35 @@ namespace Barotrauma.Items.Components
IsActive = true;
}
/// <summary>
/// Hack for allowing characters to interact with a loader to get inside a boarding pod.
/// Doing that simply by autointeracting with the contained pod is difficult, because interacting with the loader selects it
/// _after_ the Select method of the pod is called by the autointeract logic, and the character only goes inside the pod if it's the selected item.
/// </summary>
private bool forceSelectNextFrame;
public override void Update(float deltaTime, Camera cam)
{
this.cam = cam;
UserInCorrectPosition = false;
if (!ForceUserToStayAttached) { UserInCorrectPosition = false; }
string signal = IsToggle && State ? output : falseOutput;
if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal))
if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal) && !IsOutOfPower())
{
item.SendSignal(signal, "signal_out");
item.SendSignal(signal, "trigger_out");
}
if (forceSelectNextFrame && user != null)
{
user.SelectedItem = item;
}
forceSelectNextFrame = false;
if (user == null
|| user.Removed
|| !user.IsAnySelectedItem(item)
|| item.ParentInventory != null
|| (item.ParentInventory != null && !IsAttachedUser(user))
|| !user.CanInteractWith(item)
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater))
@@ -228,6 +255,17 @@ namespace Barotrauma.Items.Components
return;
}
if (ForceUserToStayAttached && Vector2.DistanceSquared(item.WorldPosition, user.WorldPosition) > 0.1f)
{
user.TeleportTo(item.WorldPosition);
user.AnimController.Collider.ResetDynamics();
foreach (var limb in user.AnimController.Limbs)
{
if (limb.Removed || limb.IsSevered) { continue; }
limb.body?.ResetDynamics();
}
}
user.AnimController.StartUsingItem();
if (userPos != Vector2.Zero)
@@ -344,6 +382,8 @@ namespace Barotrauma.Items.Components
return false;
}
if (IsOutOfPower()) { return false; }
if (IsToggle && (activator == null || lastUsed < Timing.TotalTime - 0.1))
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
@@ -380,6 +420,8 @@ namespace Barotrauma.Items.Components
return false;
}
if (IsOutOfPower()) { return false; }
focusTarget = GetFocusTarget();
if (focusTarget == null)
@@ -417,11 +459,20 @@ namespace Barotrauma.Items.Components
return true;
}
public bool IsOutOfPower()
{
if (!RequirePower) { return false; }
var powered = item.GetComponent<Powered>();
return powered == null || powered.Voltage < powered.MinVoltage;
}
public Item GetFocusTarget()
{
var positionOut = item.Connections?.Find(c => c.Name == "position_out");
if (positionOut == null) { return null; }
if (IsOutOfPower()) { return null; }
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut);
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
@@ -447,6 +498,7 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (IsOutOfPower()) { return false; }
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { return false; }
#endif
@@ -539,7 +591,16 @@ namespace Barotrauma.Items.Components
{
user = activator;
IsActive = true;
if (ForceUserToStayAttached && item.Container != null)
{
forceSelectNextFrame = true;
return false;
}
}
//allow the selection logic above to run when out of power, but allow sending signals
if (IsOutOfPower()) { return false; }
#if SERVER
item.CreateServerEvent(this);
#endif
@@ -550,6 +611,14 @@ namespace Barotrauma.Items.Components
return true;
}
/// <summary>
/// "Attached user" sticks to this item. Can be used for things such as clown crates and boarding pods.
/// </summary>
public bool IsAttachedUser(Character character)
{
return character != null && character == user && ForceUserToStayAttached;
}
public override void FlipX(bool relativeToSub)
{
if (dir != Direction.None)
@@ -14,8 +14,6 @@ namespace Barotrauma.Items.Components
private float progressTimer;
private float progressState;
private bool hasPower;
private Character user;
private float userDeconstructorSpeedMultiplier = 1.0f;
@@ -88,9 +86,8 @@ namespace Barotrauma.Items.Components
SetActive(false);
return;
}
hasPower = Voltage >= MinVoltage;
if (!hasPower) { return; }
if (!HasPower) { return; }
var repairable = item.GetComponent<Repairable>();
if (repairable != null)
@@ -243,16 +240,16 @@ namespace Barotrauma.Items.Components
if (targetItem == otherItem) { continue; }
if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r == otherItem.Prefab.Identifier))
{
var geneticMaterial1 = targetItem.GetComponent<GeneticMaterial>();
var geneticMaterial2 = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial1 != null && geneticMaterial2 != null)
var targetGeneticMaterial = targetItem.GetComponent<GeneticMaterial>();
var otherGeneticMaterial = otherItem.GetComponent<GeneticMaterial>();
if (targetGeneticMaterial != null && otherGeneticMaterial != null)
{
var result = geneticMaterial1.Combine(geneticMaterial2, user);
var result = targetGeneticMaterial.Combine(otherGeneticMaterial, user, out Item itemToDestroy);
if (result == GeneticMaterial.CombineResult.Refined)
{
inputContainer.Inventory.RemoveItem(otherItem);
OutputContainer.Inventory.RemoveItem(otherItem);
Entity.Spawner.AddItemToRemoveQueue(otherItem);
Entity.Spawner.AddItemToRemoveQueue(itemToDestroy);
}
if (result != GeneticMaterial.CombineResult.None)
{
@@ -425,31 +422,39 @@ namespace Barotrauma.Items.Components
foreach (Item inputItem in items)
{
if (!inputItem.AllowDeconstruct) { continue; }
foreach (var deconstructItem in inputItem.Prefab.DeconstructItems)
{
// check for deconstructor compatibility (for example, 'geneticresearchstation' tag)
if (deconstructItem.RequiredDeconstructor.Length > 0)
{
if (!deconstructItem.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)) { continue; }
if (deconstructItem.RequiredDeconstructor.None(requiredId => item.HasTag(requiredId) || item.Prefab.Identifier == requiredId)) { continue; }
}
// check for other required items in the same deconstructor (for example, 'geneticmaterial')
if (deconstructItem.RequiredOtherItem.Length > 0 && checkRequiredOtherItems)
{
if (!deconstructItem.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier == r))) { continue; }
// no matching item with the required id, skip
if (deconstructItem.RequiredOtherItem.None(requiredId => items.Any(it => it.HasTag(requiredId) || it.Prefab.Identifier == requiredId))) { continue; }
bool validOtherItemFound = false;
foreach (Item otherInputItem in items)
{
if (otherInputItem == inputItem) { continue; }
if (!deconstructItem.RequiredOtherItem.Any(r => otherInputItem.HasTag(r) || otherInputItem.Prefab.Identifier == r)) { continue; }
if (deconstructItem.RequiredOtherItem.None(requiredId => otherInputItem.HasTag(requiredId) || otherInputItem.Prefab.Identifier == requiredId)) { continue; }
var geneticMaterial1 = inputItem.GetComponent<GeneticMaterial>();
var geneticMaterial2 = otherInputItem.GetComponent<GeneticMaterial>();
if (geneticMaterial1 != null && geneticMaterial2 != null)
// skip if genetic materials cannot be combined (or refined)
var geneticMaterial = inputItem.GetComponent<GeneticMaterial>();
var otherGeneticMaterial = otherInputItem.GetComponent<GeneticMaterial>();
if (geneticMaterial != null && otherGeneticMaterial != null)
{
if (!geneticMaterial1.CanBeCombinedWith(geneticMaterial2)) { continue; }
if (!geneticMaterial.CanBeCombinedWith(otherGeneticMaterial)) { continue; }
}
validOtherItemFound = true;
}
if (!validOtherItemFound) { continue; }
}
yield return (inputItem, deconstructItem);
}
}
@@ -126,7 +126,7 @@ namespace Barotrauma.Items.Components
}
else
{
hasPower = Voltage > MinVoltage;
hasPower = HasPower;
}
if (lastReceivedTargetForce.HasValue)
@@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components
float forceMultiplier = 0.1f;
if (User != null)
{
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel(Tags.HelmSkill) / 100));
}
currForce *= item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
@@ -319,7 +319,7 @@ namespace Barotrauma.Items.Components
}
else
{
hasPower = Voltage >= MinVoltage;
hasPower = HasPower;
if (!hasPower)
{
@@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
hasPower = Voltage > MinVoltage;
hasPower = HasPower;
if (hasPower)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime);
@@ -46,7 +46,7 @@ namespace Barotrauma.Items.Components
if (item.CurrentHull == null) { return; }
if (Voltage < MinVoltage && PowerConsumption > 0)
if (!HasPower && PowerConsumption > 0)
{
return;
}
@@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components
}
}
public bool HasPower => IsActive && Voltage >= MinVoltage;
public override bool HasPower => IsActive && Voltage >= MinVoltage;
public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f;
private const float TinkeringSpeedIncrease = 4.0f;
@@ -140,13 +140,11 @@ namespace Barotrauma.Items.Components
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
currFlow = flowPercentage / 100.0f * MaxFlow * powerFactor;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
{
currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease;
}
currFlow = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpSpeed, currFlow);
//less effective when in a bad condition
@@ -340,7 +340,7 @@ namespace Barotrauma.Items.Components
{
foreach (Item item in containedItems)
{
if (!item.HasTag(Tags.Fuel)) { continue; }
if (!item.HasTag(Tags.ReactorFuel)) { continue; }
if (fissionRate > 0.0f)
{
bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost &&
@@ -707,13 +707,13 @@ namespace Barotrauma.Items.Components
var containObjective = AIContainItems<Reactor>(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true);
containObjective.Completed += ReportFuelRodCount;
containObjective.Abandoned += ReportFuelRodCount;
character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.Fuel, 30.0f);
character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.ReactorFuel, 30.0f);
void ReportFuelRodCount()
{
if (!character.IsOnPlayerTeam) { return; }
if (character.Submarine != Submarine.MainSub) { return; }
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.Fuel) && i.Condition > 1);
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.ReactorFuel) && i.Condition > 1);
if (remainingFuelRods == 0)
{
character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f);
@@ -191,8 +191,7 @@ namespace Barotrauma.Items.Components
if (currentMode == Mode.Active)
{
if ((Voltage >= MinVoltage) &&
(!UseTransducers || connectedTransducers.Count > 0))
if (HasPower && (!UseTransducers || connectedTransducers.Count > 0))
{
if (currentPingIndex != -1)
{
@@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components
{
UpdateOnActiveEffects(deltaTime);
if (Voltage >= MinVoltage)
if (HasPower)
{
sendSignalTimer += deltaTime;
if (sendSignalTimer > SendSignalInterval)
@@ -298,7 +298,7 @@ namespace Barotrauma.Items.Components
controlledSub = sonar.ConnectedTransducers.Any() ? sonar.ConnectedTransducers.First().Item.Submarine : null;
}
if (Voltage < MinVoltage) { return; }
if (!HasPower) { return; }
if (user != null && user.Removed)
{
@@ -311,7 +311,7 @@ namespace Barotrauma.Items.Components
if (user != null && controlledSub != null &&
(user.SelectedItem == item || item.linkedTo.Contains(user.SelectedItem)))
{
userSkill = user.GetSkillLevel("helm") / 100.0f;
userSkill = user.GetSkillLevel(Tags.HelmSkill) / 100.0f;
}
// override autopilot pathing while the AI rams, and go full speed ahead
@@ -87,7 +87,7 @@ namespace Barotrauma.Items.Components
if (Math.Abs(charge - lastSentCharge) / adjustedCapacity > 0.05f)
{
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { item.CreateServerEvent(this); }
if (GameMain.Server != null && item.FullyInitialized) { item.CreateServerEvent(this); }
#endif
lastSentCharge = charge;
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -378,14 +379,30 @@ namespace Barotrauma.Items.Components
foreach (Connection c in item.Connections)
{
connectionDirty[c] = true;
if (c.IsPower)
{
ChangedConnections.Add(c);
if (connectedRecipients.TryGetValue(c, out var recipients))
{
recipients.Where(c => c.IsPower).ForEach(c => ChangedConnections.Add(c));
}
}
}
}
public void SetConnectionDirty(Connection connection)
{
var connections = item.Connections;
if (connections == null || !connections.Contains(connection)) return;
if (connections == null || !connections.Contains(connection)) { return; }
connectionDirty[connection] = true;
if (connection.IsPower)
{
ChangedConnections.Add(connection);
if (connectedRecipients.TryGetValue(connection, out var recipients))
{
recipients.Where(c => c.IsPower).ForEach(c => ChangedConnections.Add(c));
}
}
}
public override void OnItemLoaded()
@@ -170,6 +170,8 @@ namespace Barotrauma.Items.Components
/// Can be used by status effects or sounds to check if the item has enough power to run
/// </summary>
public float RelativeVoltage => minVoltage <= 0.0f ? 1.0f : MathHelper.Clamp(Voltage / minVoltage, 0.0f, 1.0f);
public virtual bool HasPower => Voltage >= MinVoltage;
public bool PoweredByTinkering { get; set; }
@@ -668,6 +670,8 @@ namespace Barotrauma.Items.Components
return
conn1.IsPower && conn2.IsPower &&
conn1.Item.Condition > 0.0f && conn2.Item.Condition > 0.0f &&
conn1.Item.GetComponent<PowerTransfer>() is not { CanTransfer: false } &&
conn2.Item.GetComponent<PowerTransfer>() is not { CanTransfer: false } &&
(conn1.Item.HasTag(Tags.JunctionBox) || conn2.Item.HasTag(Tags.JunctionBox) || conn1.Item.HasTag(Tags.DockingPort) || conn2.Item.HasTag(Tags.DockingPort) || conn1.IsOutput != conn2.IsOutput);
}
@@ -184,6 +184,20 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "")]
public bool GoThroughLightTargets
{
get;
set;
}
[Serialize(-1f, IsPropertySaveable.No, description: $"Minimum mass of targets to stick to when {nameof(StickToLightTargets)} is disabled. Defaults to half of the projectile's mass.")]
public float LightTargetMassThreshold
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+
"It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")]
public bool Hitscan
@@ -261,6 +275,13 @@ namespace Barotrauma.Items.Components
}
private float maxJointTranslationInSimUnits = -1;
[Serialize(1000.0f, IsPropertySaveable.No)]
public float JointBreakPoint
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool Prismatic
{
@@ -310,7 +331,7 @@ namespace Barotrauma.Items.Components
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
Attack = new Attack(subElement, item.Name + ", Projectile", item);
}
if (item.body == null)
{
DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!",
@@ -830,7 +851,7 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (StickTargetRemoved() || stickJoint is PrismaticJoint pJoint && Math.Abs(pJoint.JointTranslation) > maxJointTranslationInSimUnits)
if (StickTargetRemoved() || stickJoint is PrismaticJoint pJoint && Math.Abs(pJoint.JointTranslation) > maxJointTranslationInSimUnits || !stickJoint.Enabled)
{
Unstick();
#if SERVER
@@ -858,6 +879,7 @@ namespace Barotrauma.Items.Components
{
return false;
}
if (GoThroughLightTargets && target.Body.Mass < GetLightTargetMassThreshold()) { return false; }
if (target.IsSensor) { return false; }
if (hits.Contains(target.Body)) { return false; }
if (target.Body.UserData is Submarine)
@@ -872,10 +894,7 @@ namespace Barotrauma.Items.Components
limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition);
return false;
}
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
{
return false;
}
if (ShouldIgnoreCharacterCollision(limb.character)) { return false; }
}
else if (target.Body.UserData is Item item)
{
@@ -918,6 +937,20 @@ namespace Barotrauma.Items.Components
}
}
private bool ShouldIgnoreCharacterCollision(Character character)
{
//don't hit characters "attached" to the projectile (e.g. inside a boarding pod)
if (item.GetComponent<Controller>() is { } controller && controller.User == character && controller.IsAttachedUser(controller.User))
{
return true;
}
if (!FriendlyFire && User != null && character.IsFriendly(User))
{
return true;
}
return false;
}
/// <summary>
/// Should the collision with the target submarine be ignored (e.g. did the projectile collide with the wall behind the turret when being launched)
/// </summary>
@@ -961,7 +994,8 @@ namespace Barotrauma.Items.Components
var wallBody = Submarine.PickBody(
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir,
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir,
collisionCategory: Physics.CollisionWall);
collisionCategory: Physics.CollisionWall,
customPredicate: (Fixture f) => IgnoredBodies == null || !IgnoredBodies.Contains(f.Body));
Vector2 launchPosInCurrentCoordinateSpace = launchPos;
if (item.body.Submarine == null && LaunchSub != null)
@@ -1015,10 +1049,6 @@ namespace Barotrauma.Items.Components
}
else if (target.Body.UserData is Limb limb)
{
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
{
return false;
}
// when hitting limbs with piercing ammo, don't lose as much speed
if (MaxTargetsToHit > 1)
{
@@ -1026,6 +1056,7 @@ namespace Barotrauma.Items.Components
deflectedSpeedMultiplier = 0.8f;
}
if (limb.IsSevered || limb.character == null || limb.character.Removed) { return false; }
if (ShouldIgnoreCharacterCollision(limb.character)) { return false; }
limb.character.LastDamageSource = item;
if (Attack != null) { attackResult = Attack.DoDamageToLimb(User ?? Attacker, limb, item.WorldPosition, 1.0f); }
@@ -1154,7 +1185,7 @@ namespace Barotrauma.Items.Components
else if ( remainingHits <= 0 &&
stickJoint == null && StickTarget == null &&
StickToStructures && target.Body.UserData is Structure ||
((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) &&
((StickToLightTargets || target.Body.Mass >= GetLightTargetMassThreshold()) &&
(DoesStick ||
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
(target.Body.UserData is Item i && (i.GetComponent<Door>() != null ? StickToDoors : StickToItems)))))
@@ -1218,6 +1249,11 @@ namespace Barotrauma.Items.Components
return true;
}
private float GetLightTargetMassThreshold()
{
return LightTargetMassThreshold < 0 ? item.body.Mass * 0.5f : LightTargetMassThreshold;
}
private void EnableProjectileCollisions()
{
if (item.body.CollisionCategories != Category.None)
@@ -1274,7 +1310,7 @@ namespace Barotrauma.Items.Components
MotorEnabled = true,
MaxMotorForce = 30.0f,
LimitEnabled = true,
Breakpoint = 1000.0f
Breakpoint = JointBreakPoint,
};
if (maxJointTranslationInSimUnits == -1)
@@ -752,7 +752,7 @@ namespace Barotrauma.Items.Components
}
else if (ic is Powered powered && powered is not LightComponent)
{
if (powered.Voltage >= powered.MinVoltage) { return true; }
if (powered.HasPower) { return true; }
}
}
@@ -4,6 +4,7 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma.Items.Components
{
@@ -102,6 +103,20 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when the weapon it was fired from is fired again? I.e. can there be multiple ropes coming from the weapon at the same time?")]
public bool SnapWhenWeaponFiredAgain
{
get;
set;
}
[Serialize(0.9f, IsPropertySaveable.No, description: "Multiplier for the length of the barrel when determining where the rope should start from.")]
public float BarrelLengthMultiplier
{
get;
set;
}
[Serialize(30.0f, IsPropertySaveable.No, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")]
public float TargetMinMass
{
@@ -115,7 +130,14 @@ namespace Barotrauma.Items.Components
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the force be dynamically adjusted to make it more difficult for targets to escape the pull?")]
public bool IncreaseForceForEscapingTargets
{
get;
set;
}
private bool isReelingIn;
private bool snapped;
public bool Snapped
@@ -314,7 +336,9 @@ namespace Barotrauma.Items.Components
// Currently can only apply pull forces to the source, when it's a character, not e.g. when the item would be auto-operated by an AI. Might have to change this.
if (user != null)
{
if (!snapped)
if (!snapped &&
//user can only hold on to the rope if it was launched from a holdable item, or by something else than an item (limb?)
(projectile.Launcher == null || projectile.Launcher.GetComponent<Holdable>() != null))
{
user.AnimController.HoldToRope();
if (targetCharacter != null)
@@ -332,61 +356,70 @@ namespace Barotrauma.Items.Components
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
{
isReelingIn = user.InWater && user.IsRagdolled || !user.InWater && targetCharacter is { IsIncapacitated: false };
if (isReelingIn)
PhysicsBody targetBody = GetBodyToPull(target);
if (sourceBody.UserData is Character)
{
float pullForce = SourcePullForce;
if (!user.InWater)
isReelingIn = user.InWater && user.IsRagdolled || !user.InWater && targetCharacter is { IsIncapacitated: false };
if (isReelingIn)
{
// Apply a tiny amount to the character holding the rope, so that the connection "feels" more real.
pullForce *= 0.1f;
}
float lengthFactor = MathUtils.InverseLerp(0, MaxLength / 2, currentRopeLength);
float force = LerpForces ? MathHelper.Lerp(0, pullForce, lengthFactor) : pullForce;
sourceBody.ApplyForce(forceDir * force);
// Take the target velocity into account.
PhysicsBody targetBody = GetBodyToPull(target);
if (targetBody != null)
{
if (targetCharacter != null)
float pullForce = SourcePullForce;
if (!user.InWater)
{
if (targetBody.LinearVelocity != Vector2.Zero && sourceBody.LinearVelocity != Vector2.Zero)
// Apply a tiny amount to the character holding the rope, so that the connection "feels" more real.
pullForce *= 0.1f;
}
float lengthFactor = MathUtils.InverseLerp(0, MaxLength / 2, currentRopeLength);
float force = LerpForces ? MathHelper.Lerp(0, pullForce, lengthFactor) : pullForce;
sourceBody.ApplyForce(forceDir * force);
// Take the target velocity into account.
if (targetBody != null)
{
if (targetCharacter != null)
{
Vector2 targetDir = Vector2.Normalize(targetBody.LinearVelocity);
float movementDot = Vector2.Dot(Vector2.Normalize(sourceBody.LinearVelocity), targetDir);
if (movementDot < 0)
if (targetBody.LinearVelocity != Vector2.Zero && sourceBody.LinearVelocity != Vector2.Zero)
{
// Pushing to a different dir -> add some counter force
const float multiplier = 5;
float inverseLengthFactor = MathHelper.Lerp(1, 0, lengthFactor);
sourceBody.ApplyForce(targetBody.LinearVelocity * Math.Min(targetBody.Mass * multiplier, 250) * sourceBody.Mass * -movementDot * inverseLengthFactor);
}
float forceDot = Vector2.Dot(forceDir, targetDir);
if (forceDot > 0)
{
// Pulling to the same dir -> add extra force
float targetSpeed = targetBody.LinearVelocity.Length();
const float multiplier = 25;
sourceBody.ApplyForce(forceDir * targetSpeed * sourceBody.Mass * multiplier * forceDot * lengthFactor);
}
float colliderMainLimbDistance = Vector2.Distance(sourceBody.SimPosition, user.AnimController.MainLimb.SimPosition);
const float minDist = 1;
const float maxDist = 10;
if (colliderMainLimbDistance > minDist)
{
// Move the ragdoll closer to the collider, if it's too far (the correction force in HumanAnimController is not enough -> the ragdoll would lag behind and get teleported).
float correctionForce = MathHelper.Lerp(10.0f, NetConfig.MaxPhysicsBodyVelocity, MathUtils.InverseLerp(minDist, maxDist, colliderMainLimbDistance));
Vector2 targetPos = sourceBody.SimPosition + new Vector2((float)Math.Sin(-sourceBody.Rotation), (float)Math.Cos(-sourceBody.Rotation)) * 0.4f;
user.AnimController.MainLimb.MoveToPos(targetPos, correctionForce);
Vector2 targetDir = Vector2.Normalize(targetBody.LinearVelocity);
float movementDot = Vector2.Dot(Vector2.Normalize(sourceBody.LinearVelocity), targetDir);
if (movementDot < 0)
{
// Pushing to a different dir -> add some counter force
const float multiplier = 5;
float inverseLengthFactor = MathHelper.Lerp(1, 0, lengthFactor);
sourceBody.ApplyForce(targetBody.LinearVelocity * Math.Min(targetBody.Mass * multiplier, 250) * sourceBody.Mass * -movementDot * inverseLengthFactor);
}
float forceDot = Vector2.Dot(forceDir, targetDir);
if (forceDot > 0)
{
// Pulling to the same dir -> add extra force
float targetSpeed = targetBody.LinearVelocity.Length();
const float multiplier = 25;
sourceBody.ApplyForce(forceDir * targetSpeed * sourceBody.Mass * multiplier * forceDot * lengthFactor);
}
float colliderMainLimbDistance = Vector2.Distance(sourceBody.SimPosition, user.AnimController.MainLimb.SimPosition);
const float minDist = 1;
const float maxDist = 10;
if (colliderMainLimbDistance > minDist && sourceBody.UserData is not Submarine)
{
// Move the ragdoll closer to the collider, if it's too far (the correction force in HumanAnimController is not enough -> the ragdoll would lag behind and get teleported).
float correctionForce = MathHelper.Lerp(10.0f, NetConfig.MaxPhysicsBodyVelocity, MathUtils.InverseLerp(minDist, maxDist, colliderMainLimbDistance));
Vector2 targetPos = sourceBody.SimPosition + new Vector2((float)Math.Sin(-sourceBody.Rotation), (float)Math.Cos(-sourceBody.Rotation)) * 0.4f;
user.AnimController.MainLimb.MoveToPos(targetPos, correctionForce);
}
}
}
}
else
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
else
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
}
}
}
else
{
float distance = Vector2.Distance(source.WorldPosition, target.WorldPosition);
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce;
sourceBody.ApplyForce(forceDir * force);
}
}
}
}
@@ -433,7 +466,7 @@ namespace Barotrauma.Items.Components
if (targetRagdoll.InWater || targetRagdoll.OnGround)
{
float forceMultiplier = 1;
if (!targetCharacter.IsRagdolled && !targetCharacter.IsIncapacitated)
if (!targetCharacter.IsRagdolled && !targetCharacter.IsIncapacitated && IncreaseForceForEscapingTargets)
{
// Pulling the main collider requires higher forces when the target is trying to move away.
Vector2 targetMovement = targetCharacter.AnimController.TargetMovement;
@@ -523,6 +556,7 @@ namespace Barotrauma.Items.Components
};
}
if (targetItem.body != null) { return targetItem.body; }
if (targetItem.StaticFixtures.Any() && targetItem.Submarine != null) { return targetItem.Submarine.PhysicsBody; }
}
else if (target is Limb targetLimb)
{
@@ -1,9 +1,9 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -11,24 +11,32 @@ namespace Barotrauma.Items.Components
{
[Editable, Serialize(new string[0], IsPropertySaveable.Yes, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)]
public string[] Signals { get; set; }
[Editable, Serialize("", IsPropertySaveable.Yes, description: "Identifiers or tags of items that, when contained, allow the terminal buttons to be used. Multiple ones should be separated by commas.", alwaysUseInstanceValues: true)]
public string ActivatingItems { get; set; }
private int RequiredSignalCount { get; set; }
private readonly int requiredSignalCount;
private ItemContainer Container { get; set; }
private HashSet<ItemPrefab> ActivatingItemPrefabs { get; set; } = new HashSet<ItemPrefab>();
private bool AllowUsingButtons => ActivatingItemPrefabs.None() || (Container != null && Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab)));
private bool IsActivated => ActivatingItemPrefabs.None() || (Container != null && Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab)));
private readonly IReadOnlyList<string> buttonSignalDefinitions;
public ButtonTerminal(Item item, ContentXElement element) : base(item, element)
{
RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null);
if (RequiredSignalCount < 1)
var buttons = element.GetChildElements("TerminalButton").Where(c => c.GetAttribute("style") != null);
if (buttons.None())
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!",
contentPackage: element.ContentPackage);
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements with a style defined for the ButtonTerminal component!", contentPackage: element.ContentPackage);
}
requiredSignalCount = buttons.Count();
List<string> buttonSignals = new ();
foreach (ContentXElement button in buttons)
{
buttonSignals.Add(button.GetAttributeString("signal", null));
}
buttonSignalDefinitions = buttonSignals.ToImmutableList();
InitProjSpecific(element);
}
@@ -37,57 +45,10 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
base.OnItemLoaded();
if (Signals == null)
{
Signals = new string[RequiredSignalCount];
for (int i = 0; i < RequiredSignalCount; i++)
{
Signals[i] = string.Empty;
}
}
else if (Signals.Length != RequiredSignalCount)
{
string[] newSignals = new string[RequiredSignalCount];
if (Signals.Length < RequiredSignalCount)
{
Signals.CopyTo(newSignals, 0);
for (int i = Signals.Length; i < RequiredSignalCount; i++)
{
newSignals[i] = string.Empty;
}
}
else
{
for (int i = 0; i < RequiredSignalCount; i++)
{
newSignals[i] = Signals[i];
}
}
Signals = newSignals;
}
ActivatingItemPrefabs.Clear();
if (!string.IsNullOrEmpty(ActivatingItems))
{
foreach (var activatingItem in ActivatingItems.Split(','))
{
if (MapEntityPrefab.Find(null, identifier: activatingItem, showErrorMessages: false) is ItemPrefab prefab)
{
ActivatingItemPrefabs.Add(prefab);
}
else
{
ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t == activatingItem))
.ForEach(p => ActivatingItemPrefabs.Add(p));
}
}
if (ActivatingItemPrefabs.None())
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no activating item prefabs found with identifiers or tags \"{ActivatingItems}\"");
}
}
LoadSignals();
LoadActivatingItems();
var containers = item.GetComponents<ItemContainer>();
if (containers.Count() != 1)
{
@@ -97,16 +58,100 @@ namespace Barotrauma.Items.Components
Container = containers.FirstOrDefault();
OnItemLoadedProjSpecific();
// Set active so that update loop is active and we can send the state_out signal.
IsActive = true;
}
partial void OnItemLoadedProjSpecific();
private bool SendSignal(int signalIndex, Character sender, bool isServerMessage = false)
public override void Update(float deltaTime, Camera cam)
{
if (!isServerMessage && !AllowUsingButtons) { return false; }
string signal = Signals[signalIndex];
base.Update(deltaTime, cam);
item.SendSignal(IsActivated ? "1" : "0", "state_out");
}
private void LoadSignals()
{
if (Signals == null || Signals.None())
{
Signals = new string[requiredSignalCount];
for (int i = 0; i < requiredSignalCount; i++)
{
Signals[i] = string.Empty;
}
// Load signals from the button elements, if defined.
for (int i = 0; i < buttonSignalDefinitions.Count; i++)
{
Debug.Assert(Signals.Length > i);
string overrideDefinition = buttonSignalDefinitions[i];
if (overrideDefinition != null)
{
Signals[i] = overrideDefinition;
}
}
}
else if (Signals.Length != requiredSignalCount)
{
string[] newSignals = new string[requiredSignalCount];
if (Signals.Length < requiredSignalCount)
{
Signals.CopyTo(newSignals, 0);
for (int i = Signals.Length; i < requiredSignalCount; i++)
{
newSignals[i] = string.Empty;
}
}
else
{
for (int i = 0; i < requiredSignalCount; i++)
{
newSignals[i] = Signals[i];
}
}
Signals = newSignals;
}
}
private void LoadActivatingItems()
{
ActivatingItemPrefabs.Clear();
if (!string.IsNullOrEmpty(ActivatingItems))
{
foreach (string activatingItem in ActivatingItems.Split(','))
{
Identifier itemIdentifier = activatingItem.ToIdentifier();
if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is ItemPrefab prefab)
{
ActivatingItemPrefabs.Add(prefab);
}
else
{
ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t == itemIdentifier))
.ForEach(p => ActivatingItemPrefabs.Add(p));
}
}
if (ActivatingItemPrefabs.None())
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no activating item prefabs found with identifiers or tags \"{ActivatingItems}\"");
}
}
}
public override void Reset()
{
base.Reset();
Signals = null;
LoadSignals();
LoadActivatingItems();
}
private bool SendSignal(int signalIndex, Character sender, bool ignoreState = false, string overrideSignal = null)
{
if (!ignoreState && !IsActivated) { return false; }
string signal = overrideSignal ?? Signals[signalIndex];
string connectionName = $"signal_out{signalIndex + 1}";
item.SendSignal(new Signal(signal, sender: sender), connectionName);
AchievementManager.OnButtonTerminalSignal(item, sender);
return true;
}
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
@@ -82,8 +82,13 @@ namespace Barotrauma.Items.Components
public bool IsFull => ComponentContainer?.Inventory is { } inventory && inventory.IsFull(true);
/// <summary>
/// Works the same way as the Locked property, but isn't persistent.
/// </summary>
public bool TemporarilyLocked;
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Locked circuit boxes can only be viewed and not interacted with.")]
public bool Locked { get; set; }
public bool Locked { get; private set; }
public CircuitBox(Item item, ContentXElement element) : base(item, element)
{
@@ -756,6 +761,8 @@ namespace Barotrauma.Items.Components
_ => true
};
public bool IsLocked() => Locked || TemporarilyLocked;
public static Option<Item> GetApplicableResourcePlayerHas(ItemPrefab prefab, Character? character)
{
if (character is null) { return Option.None; }
@@ -155,7 +155,7 @@ namespace Barotrauma.Items.Components
if (DisplayName.IsNullOrEmpty())
{
#if DEBUG
DebugConsole.ThrowError("Missing display name in connection " + item.Name + ": " + Name);
DebugConsole.ThrowError($"Could not find a display name for the connection {Name} in the item {item.Name} (submarine: {item.Submarine?.Info?.Name ?? "none"})");
#endif
DisplayName = Name;
}
@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Items.Components;
/// <summary>
/// Base class for signal components that can select between input/output connections (e.g. multiplexer and demultiplexer components)
/// </summary>
abstract class ConnectionSelectorComponent : ItemComponent
{
protected int selectedConnectionIndex;
protected string selectedConnectionIndexStr;
protected string selectedConnectionName;
private int connectionCount = -1;
[InGameEditable,
Serialize(0, IsPropertySaveable.Yes, description: "The index of the selected connection.", alwaysUseInstanceValues: true)]
public int SelectedConnection
{
get { return selectedConnectionIndex; }
set
{
selectedConnectionIndex = Math.Max(0, value);
//don't clamp until we've determined how many connections the item has
//(can't be done until the connection panel component has been loaded too)
if (connectionCount > -1)
{
selectedConnectionIndex = Math.Min(selectedConnectionIndex, connectionCount - 1);
}
selectedConnectionName = GetConnectionName(selectedConnectionIndex);
selectedConnectionIndexStr = selectedConnectionIndex.ToString();
}
}
[InGameEditable,
Serialize(true, IsPropertySaveable.Yes, description: "Should the selected connection go back to the first one when moving past the last one?", alwaysUseInstanceValues: true)]
public bool WrapAround
{
get;
set;
}
[InGameEditable,
Serialize(true, IsPropertySaveable.Yes, description: "Should empty connections (connections with no wires in them) be skipped over when moving the selection?", alwaysUseInstanceValues: true)]
public bool SkipEmptyConnections
{
get;
set;
}
public ConnectionSelectorComponent(Item item, ContentXElement element)
: base(item, element)
{
}
protected abstract string GetConnectionName(int connectionIndex);
/// <summary>
/// Name of the input connection that sets the selected connection.
/// </summary>
protected abstract string InputNameSetConnection { get; }
/// <summary>
/// Name of the input connection that moves the selected connection.
/// </summary>
protected abstract string InputNameMoveInput { get; }
protected abstract IEnumerable<Connection> GetConnections();
public override void OnItemLoaded()
{
connectionCount = GetConnections().Count();
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (connection.Name == InputNameSetConnection)
{
if (int.TryParse(signal.value, out int newInput))
{
SelectedConnection = newInput;
}
}
else if (connection.Name == InputNameMoveInput)
{
if (int.TryParse(signal.value, out int moveAmount))
{
if (SkipEmptyConnections)
{
for (int i = 0; i < connectionCount; i++)
{
moveInput(moveAmount);
if (item.Connections.Any(c =>
c.Name == selectedConnectionName &&
(c.Wires.Any() || c.CircuitBoxConnections.Any())))
{
break;
}
}
}
else
{
moveInput(moveAmount);
}
}
}
void moveInput(int moveAmount)
{
if (WrapAround)
{
SelectedConnection = MathUtils.PositiveModulo(selectedConnectionIndex + moveAmount, connectionCount);
}
else
{
SelectedConnection += moveAmount;
}
}
}
}
@@ -21,6 +21,14 @@ namespace Barotrauma.Items.Components
class CustomInterfaceElement : ISerializableEntity
{
public enum InputTypeOption
{
Number,
Text,
Button,
TickBox
}
public bool ContinuousSignal;
public bool State;
public string ConnectionName;
@@ -33,6 +41,7 @@ namespace Barotrauma.Items.Components
public string Signal { get; set; }
public Identifier PropertyName { get; }
public Identifier TargetItemComponent { get; }
public bool TargetOnlyParentProperty { get; }
public string NumberInputMin { get; }
@@ -44,11 +53,21 @@ namespace Barotrauma.Items.Components
public const string DefaultNumberInputMin = "0", DefaultNumberInputMax = "99", DefaultNumberInputStep = "1";
public const int DefaultNumberInputDecimalPlaces = 0;
public bool IsNumberInput { get; }
public InputTypeOption InputType { get; }
public NumberType? NumberType { get; }
public bool HasPropertyName { get; }
public bool ShouldSetProperty { get; set; }
/// <summary>
/// By default, the elements in the interface only set values of the item or send signals.
/// This can be used to make them additionally work the other way around, periodically getting the current value of the property from the item and refreshing the UI.
/// </summary>
public float GetValueInterval { get; set; } = -1.0f;
#if CLIENT
public float GetValueTimer;
#endif
public string Name => "CustomInterfaceElement";
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; set; }
@@ -59,24 +78,26 @@ namespace Barotrauma.Items.Components
/// Pass the parent component to the constructor to access the serializable properties
/// for elements which change property values.
/// </summary>
public CustomInterfaceElement(Item item, ContentXElement element, CustomInterface parent)
public CustomInterfaceElement(Item item, ContentXElement element, CustomInterface parent, InputTypeOption inputType)
{
Label = element.GetAttributeString("text", "");
ConnectionName = element.GetAttributeString("connection", "");
PropertyName = element.GetAttributeIdentifier("propertyname", "");
PropertyName = element.GetAttributeIdentifier("propertyname", Identifier.Empty);
TargetItemComponent = element.GetAttributeIdentifier("targetitemcomponent", Identifier.Empty);
TargetOnlyParentProperty = element.GetAttributeBool("targetonlyparentproperty", false);
NumberInputMin = element.GetAttributeString("min", DefaultNumberInputMin);
NumberInputMax = element.GetAttributeString("max", DefaultNumberInputMax);
NumberInputStep = element.GetAttributeString("step", DefaultNumberInputStep);
NumberInputDecimalPlaces = element.GetAttributeInt("decimalplaces", DefaultNumberInputDecimalPlaces);
MaxTextLength = element.GetAttributeInt("maxtextlength", int.MaxValue);
GetValueInterval = element.GetAttributeFloat(nameof(GetValueInterval), -1.0f);
InputType = inputType;
HasPropertyName = !PropertyName.IsEmpty;
if (HasPropertyName)
{
string elementName = element.Name.ToString().ToLowerInvariant();
IsNumberInput = elementName == "numberinput" || elementName == "integerinput"; // backwards compatibility
if (IsNumberInput)
if (inputType == InputTypeOption.Number)
{
string numberType = element.GetAttributeString("numbertype", string.Empty);
switch (numberType)
@@ -101,22 +122,7 @@ namespace Barotrauma.Items.Components
}
else if (HasPropertyName && parent != null)
{
if (TargetOnlyParentProperty)
{
if (parent.SerializableProperties.ContainsKey(PropertyName))
{
Signal = parent.SerializableProperties[PropertyName].GetValue(parent) as string;
}
}
else
{
foreach (ISerializableEntity e in parent.item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(PropertyName)) { continue; }
Signal = e.SerializableProperties[PropertyName].GetValue(e) as string;
break;
}
}
parent.SetSignalToPropertyValue(this);
}
else
{
@@ -125,7 +131,7 @@ namespace Barotrauma.Items.Components
foreach (var subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("statuseffect", System.StringComparison.OrdinalIgnoreCase))
if (subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
{
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName: "custom interface element (label " + Label + ")"));
}
@@ -193,6 +199,13 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(false, IsPropertySaveable.Yes)]
public bool ShowInsufficientPowerWarning
{
get;
set;
}
private readonly List<CustomInterfaceElement> customInterfaceElementList = new List<CustomInterfaceElement>();
public CustomInterface(Item item, ContentXElement element)
@@ -200,36 +213,44 @@ namespace Barotrauma.Items.Components
{
foreach (var subElement in element.Elements())
{
bool continuousSignalByDefault = false;
CustomInterfaceElement.InputTypeOption inputType = CustomInterfaceElement.InputTypeOption.Number;
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "button":
inputType = CustomInterfaceElement.InputTypeOption.Button;
continuousSignalByDefault = false;
break;
case "textbox":
inputType = CustomInterfaceElement.InputTypeOption.Text;
continuousSignalByDefault = false;
break;
case "integerinput": // backwards compatibility
case "numberinput":
var button = new CustomInterfaceElement(item, subElement, this)
{
ContinuousSignal = false
};
if (string.IsNullOrEmpty(button.Label))
{
button.Label = "Signal out " + customInterfaceElementList.Count(e => !e.ContinuousSignal);
}
customInterfaceElementList.Add(button);
inputType = CustomInterfaceElement.InputTypeOption.Number;
continuousSignalByDefault = false;
break;
case "tickbox":
var tickBox = new CustomInterfaceElement(item, subElement, this)
{
ContinuousSignal = true
};
if (string.IsNullOrEmpty(tickBox.Label))
{
tickBox.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal);
}
customInterfaceElementList.Add(tickBox);
inputType = CustomInterfaceElement.InputTypeOption.TickBox;
//the default behavior of tickboxes is different for mainly backwards compatibility reasons
//(e.g. keeps sending a true/false signal depending on the state of the tickbox, while the others send a signal when the value changes)
continuousSignalByDefault = true;
break;
default:
continue;
}
var ciElement = new CustomInterfaceElement(item, subElement, this, inputType)
{
ContinuousSignal = subElement.GetAttributeBool(nameof(CustomInterfaceElement.ContinuousSignal), def: continuousSignalByDefault)
};
if (string.IsNullOrEmpty(ciElement.Label))
{
ciElement.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal == ciElement.ContinuousSignal);
}
customInterfaceElementList.Add(ciElement);
IsActive |= ciElement.ContinuousSignal;
}
IsActive = true;
InitProjSpecific();
//load these here to ensure the UI elements (created in InitProjSpecific) are up-to-date
Labels = element.GetAttributeString("labels", "");
@@ -268,27 +289,55 @@ namespace Barotrauma.Items.Components
if (element.HasPropertyName && element.ShouldSetProperty)
{
if (element.TargetOnlyParentProperty)
{
if (SerializableProperties.ContainsKey(element.PropertyName))
{
SerializableProperties[element.PropertyName].TrySetValue(this, element.Signal);
}
}
else
{
foreach (var po in item.AllPropertyObjects)
{
if (!po.SerializableProperties.ContainsKey(element.PropertyName)) { continue; }
po.SerializableProperties[element.PropertyName].TrySetValue(po, element.Signal);
}
}
SetPropertyValueToSignal(element);
customInterfaceElementList[i].ShouldSetProperty = false;
}
}
UpdateSignalsProjSpecific();
}
private void SetPropertyValueToSignal(CustomInterfaceElement element)
{
if (element.TargetOnlyParentProperty)
{
if (SerializableProperties.ContainsKey(element.PropertyName))
{
SerializableProperties[element.PropertyName].TrySetValue(this, element.Signal);
}
}
else
{
foreach (var po in item.AllPropertyObjects)
{
if (!po.SerializableProperties.ContainsKey(element.PropertyName)) { continue; }
if (!element.TargetItemComponent.IsEmpty && po.Name != element.TargetItemComponent) { continue; }
po.SerializableProperties[element.PropertyName].TrySetValue(po, element.Signal);
}
}
}
private void SetSignalToPropertyValue(CustomInterfaceElement element)
{
if (element.TargetOnlyParentProperty)
{
if (SerializableProperties.ContainsKey(element.PropertyName))
{
element.Signal = SerializableProperties[element.PropertyName].GetValue(this)?.ToString();
}
}
else
{
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(element.PropertyName)) { continue; }
if (!element.TargetItemComponent.IsEmpty && e.Name != element.TargetItemComponent) { continue; }
element.Signal = e.SerializableProperties[element.PropertyName].GetValue(e)?.ToString();
break;
}
}
}
public override void OnItemLoaded()
{
foreach (CustomInterfaceElement ciElement in customInterfaceElementList)
@@ -333,41 +382,28 @@ namespace Barotrauma.Items.Components
{
if (tickBoxElement == null) { return; }
tickBoxElement.State = state;
tickBoxElement.Signal = state.ToString();
if (!tickBoxElement.ContinuousSignal)
{
SetPropertyValueToSignal(tickBoxElement);
}
}
private void TextChanged(CustomInterfaceElement textElement, string text)
{
if (textElement == null) { return; }
textElement.Signal = text;
if (!textElement.TargetOnlyParentProperty)
{
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(textElement.PropertyName)) { continue; }
e.SerializableProperties[textElement.PropertyName].TrySetValue(e, text);
}
}
else if (SerializableProperties.ContainsKey(textElement.PropertyName))
{
SerializableProperties[textElement.PropertyName].TrySetValue(this, text);
}
SetPropertyValueToSignal(textElement);
}
private void ValueChanged(CustomInterfaceElement numberInputElement, int value)
{
if (numberInputElement == null) { return; }
numberInputElement.Signal = value.ToString();
if (!numberInputElement.TargetOnlyParentProperty)
SetPropertyValueToSignal(numberInputElement);
foreach (StatusEffect effect in numberInputElement.StatusEffects)
{
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(numberInputElement.PropertyName)) { continue; }
e.SerializableProperties[numberInputElement.PropertyName].TrySetValue(e, value);
}
}
else if (SerializableProperties.ContainsKey(numberInputElement.PropertyName))
{
SerializableProperties[numberInputElement.PropertyName].TrySetValue(this, value);
item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, character: item.ParentInventory?.Owner as Character);
}
}
@@ -375,25 +411,14 @@ namespace Barotrauma.Items.Components
{
if (numberInputElement == null) { return; }
numberInputElement.Signal = value.ToString();
if (!numberInputElement.TargetOnlyParentProperty)
{
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(numberInputElement.PropertyName)) { continue; }
e.SerializableProperties[numberInputElement.PropertyName].TrySetValue(e, value);
}
}
else if (SerializableProperties.ContainsKey(numberInputElement.PropertyName))
{
SerializableProperties[numberInputElement.PropertyName].TrySetValue(this, value);
}
SetPropertyValueToSignal(numberInputElement);
}
public override void Update(float deltaTime, Camera cam)
{
foreach (CustomInterfaceElement ciElement in customInterfaceElementList)
{
if (!ciElement.ContinuousSignal) { continue; }
if (!ciElement.ContinuousSignal && ciElement.PropertyName != "Voltage") { continue; }
//TODO: allow changing output when a tickbox is not selected
if (!string.IsNullOrEmpty(ciElement.Signal) && ciElement.Connection != null)
{
@@ -407,6 +432,13 @@ namespace Barotrauma.Items.Components
}
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
//CustomInterface works even when broken (it should be possible to tick the checkboxes and change values,
//it's up to the other components to work or not work depending on whether the item is broken)
Update(deltaTime, cam);
}
public override XElement Save(XElement parentElement)
{
labels = customInterfaceElementList.Select(ci => ci.Label).ToArray();
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Items.Components;
/// <summary>
/// A component with one input and multiple outputs. Can be used to choose which output the signal should be passed to.
/// </summary>
sealed class DemultiplexerComponent : ConnectionSelectorComponent
{
public DemultiplexerComponent(Item item, ContentXElement element)
: base(item, element)
{
}
protected override string InputNameSetConnection => "set_output";
protected override string InputNameMoveInput => "move_output";
public override void OnItemLoaded()
{
base.OnItemLoaded();
IsActive = item.Connections != null && item.Connections.Any(c => c.Name == "selected_output_out");
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (connection.Name == "signal_in")
{
item.SendSignal(signal, selectedConnectionName);
}
else
{
base.ReceiveSignal(signal, connection);
}
}
public override void Update(float deltaTime, Camera cam)
{
item.SendSignal(selectedConnectionIndexStr, "selected_output_out");
}
protected override string GetConnectionName(int connectionIndex)
{
return "signal_out" + connectionIndex;
}
protected override IEnumerable<Connection> GetConnections()
{
if (item.GetComponent<ConnectionPanel>() is { } connectionPanel)
{
return connectionPanel.Connections.Where(c => c.IsOutput && c.Name.StartsWith("signal_out"));
}
return Enumerable.Empty<Connection>();
}
}
@@ -94,9 +94,9 @@ namespace Barotrauma.Items.Components
set
{
if (isOn == value && IsActive == value) { return; }
IsActive = isOn = value;
SetLightSourceState(value, value ? lightBrightness : 0.0f);
bool isLightOn = isOn && item.Condition > 0;
SetLightSourceState(isLightOn, isLightOn ? lightBrightness : 0.0f);
OnStateChanged();
}
}
@@ -259,7 +259,6 @@ namespace Barotrauma.Items.Components
#endif
IsActive = IsOn;
item.AddTag("light");
}
public override void OnItemLoaded()
@@ -1,7 +1,10 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
@@ -32,6 +35,14 @@ namespace Barotrauma.Items.Components
get;
set;
}
[Editable, Serialize("", IsPropertySaveable.Yes, description: "Does the sensor react only to certain characters (species names, groups or tags)? Doesn't have an effect, if the Target Type is incorrect.", alwaysUseInstanceValues: true)]
public string TargetCharacters
{
get => targetCharacters.ConvertToString();
set => targetCharacters = value.ToIdentifiers().ToHashSet();
}
private HashSet<Identifier> targetCharacters;
[InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)]
public bool IgnoreDead
@@ -40,7 +51,6 @@ namespace Barotrauma.Items.Components
set;
}
[InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Horizontal detection range.", alwaysUseInstanceValues: true)]
public float RangeX
{
@@ -259,40 +269,22 @@ namespace Barotrauma.Items.Components
bool triggerFromMonsters = Target.HasFlag(TargetType.Monster);
bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
if (!hasTriggers) { return; }
foreach (Character c in Character.CharacterList)
foreach (Character character in Character.CharacterList)
{
if (IgnoreDead && c.IsDead) { continue; }
//ignore characters that have spawned a second or less ago
//makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground
if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; }
if (c.IsHuman)
{
if (!triggerFromHumans) { continue; }
}
else if (c.IsPet)
{
if (!triggerFromPets) { continue; }
}
else
{
// Not a human or a pet -> monster?
if (!triggerFromMonsters) { continue; }
if (CharacterParams.CompareGroup(c.Group, CharacterPrefab.HumanGroup))
{
//characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor)
continue;
}
}
if (character.SpawnTime > Timing.TotalTime - 1.0) { continue; }
if (!TriggersOn(character)) { continue; }
//do a rough check based on the position of the character's collider first
//before the more accurate limb-based check
if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY)
if (Math.Abs(character.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(character.WorldPosition.Y - detectPos.Y) > broadRangeY)
{
continue;
}
foreach (Limb limb in c.AnimController.Limbs)
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; }
@@ -304,7 +296,56 @@ namespace Barotrauma.Items.Components
}
}
}
public bool TriggersOn(Character character)
{
bool triggerFromHumans = Target.HasFlag(TargetType.Human);
bool triggerFromPets = Target.HasFlag(TargetType.Pet);
bool triggerFromMonsters = Target.HasFlag(TargetType.Monster);
bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
if (!hasTriggers) { return false; }
return TriggersOn(character, triggerFromHumans, triggerFromPets, triggerFromMonsters);
}
private bool TriggersOn(Character character, bool triggerFromHumans, bool triggerFromPets, bool triggerFromMonsters)
{
if (IgnoreDead && character.IsDead) { return false; }
if (character.IsHuman)
{
if (!triggerFromHumans) { return false; }
}
else if (character.IsPet)
{
if (!triggerFromPets) { return false; }
}
else
{
// Not a human or a pet -> monster?
if (!triggerFromMonsters) { return false; }
if (CharacterParams.CompareGroup(character.Group, CharacterPrefab.HumanGroup))
{
//characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor)
return false;
}
}
// Check matching character, if defined.
if (targetCharacters.Any())
{
// Performance critical code -> using a foreach loop to avoid having to capture variables in lambdas.
bool matchFound = false;
foreach (Identifier target in targetCharacters)
{
if (character.MatchesSpeciesNameOrGroup(target) || character.Params.HasTag(target))
{
matchFound = true;
break;
}
}
if (!matchFound) { return false; }
}
return true;
}
public override XElement Save(XElement parentElement)
{
Vector2 prevDetectOffset = detectOffset;
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Items.Components;
/// <summary>
/// A component with multiple inputs and one output. Can be used to choose which input the component passes signals to the output from.
/// </summary>
sealed class MultiplexerComponent : ConnectionSelectorComponent
{
public MultiplexerComponent(Item item, ContentXElement element)
: base(item, element)
{
}
protected override string InputNameSetConnection => "set_input";
protected override string InputNameMoveInput => "move_input";
public override void OnItemLoaded()
{
base.OnItemLoaded();
IsActive = item.Connections != null && item.Connections.Any(c => c.Name == "selected_input_out");
}
public override void Update(float deltaTime, Camera cam)
{
item.SendSignal(selectedConnectionIndexStr, "selected_input_out");
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (connection.Name.StartsWith("signal_in"))
{
if (connection.Name == selectedConnectionName)
{
item.SendSignal(signal, "signal_out");
}
}
else
{
base.ReceiveSignal(signal, connection);
}
}
protected override string GetConnectionName(int connectionIndex)
{
return "signal_in" + connectionIndex;
}
protected override IEnumerable<Connection> GetConnections()
{
if (item.GetComponent<ConnectionPanel>() is { } connectionPanel)
{
return connectionPanel.Connections.Where(c => !c.IsOutput && c.Name.StartsWith("signal_in"));
}
return Enumerable.Empty<Connection>();
}
}
@@ -1,13 +1,11 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
namespace Barotrauma.Items.Components
{
class OxygenDetector : ItemComponent
{
public const int LowOxygenPercentage = 35;
private int prevSentOxygenValue;
private string oxygenSignal;
public string OxygenSignal { get; private set; }
public OxygenDetector(Item item, ContentXElement element)
: base (item, element)
@@ -20,13 +18,13 @@ namespace Barotrauma.Items.Components
if (item.CurrentHull == null) { return; }
int currOxygenPercentage = (int)item.CurrentHull.OxygenPercentage;
if (prevSentOxygenValue != currOxygenPercentage || oxygenSignal == null)
if (prevSentOxygenValue != currOxygenPercentage || OxygenSignal == null)
{
prevSentOxygenValue = currOxygenPercentage;
oxygenSignal = prevSentOxygenValue.ToString();
OxygenSignal = prevSentOxygenValue.ToString();
}
item.SendSignal(oxygenSignal, "signal_out");
item.SendSignal(OxygenSignal, "signal_out");
item.SendSignal(currOxygenPercentage <= LowOxygenPercentage ? "1" : "0", "low_oxygen");
}
@@ -1,5 +1,4 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -8,7 +7,7 @@ namespace Barotrauma.Items.Components
const float FireCheckInterval = 1.0f;
private float fireCheckTimer;
private bool fireInRange;
public bool FireInRange { get; private set; }
private int maxOutputLength;
[Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")]
@@ -80,10 +79,10 @@ namespace Barotrauma.Items.Components
fireCheckTimer -= deltaTime;
if (fireCheckTimer <= 0.0f)
{
fireInRange = IsFireInRange();
FireInRange = IsFireInRange();
fireCheckTimer = FireCheckInterval;
}
string signalOut = fireInRange ? Output : FalseOutput;
string signalOut = FireInRange ? Output : FalseOutput;
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); }
}
}
@@ -66,6 +66,7 @@ namespace Barotrauma.Items.Components
private float editNodeDelay;
private bool locked;
public bool Locked
{
get
@@ -1,11 +1,13 @@
using FarseerPhysics;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
@@ -17,6 +19,7 @@ namespace Barotrauma.Items.Components
public bool DistanceBasedForce { get; set; }
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force fluctuates over time or if it stays constant.", alwaysUseInstanceValues: true)]
public bool ForceFluctuation { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the fluctuation affects the force. 1 is the maximum fluctuation, 0 is no fluctuation.", alwaysUseInstanceValues: true)]
private float ForceFluctuationStrength
{
@@ -55,7 +58,65 @@ namespace Barotrauma.Items.Components
}
public PhysicsBody PhysicsBody { get; private set; }
private float Radius { get; set; }
private float radius;
[Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
public float Radius
{
get => radius;
set
{
if (radius == value) { return; }
radius = value;
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
}
}
private float width;
[Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
public float Width
{
get => width;
set
{
if (width == value) { return; }
width = value;
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
}
}
private float height;
[Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
public float Height
{
get => height;
set
{
if (height == value) { return; }
height = value;
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
}
}
private float currentRadius, currentWidth, currentHeight;
private Vector2 bodyOffset;
[Editable, Serialize("0,0", IsPropertySaveable.Yes)]
public Vector2 BodyOffset
{
get => bodyOffset;
set
{
if (bodyOffset == value) { return; }
bodyOffset = value;
if (PhysicsBody != null) { SetPhysicsBodyPosition(); }
}
}
private float RadiusInDisplayUnits { get; set; }
private bool TriggeredOnce { get; set; }
private float CurrentForceFluctuation { get; set; } = 1.0f;
@@ -75,7 +136,24 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool MoveOutsideSub { get; set; }
public override bool IsActive
{
get => base.IsActive;
set
{
base.IsActive = value;
if (!IsActive)
{
TriggerActive = false;
triggerers.Clear();
}
}
}
private readonly LevelTrigger.TriggererType triggeredBy;
private readonly Identifier triggerSpeciesOrGroup;
private readonly PropertyConditional.LogicalComparison conditionals;
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
private readonly bool triggerOnce;
private readonly List<ISerializableEntity> statusEffectTargets = new List<ISerializableEntity>();
@@ -94,11 +172,20 @@ namespace Barotrauma.Items.Components
public TriggerComponent(Item item, ContentXElement element) : base(item, element)
{
string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character");
if (!Enum.TryParse(triggeredByAttribute, out triggeredBy))
string triggeredByString = element.GetAttributeString("triggeredby", "Character");
if (!Enum.TryParse(triggeredByString, out triggeredBy))
{
DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.",
contentPackage: element.ContentPackage);
Identifier speciesOrGroup = triggeredByString.ToIdentifier();
if (CharacterPrefab.Prefabs.Any(p => p.MatchesSpeciesNameOrGroup(speciesOrGroup)))
{
triggerSpeciesOrGroup = speciesOrGroup;
triggeredBy = LevelTrigger.TriggererType.Character;
}
else
{
DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByString}\" is not a valid triggerer type.",
contentPackage: element.ContentPackage);
}
}
triggerOnce = element.GetAttributeBool("triggeronce", false);
string parentDebugName = $"TriggerComponent in {item.Name}";
@@ -115,36 +202,75 @@ namespace Barotrauma.Items.Components
break;
}
}
conditionals = PropertyConditional.LoadConditionals(element);
IsActive = true;
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
float radiusAttribute = originalElement.GetAttributeFloat("radius", 10.0f);
Radius = ConvertUnits.ToSimUnits(radiusAttribute * item.Scale);
PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
RefreshPhysicsBodySize();
}
private void RefreshPhysicsBodySize()
{
PhysicsBody?.Remove();
currentWidth = ConvertUnits.ToSimUnits(Width * item.Scale);
currentHeight = ConvertUnits.ToSimUnits(Height * item.Scale);
if (currentWidth > 0 && currentHeight > 0)
{
UserData = item
};
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
PhysicsBody.FarseerBody.SetIsSensor(true);
PhysicsBody = new PhysicsBody(currentWidth, currentHeight, radius: 0.0f, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
UserData = item
};
}
else
{
currentRadius = Math.Max(ConvertUnits.ToSimUnits(Radius * item.Scale), 0.01f);
PhysicsBody = new PhysicsBody(width: 0.0f, height: 0.0f, radius: currentRadius, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
UserData = item
};
}
SetPhysicsBodyPosition();
PhysicsBody.FarseerBody.SetIsSensor(originalElement.GetAttributeBool("sensor", true));
PhysicsBody.FarseerBody.OnCollision += OnCollision;
PhysicsBody.FarseerBody.OnSeparation += OnSeparation;
RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.Radius);
}
public void SetPhysicsBodyPosition(bool ignoreContacts = true)
{
if (PhysicsBody == null) { return; }
Vector2 offset = ConvertUnits.ToSimUnits(BodyOffset * item.Scale);
if (!MathUtils.NearlyEqual(item.RotationRad, 0))
{
Matrix transform = Matrix.CreateRotationZ(-item.RotationRad);
offset = Vector2.Transform(offset, transform);
}
if (ignoreContacts)
{
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition + offset, -item.RotationRad);
}
else
{
PhysicsBody.SetTransform(item.SimPosition + offset, -item.RotationRad);
}
PhysicsBody.UpdateDrawPosition();
}
public override void OnMapLoaded()
{
base.OnMapLoaded();
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
SetPhysicsBodyPosition(true);
PhysicsBody.Submarine = item.Submarine;
}
private bool OnCollision(Fixture sender, Fixture other, Contact contact)
{
if (LevelTrigger.GetEntity(other) is not Entity entity) { return false; }
if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; }
if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, triggerSpeciesOrGroup, conditionals, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; }
triggerers.Add(entity);
return true;
}
@@ -174,7 +300,12 @@ namespace Barotrauma.Items.Components
item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition), item.Rotation);
item.CurrentHull = null;
item.Submarine = null;
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
SetPhysicsBodyPosition();
PhysicsBody.Submarine = item.Submarine;
}
else if (item.body is { BodyType: BodyType.Dynamic })
{
SetPhysicsBodyPosition();
PhysicsBody.Submarine = item.Submarine;
}
@@ -187,11 +318,34 @@ namespace Barotrauma.Items.Components
{
TriggeredOnce = true;
IsActive = false;
triggerers.Clear();
}
}
TriggerActive = triggerers.Any();
if (TriggerActive && conditionals != null)
{
switch (conditionals.LogicalOperator)
{
case PropertyConditional.LogicalOperatorType.And:
{
if (triggerers.Any(t => !PropertyConditional.CheckConditionals((ISerializableEntity)t, conditionals.Conditionals, conditionals.LogicalOperator)))
{
// Some of the conditionals doesn't match
IsActive = false;
}
break;
}
case PropertyConditional.LogicalOperatorType.Or:
{
if (triggerers.None(t => !PropertyConditional.CheckConditionals((ISerializableEntity)t, conditionals.Conditionals, conditionals.LogicalOperator)))
{
// None of the conditionals match
IsActive = false;
}
break;
}
}
}
if (ForceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
@@ -208,7 +362,7 @@ namespace Barotrauma.Items.Components
foreach (Entity triggerer in triggerers)
{
LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets);
LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets, targetItem: Item);
if (triggerer is IDamageable damageable)
{
@@ -260,12 +414,17 @@ namespace Barotrauma.Items.Components
private void ApplyForce(PhysicsBody body, float multiplier = 1.0f)
{
Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition);
Vector2 diff = ConvertUnits.ToDisplayUnits(item.SimPosition - body.SimPosition);
if (diff.LengthSquared() < 0.0001f) { return; }
float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f;
if (distanceFactor <= 0.0f) { return; }
Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff) * multiplier;
if (force.LengthSquared() < 0.01f) { return; }
if (body.Mass < 1)
{
//restrict the force if the body is very light, otherwise it can end up moving at a speed that breaks physics
force *= body.Mass;
}
body.ApplyForce(force);
}
@@ -273,14 +432,7 @@ namespace Barotrauma.Items.Components
{
if (PhysicsBody != null)
{
if (ignoreContacts)
{
PhysicsBody.SetTransformIgnoreContacts(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
else
{
PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
SetPhysicsBodyPosition(ignoreContacts);
PhysicsBody.Submarine = item.Submarine;
}
}
@@ -14,6 +14,7 @@ namespace Barotrauma.Items.Components
partial class Turret : Powered, IDrawableComponent, IServerSerializable
{
private Sprite barrelSprite, railSprite;
private Sprite barrelSpriteBroken, railSpriteBroken;
private readonly List<(Sprite sprite, Vector2 position)> chargeSprites = new List<(Sprite sprite, Vector2 position)>();
private readonly List<Sprite> spinningBarrelSprites = new List<Sprite>();
@@ -89,6 +90,8 @@ namespace Barotrauma.Items.Components
private List<LightComponent> lightComponents;
private Projectile lastProjectile;
private readonly bool isSlowTurret;
public float Rotation { get; private set; }
@@ -320,9 +323,12 @@ namespace Barotrauma.Items.Components
[Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."),
Editable(TransferToSwappedItem = true)]
public Identifier FriendlyTag { get; private set; }
[Serialize("None", IsPropertySaveable.Yes, description: "[Auto Operate] Team that the turret considers friendly. If set to None, the team the submarine/outpost belongs to is considered the friendly team."),
Editable(TransferToSwappedItem = true)]
public CharacterTeamType FriendlyTeam { get; private set; }
#endregion
private const string SetAutoOperateConnection = "set_auto_operate";
private const string ToggleAutoOperateConnection = "toggle_auto_operate";
@@ -341,6 +347,12 @@ namespace Barotrauma.Items.Components
case "railsprite":
railSprite = new Sprite(subElement);
break;
case "barrelspritebroken":
barrelSpriteBroken = new Sprite(subElement);
break;
case "railspritebroken":
railSpriteBroken = new Sprite(subElement);
break;
case "chargesprite":
chargeSprites.Add((new Sprite(subElement), subElement.GetAttributeVector2("chargetarget", Vector2.Zero)));
break;
@@ -779,6 +791,12 @@ namespace Barotrauma.Items.Components
if (launchedProjectile != null || LaunchWithoutProjectile)
{
if (launchedProjectile?.Item.GetComponent<Rope>() != null &&
lastProjectile?.Item.GetComponent<Rope>() is { SnapWhenWeaponFiredAgain: true } rope)
{
rope.Snap();
}
if (projectiles.Any())
{
foreach (Projectile projectile in projectiles)
@@ -806,6 +824,8 @@ namespace Barotrauma.Items.Components
}
}
lastProjectile = launchedProjectile;
#if SERVER
if (character != null && launchedProjectile != null)
{
@@ -900,6 +920,10 @@ namespace Barotrauma.Items.Components
projectileComponent.Attack.DamageMultiplier = (1f * DamageMultiplier) + (TinkeringDamageIncrease * tinkeringStrength);
}
projectileComponent.Use(null, LaunchImpulse);
if (item.GetComponent<TriggerComponent>() is { } trigger)
{
projectileComponent.IgnoredBodies.Add(trigger.PhysicsBody.FarseerBody);
}
projectile.GetComponent<Rope>()?.Attach(item, projectile);
projectileComponent.User = user;
@@ -1681,14 +1705,18 @@ namespace Barotrauma.Items.Components
{
if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) { return false; }
}
if (FriendlyTeam != CharacterTeamType.None)
{
if (target.TeamID == FriendlyTeam) { return false; }
}
bool isHuman = target.IsHuman || target.Group == CharacterPrefab.HumanSpeciesName;
if (isHuman)
{
if (item.Submarine != null)
{
if (item.Submarine.Info.IsOutpost) { return false; }
// Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP).
return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans;
var turretTeam = FriendlyTeam == CharacterTeamType.None ? item.Submarine.TeamID : FriendlyTeam;
return !target.IsOnFriendlyTeam(turretTeam) && TargetHumans;
}
return TargetHumans;
}
@@ -1792,6 +1820,8 @@ namespace Barotrauma.Items.Components
barrelSprite?.Remove(); barrelSprite = null;
railSprite?.Remove(); railSprite = null;
barrelSpriteBroken?.Remove(); barrelSpriteBroken = null;
railSpriteBroken?.Remove(); railSpriteBroken = null;
#if CLIENT
crosshairSprite?.Remove(); crosshairSprite = null;
@@ -269,6 +269,13 @@ namespace Barotrauma
IsInitialized = true;
}
public void Remove()
{
Sprite?.Remove();
//don't use the Picker setter, because it causes the sprite to be re-initialized for "no character"
_picker = null;
}
}
}
@@ -350,7 +357,7 @@ namespace Barotrauma.Items.Components
damageModifiers = new List<DamageModifier>();
SkillModifiers = new Dictionary<Identifier, float>();
int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite");
int spriteCount = element.Elements().Count(x => x.Name.ToString().ToLowerInvariant() == "sprite");
Variants = element.GetAttributeInt("variants", 0);
variant = Rand.Range(1, Variants + 1, Rand.RandSync.ServerAndClient);
wearableSprites = new WearableSprite[spriteCount];
@@ -569,8 +576,7 @@ namespace Barotrauma.Items.Components
foreach (WearableSprite wearableSprite in wearableSprites)
{
wearableSprite?.Sprite?.Remove();
wearableSprite.Picker = null;
wearableSprite.Remove();
}
}