Unstable 1.8.4.0

This commit is contained in:
Markus Isberg
2025-03-12 12:56:27 +00:00
parent a4c3e868e4
commit a4a3427e4e
627 changed files with 29860 additions and 10018 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
@@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components
private Gap linkedGap;
private bool isOpen;
private float openState;
private float openState, lastOpenState;
private readonly Sprite doorSprite, weldedSprite, brokenSprite;
private readonly bool scaleBrokenSprite, fadeBrokenSprite;
private readonly bool autoOrientGap;
@@ -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;
@@ -216,6 +218,7 @@ namespace Barotrauma.Items.Components
get { return openState; }
set
{
lastOpenState = openState;
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
#if CLIENT
float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
@@ -327,13 +330,24 @@ namespace Barotrauma.Items.Components
private readonly LocalizedString cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar";
if (IsBroken)
{
return false;
}
if (isOpen)
{
Msg = HasAccess(character) ? "ItemMsgClose" : "ItemMsgForceCloseCrowbar";
}
else
{
Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar";
}
ParseMsg();
if (addMessage)
{
msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value;
msg ??= (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value;
}
return isBroken || base.HasRequiredItems(character, addMessage, msg);
return base.HasRequiredItems(character, addMessage, msg);
}
public override bool Pick(Character picker)
@@ -350,6 +364,7 @@ namespace Barotrauma.Items.Components
if (!HasAccess(picker))
{
ToggleState(ActionType.OnPicked, picker);
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
}
return false;
}
@@ -458,12 +473,12 @@ namespace Barotrauma.Items.Components
if (PredictedState == null)
{
OpenState += deltaTime * (isOpen ? OpeningSpeed : -ClosingSpeed);
isClosing = openState > 0.0f && openState < 1.0f && !isOpen;
isClosing = openState is > 0.0f and < 1.0f && !isOpen;
}
else
{
OpenState += deltaTime * ((bool)PredictedState ? OpeningSpeed : -ClosingSpeed);
isClosing = openState > 0.0f && openState < 1.0f && !(bool)PredictedState;
OpenState += deltaTime * (PredictedState.Value ? OpeningSpeed : -ClosingSpeed);
isClosing = openState is > 0.0f and < 1.0f && !PredictedState.Value;
resetPredictionTimer -= deltaTime;
if (resetPredictionTimer <= 0.0f)
@@ -476,7 +491,11 @@ namespace Barotrauma.Items.Components
if (isClosing)
{
if (OpenState < 0.9f) { PushCharactersAway(); }
//server gives the clients more leeway on moving through closing doors
//latency can often otherwise make a client get blocked by a closing door server-side even if it seemed like they made it through client-side
float pushCharactersAwayThreshold = GameMain.NetworkMember is { IsServer: true } ? 0.1f : 0.9f;
if (OpenState < pushCharactersAwayThreshold) { PushCharactersAway(); }
if (CheckSubmarinesInDoorWay())
{
PredictedState = null;
@@ -707,6 +726,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))
@@ -767,11 +787,11 @@ namespace Barotrauma.Items.Components
{
if (IsHorizontal)
{
body.SetTransform(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
body.SetTransformIgnoreContacts(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
}
else
{
body.SetTransform(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
body.SetTransformIgnoreContacts(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
}
}
@@ -797,23 +817,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
{
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Linq;
@@ -70,6 +70,19 @@ namespace Barotrauma.Items.Components
[Editable, Serialize(false, IsPropertySaveable.Yes, "")]
public bool PreloadCharacter { get; set; }
[Editable, Serialize(false, IsPropertySaveable.Yes, "Should the \"spawn monsters\" setting affect this item in the PvP mode?")]
public bool AffectedByPvPSpawnMonstersSetting { get; set; }
/// <summary>
/// Implemented as a property and checked on the fly instead of disabling the component,
/// because the signals sent by the component might be necessary even if it can't spawn anything.
/// </summary>
private bool DisabledByByPvPSpawnMonstersSetting =>
!SpeciesName.IsNullOrEmpty() &&
AffectedByPvPSpawnMonstersSetting &&
GameMain.GameSession?.GameMode is PvPMode &&
GameMain.NetworkMember is { ServerSettings.PvPSpawnMonsters: false };
private float spawnTimer;
private float? spawnTimerGoal;
@@ -114,15 +127,24 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated)
if (DisabledByByPvPSpawnMonstersSetting)
{
SpawnCharacter(Vector2.Zero, onSpawn: (Character c) =>
CanSpawn = false;
//in most cases we could probably just disable the component here and return,
//but the state_out signal might be needed for something even if the spawning is disabled
}
else
{
if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated)
{
preloadedCharacter = c;
c.DisabledByEvent = true;
});
preloadInitiated = true;
return;
SpawnCharacter(Vector2.Zero, onSpawn: (Character c) =>
{
preloadedCharacter = c;
c.DisabledByEvent = true;
});
preloadInitiated = true;
return;
}
}
base.Update(deltaTime, cam);
@@ -182,7 +204,7 @@ namespace Barotrauma.Items.Components
private bool CanSpawnMore()
{
if (!CanSpawn) { return false; }
if (!CanSpawn || DisabledByByPvPSpawnMonstersSetting) { return false; }
if (MaximumAmount > 0 && spawnedAmount >= MaximumAmount) { return false; }
if (OnlySpawnWhenCrewInRange)
@@ -300,7 +322,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 +342,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,23 +234,60 @@ namespace Barotrauma.Items.Components
base.Update(deltaTime, cam);
if (targetCharacter != null)
{
if (SetTaintedOnDeath && !tainted &&
targetCharacter.IsDead && targetCharacter.CauseOfDeath is not { Type: CauseOfDeathType.Disconnected })
{
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);
IsActive = false;
Character prevTargetCharacter = targetCharacter;
var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null) { affliction.Strength = GetCombinedEffectStrength(); }
var taintedAffliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (taintedAffliction != null) { taintedAffliction.Strength = GetCombinedTaintedEffectStrength(); }
//deactivate so the material is no longer updated or considered to be "in effect" in GetCombinedEffectStrength
Deactivate();
if (rootContainer != null)
{
foreach (var otherItem in rootContainer.ContainedItems)
{
if (otherItem != item && otherItem.GetComponent<GeneticMaterial>() is { IsActive: true } otherGeneticMaterial)
{
//we need to deactivate other genetic materials in the container too at this point,
//otherwise their effects might get triggered by the damage done by removing the gene
otherGeneticMaterial.Deactivate();
}
}
}
targetCharacter = null;
//do this after nullifying the effects, otherwise the damage from removing the genes could trigger the gene's own effects
item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, prevTargetCharacter);
}
}
}
private void Deactivate()
{
IsActive = false;
if (targetCharacter != null)
{
var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null)
{
affliction.Strength = GetCombinedEffectStrength();
}
var taintedAffliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (taintedAffliction != null)
{
taintedAffliction.Strength = GetCombinedTaintedEffectStrength();
}
}
NestedMaterial?.Deactivate();
targetCharacter = null;
}
public enum CombineResult
{
None,
@@ -160,35 +295,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 +383,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 +400,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 +422,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 +455,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
@@ -14,13 +14,15 @@ namespace Barotrauma.Items.Components
{
partial class Holdable : Pickable, IServerSerializable, IClientSerializable
{
private readonly struct EventData : IEventData
private readonly struct AttachEventData : IEventData
{
public readonly Vector2 AttachPos;
public EventData(Vector2 attachPos)
public readonly Character Attacher;
public AttachEventData(Vector2 attachPos, Character attacher)
{
AttachPos = attachPos;
Attacher = attacher;
}
}
@@ -225,6 +227,51 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "ItemMsg", description: "A text displayed next to the item when it's been dropped on the floor (not attached to a wall).")]
public string MsgWhenDropped
{
get;
set;
}
/// <summary>
/// For setting the handle positions using status effects
/// </summary>
public Vector2 Handle1
{
get { return ConvertUnits.ToDisplayUnits(handlePos[0]); }
set
{
handlePos[0] = ConvertUnits.ToSimUnits(value);
if (item.FlippedX)
{
handlePos[0].X = -handlePos[0].X;
}
if (!secondHandlePosDefined)
{
Handle2 = value;
}
}
}
/// <summary>
/// For setting the handle positions using status effects
/// </summary>
public Vector2 Handle2
{
get { return ConvertUnits.ToDisplayUnits(handlePos[1]); }
set
{
handlePos[1] = ConvertUnits.ToSimUnits(value);
if (item.FlippedX)
{
handlePos[1].X = -handlePos[1].X;
}
}
}
private bool secondHandlePosDefined;
public Holdable(Item item, ContentXElement element)
: base(item, element)
{
@@ -254,9 +301,14 @@ namespace Barotrauma.Items.Components
{
int index = i - 1;
string attributeName = "handle" + i;
var attribute = element.GetAttribute(attributeName);
// If no value is defind for handle2, use the value of handle1.
var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue;
Vector2 value = previousValue;
var attribute = element.GetAttribute(attributeName);
if (attribute != null)
{
secondHandlePosDefined = i > 1;
value = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value));
}
handlePos[index] = value;
previousValue = value;
}
@@ -688,7 +740,19 @@ namespace Barotrauma.Items.Components
#endif
//make the item pickable with the default pick key and with no specific tools/items when it's deattached
RequiredItems.Clear();
DisplayMsg = "";
if (MsgWhenDropped.IsNullOrEmpty())
{
DisplayMsg = "";
}
else
{
DisplayMsg = TextManager.Get(MsgWhenDropped);
DisplayMsg =
DisplayMsg.Loaded ?
TextManager.ParseInputTypes(DisplayMsg) :
MsgWhenDropped;
}
PickKey = InputType.Select;
#if CLIENT
item.DrawDepthOffset = SpriteDepthWhenDropped - item.SpriteDepth;
@@ -755,21 +819,14 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember != null)
{
if (character != Character.Controlled)
{
return false;
}
else if (GameMain.NetworkMember.IsServer)
{
return false;
}
else
{
#if CLIENT
if (character == Character.Controlled)
{
Vector2 attachPos = ConvertUnits.ToSimUnits(GetAttachPosition(character));
item.CreateClientEvent(this, new EventData(attachPos));
#endif
item.CreateClientEvent(this, new AttachEventData(attachPos, character));
}
#endif
//don't attach at this point in MP: instead rely on the network events created above
return false;
}
else
@@ -824,9 +881,13 @@ namespace Barotrauma.Items.Components
if (user.Submarine != null)
{
//we must add some "padding" to the raycast to ensure it reaches all the way to a wall
//otherwise the cursor might be outside a wall, but the grid cell it's in might be partially inside
Vector2 padding = Submarine.GridSize * new Vector2(Math.Sign(mouseDiff.X), Math.Sign(mouseDiff.Y));
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(user.Position),
ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null)
ConvertUnits.ToSimUnits(user.Position + mouseDiff + padding), collisionCategory: Physics.CollisionWall) != null)
{
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset;
@@ -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;
@@ -52,7 +52,11 @@ namespace Barotrauma.Items.Components
{
if (holdable.Attached)
{
GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + item.Prefab.Identifier);
//we don't need info of every collected resource, we can get a good sample size just by logging a small sample
if (GameAnalyticsManager.ShouldLogRandomSample())
{
GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + item.Prefab.Identifier);
}
holdable.DeattachFromWall();
}
trigger.Enabled = false;
@@ -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();
@@ -327,6 +334,7 @@ namespace Barotrauma.Items.Components
if (targetLimb.character.IgnoreMeleeWeapons) { return false; }
var targetCharacter = targetLimb.character;
if (targetCharacter == picker) { return false; }
if (HitFriendlyTarget(targetCharacter)) { return false; }
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetCharacter)) { return false; }
@@ -341,7 +349,7 @@ namespace Barotrauma.Items.Components
{
if (targetCharacter == picker || targetCharacter == User) { return false; }
if (targetCharacter.IgnoreMeleeWeapons) { return false; }
targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
if (HitFriendlyTarget(targetCharacter)) { return false; }
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetCharacter)) { return false; }
@@ -380,6 +388,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);
}
}
@@ -387,10 +396,22 @@ namespace Barotrauma.Items.Components
{
return false;
}
impactQueue.Enqueue(f2);
return true;
// Prevent bots from hitting friendly targets.
bool HitFriendlyTarget(Character target)
{
if (User.IsPlayer) { return false; }
if (User.AIController is HumanAIController { Enabled: true } humanAI)
{
if (humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat && combat.Enemy != target)
{
if (humanAI.IsFriendly(target, onlySameTeam: true)) { return true; }
}
}
return false;
}
}
private System.Text.StringBuilder serverLogger;
@@ -412,7 +433,7 @@ namespace Barotrauma.Items.Components
Limb targetLimb = target.UserData as Limb;
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
Item targetItem = target.UserData as Item ?? targetFixture.UserData as Item;
Item targetItem = target.UserData is Holdable h ? h.Item : target.UserData as Item ?? targetFixture.UserData as Item;
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
if (Attack != null)
{
@@ -452,10 +473,9 @@ namespace Barotrauma.Items.Components
}
#endif
}
else if (target.UserData is Holdable holdable && holdable.CanPush)
else if (target.UserData is Holdable { CanPush: true } holdable)
{
if (holdable.Item.Removed) { return; }
Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f);
RestoreCollision();
hitting = false;
User = null;
@@ -475,8 +495,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}");
@@ -202,12 +202,26 @@ namespace Barotrauma.Items.Components
#if CLIENT
if (requiredTime < float.MaxValue && picker == Character.Controlled)
{
string text = string.Empty;
if (!string.IsNullOrWhiteSpace(PickingMsg))
{
text = PickingMsg;
}
else if (this is Door door)
{
text = door.IsClosed ? "progressbar.opening" : "progressbar.closing";
}
else
{
text = "progressbar.deattaching";
}
Character.Controlled?.UpdateHUDProgressBar(
this,
item.WorldPosition,
pickTimer / requiredTime,
GUIStyle.Red, GUIStyle.Green,
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
text);
}
#endif
picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
@@ -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,9 +291,14 @@ 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;
float rangedAttackMultiplier = character?.GetStatValue(StatTypes.RangedAttackMultiplier) ?? 0;
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier) + rangedAttackMultiplier) * WeaponDamageModifier;
projectile.Launcher = item;
ignoredBodies.Clear();
@@ -303,6 +308,9 @@ namespace Barotrauma.Items.Components
{
if (l.IsSevered) { continue; }
ignoredBodies.Add(l.body.FarseerBody);
#if SERVER
ignoredBodies.Add(l.LagCompensatedBody.FarseerBody);
#endif
}
foreach (Item heldItem in character.HeldItems)
@@ -314,6 +322,7 @@ namespace Barotrauma.Items.Components
}
}
}
projectile.Item.body.Dir = Item.body.Dir;
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse);
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
if (projectile.Item.body != null)
@@ -248,7 +248,7 @@ namespace Barotrauma.Items.Components
var barrelHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(rayStartWorld), item.CurrentHull, useWorldCoordinates: true);
if (barrelHull != null && barrelHull != item.CurrentHull)
{
if (MathUtils.GetLineRectangleIntersection(ConvertUnits.ToDisplayUnits(sourcePos), ConvertUnits.ToDisplayUnits(rayStart), item.CurrentHull.Rect, out Vector2 hullIntersection))
if (MathUtils.GetLineWorldRectangleIntersection(ConvertUnits.ToDisplayUnits(sourcePos), ConvertUnits.ToDisplayUnits(rayStart), item.CurrentHull.Rect, out Vector2 hullIntersection))
{
if (!item.CurrentHull.ConnectedGaps.Any(g => g.Open > 0.0f && Submarine.RectContains(g.Rect, hullIntersection)))
{
@@ -320,7 +320,7 @@ namespace Barotrauma.Items.Components
private readonly List<FireSource> fireSourcesInRange = new List<FireSource>();
private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies)
{
var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall;
var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall | Physics.CollisionItemBlocking;
if (!IgnoreCharacters)
{
collisionCategories |= Physics.CollisionCharacter;
@@ -654,8 +654,9 @@ namespace Barotrauma.Items.Components
FixCharacterProjSpecific(user, deltaTime, targetLimb.character);
return true;
}
else if (targetBody.UserData is Item targetItem)
else if (targetBody.UserData is Barotrauma.Item or Holdable)
{
Item targetItem = targetBody.UserData is Holdable holdable ? holdable.Item : (Item)targetBody.UserData;
if (!HitItems || !targetItem.IsInteractable(user)) { return false; }
var levelResource = targetItem.GetComponent<LevelResource>();
@@ -764,7 +765,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)
@@ -75,6 +75,9 @@ namespace Barotrauma.Items.Components
public readonly ContentXElement originalElement;
/// <summary>
/// The default delay for delayed client-side corrections (see <see cref="StartDelayedCorrection"/>.
/// </summary>
protected const float CorrectionDelay = 1.0f;
protected CoroutineHandle delayedCorrectionCoroutine;
@@ -661,10 +664,13 @@ namespace Barotrauma.Items.Components
protected virtual void RemoveComponentSpecific()
{
#if CLIENT
HUDOverlay?.Remove();
HUDOverlay = null;
#endif
}
protected string GetTextureDirectory(ContentXElement subElement)
=> subElement.DoesAttributeReferenceFileNameAlone("texture") ? Path.GetDirectoryName(item.Prefab.FilePath) : string.Empty;
protected string GetTextureDirectory(ContentXElement subElement) => item.Prefab.GetTexturePath(subElement, item.Prefab.ParentPrefab);
public bool HasRequiredSkills(Character character)
{
@@ -794,12 +800,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)
{
@@ -108,7 +108,10 @@ namespace Barotrauma.Items.Components
[Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
public int ItemsPerRow { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")]
[Serialize(false, IsPropertySaveable.No, description: "Should items be drawn based on their position within the inventory?")]
public bool ItemsUseInventoryPlacement { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected. Note that this does not prevent dragging and dropping items to the item.")]
public bool DrawInventory
{
get;
@@ -139,13 +142,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 +435,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 +505,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 +676,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 +742,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 +795,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 +851,12 @@ namespace Barotrauma.Items.Components
}
}
private bool CanAutoInteractWithContained(Item containedItem)
{
return AutoInteractWithContained &&
(autoInteractWithContainedTags.None() || autoInteractWithContainedTags.Any(t => containedItem.HasTag(t)));
}
private void SetContainedActive(bool active)
{
if ((ContainableItems == null || !ContainableItems.Any(c => c.SetActive)) &&
@@ -888,58 +926,16 @@ 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);
if (containedItems.Count == 0) { return; }
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;
}
}
var rootBody = item.RootContainer?.body ?? item.body;
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 +948,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 +986,7 @@ namespace Barotrauma.Items.Components
}
if (item.body != null)
{
rotation *= item.body.Dir;
rotation *= rootBody.Dir;
rotation += item.body.Rotation;
}
else
@@ -998,8 +994,7 @@ namespace Barotrauma.Items.Components
rotation += -item.RotationRad;
}
contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation);
contained.Item.body.UpdateDrawPosition();
contained.Item.body.UpdateDrawPosition(interpolate: false);
}
catch (Exception e)
{
@@ -1021,6 +1016,11 @@ namespace Barotrauma.Items.Components
contained.Item.CurrentHull = item.CurrentHull;
contained.Item.SetContainedItemPositions();
foreach (var lightComponent in contained.Item.GetComponents<LightComponent>())
{
lightComponent.SetLightSourceTransform();
}
i++;
if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f)
{
@@ -1034,11 +1034,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 && item.Prefab.CanSpriteFlipX);
flippedY = item.RootContainer?.FlippedY ?? (item.FlippedY && item.Prefab.CanSpriteFlipY);
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 +1157,8 @@ namespace Barotrauma.Items.Components
{
SpawnAlwaysContainedItems();
}
SetContainedItemPositions();
}
private void SpawnAlwaysContainedItems()
@@ -1096,7 +1169,7 @@ namespace Barotrauma.Items.Components
foreach (string id in splitIds)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == id);
if (prefab != null && Inventory != null && Inventory.CanBePut(prefab))
if (prefab != null && Inventory != null && Inventory.CanProbablyBePut(prefab))
{
bool isEditor = false;
#if CLIENT
@@ -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,25 +213,44 @@ 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;
private float userCanInteractCheckTimer;
private const float UserCanInteractCheckInterval = 1.0f;
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;
userCanInteractCheckTimer -= deltaTime;
if (user == null
|| user.Removed
|| !user.IsAnySelectedItem(item)
|| item.ParentInventory != null
|| !user.CanInteractWith(item)
|| (item.ParentInventory != null && !IsAttachedUser(user))
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater))
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater)
|| !CheckUserCanInteract())
{
if (user != null)
{
@@ -228,6 +261,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)
@@ -330,6 +374,22 @@ namespace Barotrauma.Items.Components
}
}
private bool CheckUserCanInteract()
{
//optimization: CanInteractWith is relatively heavy (can involve visibility checks for example), let's not do it every frame
if (user != null)
{
if (userCanInteractCheckTimer <= 0.0f)
{
userCanInteractCheckTimer = UserCanInteractCheckInterval;
return user.CanInteractWith(item);
}
}
//we only do the actual check every UserCanInteractCheckInterval seconds
//can mean the component can stay selected for <1s after the user no longer has access to it
return true;
}
private double lastUsed;
public override bool Use(float deltaTime, Character activator = null)
@@ -344,6 +404,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 +442,8 @@ namespace Barotrauma.Items.Components
return false;
}
if (IsOutOfPower()) { return false; }
focusTarget = GetFocusTarget();
if (focusTarget == null)
@@ -417,11 +481,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 +520,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 +613,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 +633,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)
@@ -206,7 +206,7 @@ namespace Barotrauma.Items.Components
private void StartFabricating(FabricationRecipe selectedItem, Character user, bool addToServerLog = true)
{
if (selectedItem == null) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
if (!outputContainer.Inventory.CanProbablyBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
IsActive = true;
this.user = user;
@@ -319,7 +319,7 @@ namespace Barotrauma.Items.Components
}
else
{
hasPower = Voltage >= MinVoltage;
hasPower = HasPower;
if (!hasPower)
{
@@ -499,12 +499,13 @@ namespace Barotrauma.Items.Components
{
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier);
}
InvSlotType invSlot = fabricatedItem.MoveToSlot;
if (i < amountFittingContainer)
{
Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality,
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
onItemSpawned(spawnedItem, tempUser, invSlot);
spawnedItem.Quality = quality;
spawnedItem.StolenDuringRound = ingredientsStolen;
spawnedItem.AllowStealing = ingredientsAllowStealing;
@@ -517,7 +518,7 @@ namespace Barotrauma.Items.Components
Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality,
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
onItemSpawned(spawnedItem, tempUser, invSlot);
spawnedItem.Quality = quality;
spawnedItem.StolenDuringRound = ingredientsStolen;
spawnedItem.AllowStealing = ingredientsAllowStealing;
@@ -527,15 +528,28 @@ namespace Barotrauma.Items.Components
}
}
void onItemSpawned(Item spawnedItem, Character user)
void onItemSpawned(Item spawnedItem, Character user, InvSlotType slot)
{
if (user != null && user.TeamID != CharacterTeamType.None)
CharacterTeamType teamID = CharacterTeamType.None;
if (user != null)
{
teamID = user.TeamID;
}
else if (item.Submarine != null)
{
teamID = item.Submarine.TeamID;
}
if (teamID != CharacterTeamType.None)
{
foreach (WifiComponent wifiComponent in spawnedItem.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = user.TeamID;
wifiComponent.TeamID = teamID;
}
}
if (slot != InvSlotType.None)
{
user?.Inventory.TryPutItem(spawnedItem, user, slot.ToEnumerable());
}
OnItemFabricated?.Invoke(spawnedItem, user);
}
if (user?.Info != null && !user.Removed)
@@ -562,7 +576,6 @@ namespace Barotrauma.Items.Components
StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false);
}
}
}
/// <summary>
@@ -720,17 +733,24 @@ namespace Barotrauma.Items.Components
private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
public bool MissingRequiredRecipe(FabricationRecipe fabricableItem, Character character)
{
if (fabricableItem == null) { return false; }
if (fabricableItem.RequiresRecipe)
if (fabricableItem.RequiresRecipe)
{
if (character == null) { return false; }
if (!AnyOneHasRecipeForItem(character, fabricableItem.TargetItem))
{
return false;
return true;
}
}
return false;
}
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
{
if (fabricableItem == null) { return false; }
if (MissingRequiredRecipe(fabricableItem, character)) { return false; }
if (fabricableItem.HideForNonTraitors)
{
@@ -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
@@ -80,6 +80,7 @@ namespace Barotrauma.Items.Components
private set
{
if (lastUser == value) { return; }
if (Screen.Selected.IsEditor) { return; }
lastUser = value;
if (lastUser == null)
{
@@ -246,6 +247,13 @@ namespace Barotrauma.Items.Components
}
}
//rapidly adjust the reactor in the first few seconds of the round to prevent overvoltages if the load changed between rounds
//(unless the reactor is being operated by a player)
if (GameMain.GameSession is { RoundDuration: <5 } && lastUser is not { IsPlayer: true })
{
UpdateAutoTemp(100.0f, (float)(Timing.Step * 10.0f));
}
#if CLIENT
if (PowerOn && AvailableFuel < 1)
{
@@ -340,7 +348,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 +715,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);
@@ -8,6 +8,8 @@ namespace Barotrauma.Items.Components
{
partial class Sonar : Powered, IServerSerializable, IClientSerializable
{
public static List<Sonar> SonarList = new List<Sonar>();
public enum Mode
{
Active,
@@ -167,6 +169,7 @@ namespace Barotrauma.Items.Components
IsActive = true;
InitProjSpecific(element);
CurrentMode = Mode.Passive;
SonarList.Add(this);
}
partial void InitProjSpecific(ContentXElement element);
@@ -191,8 +194,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)
{
@@ -380,6 +382,29 @@ namespace Barotrauma.Items.Components
}
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
#if CLIENT
sonarBlip?.Remove();
pingCircle?.Remove();
directionalPingCircle?.Remove();
screenOverlay?.Remove();
screenBackground?.Remove();
lineSprite?.Remove();
foreach (var t in targetIcons.Values)
{
t.Item1.Remove();
}
targetIcons.Clear();
MineralClusters = null;
#endif
SonarList.Remove(this);
}
public void ServerEventRead(IReadMessage msg, Client c)
{
bool isActive = msg.ReadBoolean();
@@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components
{
UpdateOnActiveEffects(deltaTime);
if (Voltage >= MinVoltage)
if (HasPower)
{
sendSignalTimer += deltaTime;
if (sendSignalTimer > SendSignalInterval)
@@ -61,6 +61,7 @@ namespace Barotrauma.Items.Components
private Sonar sonar;
private Submarine controlledSub;
public Submarine ControlledSub => controlledSub;
// AI interfacing
public Vector2 AITacticalTarget { get; set; }
@@ -75,6 +76,7 @@ namespace Barotrauma.Items.Components
private double lastReceivedSteeringSignalTime;
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)]
public bool AutoPilot
{
get { return autoPilot; }
@@ -298,7 +300,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 +313,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
@@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components
SuitablePlantItem plantItem = GetSuitableItem(character);
if (!plantItem.IsNull())
if (!plantItem.IsNull() && item.GetComponent<Holdable>() is not { Attachable: true, Attached: false })
{
Msg = plantItem.Type switch
{
@@ -159,7 +159,6 @@ namespace Barotrauma.Items.Components
{
SuitablePlantItem plantItem = GetSuitableItem(character);
PickingMsg = plantItem.IsNull() ? MsgUprooting : plantItem.ProgressBarMessage;
return base.Pick(character);
}
@@ -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;
@@ -231,7 +232,7 @@ namespace Barotrauma.Items.Components
float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f);
Overload = Voltage > maxOverVoltage;
Overload = Voltage > maxOverVoltage && GameMain.GameSession is not { RoundDuration: < 5 };
if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
@@ -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()
@@ -157,6 +157,13 @@ namespace Barotrauma.Items.Components
{
if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; }
}
if (this is PowerTransfer && item.Condition <= 0.0f)
{
//if the junction box or other power transfer device is broken,
//it cannot be supplying any power (voltage = 0)
return 0.0f;
}
return PowerConsumption <= 0.0f ? 1.0f : voltage;
}
set
@@ -170,6 +177,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; }
@@ -208,7 +217,7 @@ namespace Barotrauma.Items.Components
{
if (!powerOnSoundPlayed && powerOnSound != null)
{
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling, freqMult: powerOnSound.GetRandomFrequencyMultiplier());
SoundPlayer.PlaySound(powerOnSound, item.WorldPosition, hullGuess: item.CurrentHull);
powerOnSoundPlayed = true;
}
}
@@ -668,6 +677,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);
}
@@ -28,20 +28,24 @@ namespace Barotrauma.Items.Components
SpreadCounter = 0;
}
struct HitscanResult
readonly struct HitscanResult
{
public Fixture Fixture;
public Vector2 Point;
public Vector2 Normal;
public float Fraction;
public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction)
public readonly Fixture Fixture;
public readonly Vector2 Point;
public readonly Vector2 Normal;
public readonly float Fraction;
public readonly Submarine Submarine;
public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction, Submarine sub)
{
Fixture = fixture;
Point = point;
Normal = normal;
Fraction = fraction;
Submarine = sub;
}
}
struct Impact
{
public Fixture Fixture;
@@ -184,6 +188,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 +279,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 +335,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!",
@@ -372,8 +397,6 @@ namespace Barotrauma.Items.Components
if (Item.Removed) { return; }
launchPos = simPosition;
LaunchSub = item.Submarine;
//set the rotation of the projectile again because dropping the projectile resets the rotation
Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians), findNewHull: false);
if (DeactivationTime > 0)
{
deactivationTimer = DeactivationTime;
@@ -419,6 +442,14 @@ namespace Barotrauma.Items.Components
//can't launch if already launched
if (StickTarget != null || IsActive) { return false; }
#if SERVER
var owner = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == User);
if (owner != null)
{
Limb.SetLagCompensatedBodyPositions(owner);
}
#endif
float initialRotation = item.body.Rotation;
//if the item is being launched from an inventory, assume it's being fired by a gun that handles setting the rotation correctly
//but if the item is e.g. being thrown by a character, we need to take the direction into account
@@ -440,10 +471,10 @@ namespace Barotrauma.Items.Components
spreadIndex++;
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
Vector2 prevSimpos = item.SimPosition;
item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle);
if (Hitscan)
{
Vector2 prevSimpos = item.SimPosition;
item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle);
DoHitscan(launchDir);
if (i < HitScanCount - 1)
{
@@ -452,9 +483,15 @@ namespace Barotrauma.Items.Components
}
else
{
item.body.SetTransform(item.body.SimPosition, launchAngle);
float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
DoLaunch(launchDir * modifiedLaunchImpulse);
//needs to be set after DoLaunch, because dropping the item resets the rotation and dir
float afterLaunchAngle = launchAngle + (item.body.Dir * LaunchRotationRadians);
if (item.body.Dir < 0)
{
afterLaunchAngle -= MathHelper.Pi;
}
item.SetTransform(item.body.SimPosition, afterLaunchAngle, findNewHull: false);
}
}
User = character;
@@ -579,7 +616,8 @@ namespace Barotrauma.Items.Components
inSubHits[i].Fixture,
inSubHits[i].Point + submarine.SimPosition,
inSubHits[i].Normal,
inSubHits[i].Fraction);
inSubHits[i].Fraction,
sub: null);
}
hits.AddRange(inSubHits);
}
@@ -592,6 +630,7 @@ namespace Barotrauma.Items.Components
{
var h = hits[i];
item.SetTransform(h.Point, rotation);
item.Submarine = h.Submarine;
item.UpdateTransform();
if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
{
@@ -649,8 +688,6 @@ namespace Barotrauma.Items.Components
}
if (fixture.Body.UserData is VineTile) { return true; }
if (fixture.CollidesWith == Category.None) { return true; }
//only collides with characters = probably an "outsideCollisionBlocker" created by a gap
if (fixture.CollidesWith == Physics.CollisionCharacter) { return true; }
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; }
@@ -669,6 +706,11 @@ namespace Barotrauma.Items.Components
if (item.Condition <= 0) { return true; }
if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return true; }
}
else if (fixture.Body.UserData is Gap)
{
//an "outsideCollisionBlocker" created by a gap, should never collide
return true;
}
else if (fixture.Body.UserData is Holdable { CanPush: false })
{
// Ignore holdables that can't push -> shouldn't block
@@ -686,7 +728,7 @@ namespace Barotrauma.Items.Components
fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
hits.Add(new HitscanResult(fixture, rayStart, -dir, 0.0f));
hits.Add(new HitscanResult(fixture, rayStart, -dir, 0.0f, submarine));
return true;
}, ref aabb);
@@ -703,14 +745,17 @@ namespace Barotrauma.Items.Components
return -1;
}
if (fixture.Body.UserData is VineTile) { return -1; }
if (fixture.CollidesWith == Category.None) { return -1; }
//only collides with characters = probably an "outsideCollisionBlocker" created by a gap
if (fixture.CollidesWith == Physics.CollisionCharacter) { return -1; }
if (fixture.CollidesWith == Category.None && fixture.CollisionCategories != Physics.CollisionLagCompensationBody) { return -1; }
if (fixture.Body.UserData is Item item)
{
if (item.Condition <= 0) { return -1; }
if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return -1; }
}
else if (fixture.Body.UserData is Gap)
{
//an "outsideCollisionBlocker" created by a gap, should never collide
return -1;
}
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; }
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
@@ -755,10 +800,10 @@ namespace Barotrauma.Items.Components
}
}
hits.Add(new HitscanResult(fixture, point, normal, fraction));
hits.Add(new HitscanResult(fixture, point, normal, fraction, submarine));
return 1;
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile);
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile | Physics.CollisionLagCompensationBody);
return hits;
}
@@ -830,7 +875,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 +903,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 +918,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 +961,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 +1018,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 +1073,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 +1080,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); }
@@ -1057,11 +1112,11 @@ namespace Barotrauma.Items.Components
{
if (Attack != null)
{
Vector2 pos = item.WorldPosition;
if (item.Submarine == null && damageable is Structure structure && structure.Submarine != null && Vector2.DistanceSquared(item.WorldPosition, structure.WorldPosition) > 10000.0f * 10000.0f)
{
item.Submarine = structure.Submarine;
}
Vector2 pos = item.WorldPosition;
attackResult = Attack.DoDamage(User ?? Attacker, damageable, pos, 1.0f);
}
}
@@ -1154,7 +1209,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 +1273,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 +1334,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)
{
@@ -635,6 +640,18 @@ namespace Barotrauma.Items.Components
OnViewUpdateProjSpecific();
}
public void RemoveWire(Wire wireItem)
{
foreach (CircuitBoxWire wire in Wires.ToImmutableArray())
{
if (wire.BackingWire.TryUnwrap(out var backingWire) && backingWire == wireItem.Item)
{
RemoveWireCollectionUnsafe(wire);
}
}
OnViewUpdateProjSpecific();
}
private void RemoveWireCollectionUnsafe(CircuitBoxWire wire)
{
foreach (CircuitBoxOutputConnection output in Outputs)
@@ -756,6 +773,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();
}
}
@@ -198,6 +198,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize("0,0", IsPropertySaveable.No, description: "Offset of the light from the position of the item (in pixels).")]
public Vector2 LightOffset
{
get;
set;
}
/// <summary>
/// Returns true if the red component of the light is twice as bright as the blue and green. Can be used by StatusEffects.
/// </summary>
@@ -259,7 +266,6 @@ namespace Barotrauma.Items.Components
#endif
IsActive = IsOn;
item.AddTag("light");
}
public override void OnItemLoaded()
@@ -305,17 +311,18 @@ namespace Barotrauma.Items.Components
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
{
if (item.body == null || item.body.Enabled ||
(item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems))
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
}
else
PhysicsBody body = ParentBody ?? item.body;
if ((body == null || !body.Enabled) &&
(item.FindParentInventory(static it => it is ItemInventory { Container.HideItems: true }) != null))
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
}
else
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
}
isOn = true;
SetLightSourceTransformProjSpecific();
base.IsActive = false;
@@ -367,7 +374,7 @@ namespace Barotrauma.Items.Components
SetLightSourceTransformProjSpecific();
PhysicsBody body = ParentBody ?? item.body;
if (body != null && !body.Enabled && !visibleInContainer)
if ((body == null || !body.Enabled) && !visibleInContainer)
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
@@ -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
{
@@ -12,6 +15,9 @@ namespace Barotrauma.Items.Components
private Vector2 detectOffset;
private float updateTimer;
[Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
public bool MotionDetected { get; set; }
[Flags]
public enum TargetType
@@ -23,15 +29,34 @@ namespace Barotrauma.Items.Components
Any = Human | Monster | Wall | Pet,
}
[Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
public bool MotionDetected { get; set; }
private bool triggerFromHumans = true;
private bool triggerFromPets = true;
private bool triggerFromMonsters = true;
private TargetType _target;
[InGameEditable, Serialize(TargetType.Any, IsPropertySaveable.Yes, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)]
public TargetType Target
{
get;
set;
get => _target;
set
{
if (_target != value)
{
_target = value;
triggerFromHumans = Target.HasFlag(TargetType.Human);
triggerFromPets = Target.HasFlag(TargetType.Pet);
triggerFromMonsters = Target.HasFlag(TargetType.Monster);
}
}
}
[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 +65,6 @@ namespace Barotrauma.Items.Components
set;
}
[InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Horizontal detection range.", alwaysUseInstanceValues: true)]
public float RangeX
{
@@ -248,51 +272,37 @@ namespace Barotrauma.Items.Components
if (worldBorders.Intersects(detectRect))
{
MotionDetected = true;
return;
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine == sub &&
wall.WorldRect.Intersects(detectRect))
{
MotionDetected = true;
return;
}
}
}
}
}
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; }
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 +314,53 @@ namespace Barotrauma.Items.Components
}
}
}
public bool TriggersOn(Character character)
{
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");
}
@@ -80,7 +80,6 @@ namespace Barotrauma.Items.Components
set
{
isOn = value;
CanTransfer = value;
if (!isOn)
{
currPowerConsumption = 0.0f;
@@ -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"); }
}
}
@@ -96,8 +96,20 @@ namespace Barotrauma.Items.Components
[Editable, Serialize("> ", IsPropertySaveable.Yes)]
public string LineStartSymbol { get; set; }
[Editable, Serialize(false, IsPropertySaveable.No)]
public bool Readonly { get; set; }
private bool _readonly;
[Editable, Serialize(false, IsPropertySaveable.Yes)]
public bool Readonly
{
get => _readonly;
set
{
_readonly = value;
#if CLIENT
RefreshInputElements();
#endif
}
}
[Serialize(true, IsPropertySaveable.No)]
public bool AutoScrollToBottom { get; set; }
@@ -8,7 +8,7 @@ using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class WifiComponent : ItemComponent, IServerSerializable
partial class WifiComponent : ItemComponent, IServerSerializable, IClientSerializable
{
private static readonly List<WifiComponent> list = new List<WifiComponent>();
@@ -56,7 +56,6 @@ namespace Barotrauma.Items.Components
}
}
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.", alwaysUseInstanceValues: true)]
public bool AllowCrossTeamCommunication
{
@@ -229,14 +228,20 @@ namespace Barotrauma.Items.Components
public void TransmitSignal(Signal signal, bool sentFromChat)
{
if (sentFromChat)
{
item.LastSentSignalRecipients.Clear();
}
bool chatMsgSent = false;
var receivers = GetReceiversInRange();
if (sentFromChat)
{
//if sent from chat, we need to reset the "signal chain" at this point
//so we can correctly detect which components the signal has already passed through to avoid infinite loops
//only relevant for signals originating from the chat - normally this is handled in Item.SendSignal
item.LastSentSignalRecipients.Clear();
foreach (WifiComponent receiver in receivers)
{
receiver.item.LastSentSignalRecipients.Clear();
}
}
foreach (WifiComponent wifiComp in receivers)
{
if (sentFromChat && !wifiComp.LinkToChat) { continue; }
@@ -366,5 +371,25 @@ namespace Barotrauma.Items.Components
element.Add(new XAttribute("channelmemory", string.Join(',', channelMemory)));
return element;
}
protected void SharedEventWrite(IWriteMessage msg)
{
msg.WriteRangedInteger(Channel, MinChannel, MaxChannel);
for (int i = 0; i < ChannelMemorySize; i++)
{
msg.WriteRangedInteger(channelMemory[i], MinChannel, MaxChannel);
}
}
protected void SharedEventRead(IReadMessage msg)
{
Channel = msg.ReadRangedInteger(MinChannel, MaxChannel);
for (int i = 0; i < ChannelMemorySize; i++)
{
channelMemory[i] = msg.ReadRangedInteger(MinChannel, MaxChannel);
}
}
}
}
@@ -66,6 +66,7 @@ namespace Barotrauma.Items.Components
private float editNodeDelay;
private bool locked;
public bool Locked
{
get
@@ -83,6 +84,13 @@ namespace Barotrauma.Items.Components
public float Length { get; private set; }
[Serialize(0.3f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f, DecimalCount = 2)]
public float Width
{
get;
set;
}
[Serialize(5000.0f, IsPropertySaveable.No, description: "The maximum distance the wire can extend (in pixels).")]
public float MaxLength
{
@@ -884,8 +892,13 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
if (item.Container?.GetComponent<CircuitBox>() is { } circuitBox)
{
circuitBox.RemoveWire(this);
}
ClearConnections();
base.RemoveComponentSpecific();
#if CLIENT
if (DraggingWire == this) { draggingWire = null; }
overrideSprite?.Remove();
@@ -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,93 @@ 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 (item.FlippedX)
{
offset.X = -offset.X;
}
if (item.FlippedY)
{
offset.Y = -offset.Y;
}
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 FlipX(bool relativeToSub)
{
SetPhysicsBodyPosition();
}
public override void FlipY(bool relativeToSub)
{
SetPhysicsBodyPosition();
}
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 +318,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 +336,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 +380,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 +432,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 +450,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,22 @@ 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("OwnSub", IsPropertySaveable.Yes, description: "[Auto Operate] Team that the turret considers friendly."),
Editable(TransferToSwappedItem = true)]
public TeamType FriendlyTeamType { get; private set; }
public enum TeamType
{
OwnSub,
Team1,
Team2,
FriendlyNPC,
NoneTeam
}
#endregion
private const string SetAutoOperateConnection = "set_auto_operate";
private const string ToggleAutoOperateConnection = "toggle_auto_operate";
@@ -330,7 +346,7 @@ namespace Barotrauma.Items.Components
: base(item, element)
{
IsActive = true;
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -341,6 +357,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;
@@ -376,6 +398,7 @@ namespace Barotrauma.Items.Components
base.OnMapLoaded();
if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; }
if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; }
if (loadedFriendlyTeamType.HasValue) { FriendlyTeamType = loadedFriendlyTeamType.Value; }
targetRotation = Rotation;
UpdateTransformedBarrelPos();
if (!AllowAutoOperateWithWiring &&
@@ -779,6 +802,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 +835,8 @@ namespace Barotrauma.Items.Components
}
}
lastProjectile = launchedProjectile;
#if SERVER
if (character != null && launchedProjectile != null)
{
@@ -900,6 +931,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;
@@ -1027,7 +1062,7 @@ namespace Barotrauma.Items.Components
}
if (TargetItems)
{
foreach (Item targetItem in Item.ItemList)
foreach (Item targetItem in Item.TurretTargetItems)
{
if (!IsValidTarget(targetItem)) { continue; }
float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority;
@@ -1117,7 +1152,7 @@ namespace Barotrauma.Items.Components
if (target is Hull targetHull)
{
Vector2 barrelDir = GetBarrelDir();
if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
if (!MathUtils.GetLineWorldRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
{
return;
}
@@ -1351,7 +1386,7 @@ namespace Barotrauma.Items.Components
}
// Don't aim monsters that are inside any submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
if (HumanAIController.IsFriendly(character, enemy, ignoreHuskDisguising: true)) { continue; }
// Don't shoot at captured enemies.
if (enemy.LockHands) { continue; }
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
@@ -1371,7 +1406,7 @@ namespace Barotrauma.Items.Components
closestDistance = dist / priority;
currentTarget = closestEnemy;
}
foreach (Item targetItem in Item.ItemList)
foreach (Item targetItem in Item.TurretTargetItems)
{
if (!IsValidTarget(targetItem)) { continue; }
float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority;
@@ -1675,22 +1710,34 @@ namespace Barotrauma.Items.Components
return true;
}
private CharacterTeamType GetFriendlyTeam()
{
return FriendlyTeamType switch
{
TeamType.Team1 => CharacterTeamType.Team1,
TeamType.Team2 => CharacterTeamType.Team2,
TeamType.FriendlyNPC => CharacterTeamType.FriendlyNPC,
TeamType.NoneTeam => CharacterTeamType.None,
TeamType.OwnSub => item.Submarine?.TeamID ?? CharacterTeamType.None,
_ => throw new NotImplementedException(),
};
}
private bool IsValidTargetForAutoOperate(Character target, Identifier friendlyTag)
{
if (!friendlyTag.IsEmpty)
{
if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) { return false; }
}
CharacterTeamType friendlyTeam = GetFriendlyTeam();
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;
}
return TargetHumans;
return !target.IsOnFriendlyTeam(friendlyTeam) && TargetHumans;
}
else
{
@@ -1719,7 +1766,7 @@ namespace Barotrauma.Items.Components
{
if (user != null)
{
if (HumanAIController.IsFriendly(user, targetCharacter))
if (HumanAIController.IsFriendly(user, targetCharacter, ignoreHuskDisguising: true))
{
return false;
}
@@ -1739,8 +1786,15 @@ namespace Barotrauma.Items.Components
Submarine sub = e.Submarine ?? e as Submarine;
if (sub == null) { return true; }
if (sub == Item.Submarine) { return false; }
if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; }
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon || sub.Info.IsRuin) { return false; }
if (item.Submarine == null)
{
if (sub.TeamID == GetFriendlyTeam()) { return false; }
}
else
{
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
}
}
else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true })
{
@@ -1758,6 +1812,8 @@ namespace Barotrauma.Items.Components
customPredicate: (Fixture f) =>
{
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
if (f.CollidesWith == Physics.CollisionNone) { return false; }
if (f.Body.UserData == item) { return false; }
if (f.UserData is Hull) { return false; }
return !item.StaticFixtures.Contains(f);
});
@@ -1792,6 +1848,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;
@@ -1974,11 +2032,27 @@ namespace Barotrauma.Items.Components
private Vector2? loadedRotationLimits;
private float? loadedBaseRotation;
private TeamType? loadedFriendlyTeamType;
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
loadedRotationLimits = componentElement.GetAttributeVector2("rotationlimits", RotationLimits);
loadedBaseRotation = componentElement.GetAttributeFloat("baserotation", componentElement.Parent.GetAttributeFloat("rotation", BaseRotation));
//backwards compatibility: previously None made the turret consider the team the submarine/outpost belongs as the friendly team
if (componentElement.GetAttribute("FriendlyTeam") is { } friendlyTeamAttribute)
{
CharacterTeamType friendlyTeam = XMLExtensions.ParseEnumValue(friendlyTeamAttribute.Value, defaultValue: CharacterTeamType.None, friendlyTeamAttribute);
loadedFriendlyTeamType = friendlyTeam switch
{
CharacterTeamType.None => TeamType.OwnSub,
CharacterTeamType.Team1 => TeamType.Team1,
CharacterTeamType.Team2 => TeamType.Team2,
CharacterTeamType.FriendlyNPC => TeamType.FriendlyNPC,
_ => throw new NotImplementedException()
};
}
}
public override void OnItemLoaded()
@@ -1991,6 +2065,8 @@ namespace Barotrauma.Items.Components
if (item.FlippedX) { FlipX(relativeToSub: false); }
if (item.FlippedY) { FlipY(relativeToSub: false); }
}
UpdateTransformedBarrelPos();
UpdateLightComponents();
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
@@ -165,10 +165,11 @@ namespace Barotrauma
{
if (element.DoesAttributeReferenceFileNameAlone("texture"))
{
var basePrefab = WearableComponent.Item.Prefab.ParentPrefab ?? WearableComponent.Item.Prefab;
string textureName = element.GetAttributeString("texture", "");
return ContentPath.FromRaw(
element.ContentPackage,
$"{Path.GetDirectoryName(WearableComponent.Item.Prefab.FilePath)}/{textureName}");
$"{Path.GetDirectoryName(basePrefab.FilePath)}/{textureName}");
}
else
{
@@ -269,6 +270,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 +358,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 +577,7 @@ namespace Barotrauma.Items.Components
foreach (WearableSprite wearableSprite in wearableSprites)
{
wearableSprite?.Sprite?.Remove();
wearableSprite.Picker = null;
wearableSprite.Remove();
}
}