Merge remote-tracking branch 'upstream/master' into develop

This commit is contained in:
EvilFactory
2024-04-24 12:20:11 -03:00
397 changed files with 15250 additions and 6473 deletions
@@ -326,6 +326,8 @@ namespace Barotrauma.Items.Components
ConnectWireBetweenPorts();
CreateJoint(true);
item.SendSignal("1", "on_dock");
DockingTarget.Item.SendSignal("1", "on_dock");
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
@@ -655,7 +657,8 @@ namespace Barotrauma.Items.Components
hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
hulls[i] = new Hull(hullRects[i], subs[i])
{
RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"
RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch",
AvoidStaying = true
};
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
@@ -974,6 +977,7 @@ namespace Barotrauma.Items.Components
item.linkedTo.Clear();
docked = false;
item.SendSignal("1", "on_undock");
Item.Submarine.RefreshOutdoorNodes();
Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine);
@@ -165,6 +165,8 @@ namespace Barotrauma.Items.Components
public bool IsHorizontal { get; private set; }
public bool IsConvexHullHorizontal => autoOrientGap && linkedGap != null ? !linkedGap.IsHorizontal : IsHorizontal;
[Serialize("0.0,0.0,0.0,0.0", IsPropertySaveable.No, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")]
public Rectangle Window { get; set; }
@@ -321,7 +323,7 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (item.Condition < RepairThreshold && item.GetComponent<Repairable>().HasRequiredItems(picker, addMessage: false)) { return true; }
if (requiredItems.None()) { return false; }
if (RequiredItems.None()) { return false; }
if (HasAccess(picker) && HasRequiredItems(picker, false)) { return false; }
return base.Pick(picker);
}
@@ -559,6 +561,7 @@ namespace Barotrauma.Items.Components
public void RefreshLinkedGap()
{
LinkedGap.Layer = item.Layer;
LinkedGap.ConnectedDoor = this;
if (autoOrientGap)
{
@@ -572,10 +575,10 @@ namespace Barotrauma.Items.Components
{
RefreshLinkedGap();
#if CLIENT
convexHull = new ConvexHull(doorRect, IsHorizontal, item);
convexHull = new ConvexHull(doorRect, IsConvexHullHorizontal, item);
if (Window != Rectangle.Empty)
{
convexHull2 = new ConvexHull(doorRect, IsHorizontal, item);
convexHull2 = new ConvexHull(doorRect, IsConvexHullHorizontal, item);
}
UpdateConvexHulls();
#endif
@@ -77,6 +77,9 @@ namespace Barotrauma.Items.Components
{
Stem = 0b0000,
CrossJunction = 0b1111,
HorizontalLine = 0b1010,
VerticalLine = 0b0101,
/*backwards compatibility, the vertical and horizontal "lane" used to be backwards*/
VerticalLane = 0b1010,
HorizontalLane = 0b0101,
TurnTopRight = 0b1001,
@@ -32,11 +32,6 @@ namespace Barotrauma.Items.Components
private LocalizedString prevMsg;
private Dictionary<RelatedItem.RelationType, List<RelatedItem>> prevRequiredItems;
//the distance from the holding characters elbow to center of the physics body of the item
protected Vector2 holdPos;
protected Vector2 aimPos;
private float swingState;
private Character prevEquipper;
@@ -131,6 +126,9 @@ namespace Barotrauma.Items.Components
get { return ConvertUnits.ToDisplayUnits(holdPos); }
set { holdPos = ConvertUnits.ToSimUnits(value); }
}
//the distance from the holding characters elbow to center of the physics body of the item
protected Vector2 holdPos;
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+
" Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")]
@@ -139,6 +137,7 @@ namespace Barotrauma.Items.Components
get { return ConvertUnits.ToDisplayUnits(aimPos); }
set { aimPos = ConvertUnits.ToSimUnits(value); }
}
protected Vector2 aimPos;
protected float holdAngle;
#if DEBUG
@@ -258,12 +257,12 @@ namespace Barotrauma.Items.Components
}
canBePicked = true;
prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(RequiredItems);
if (attachable)
{
prevMsg = DisplayMsg;
prevPickKey = PickKey;
prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
if (item.Submarine != null)
{
@@ -320,7 +319,7 @@ namespace Barotrauma.Items.Components
if (attachable)
{
prevMsg = DisplayMsg;
prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(RequiredItems);
}
}
@@ -649,7 +648,7 @@ namespace Barotrauma.Items.Components
DisplayMsg = prevMsg;
PickKey = prevPickKey;
requiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(prevRequiredItems);
RequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(prevRequiredItems);
Attached = true;
#if CLIENT
@@ -667,7 +666,7 @@ namespace Barotrauma.Items.Components
item.DrawDepthOffset = 0.0f;
#endif
//make the item pickable with the default pick key and with no specific tools/items when it's deattached
requiredItems.Clear();
RequiredItems.Clear();
DisplayMsg = "";
PickKey = InputType.Select;
#if CLIENT
@@ -916,15 +915,25 @@ namespace Barotrauma.Items.Components
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim && !UsageDisabledByRangedWeapon(picker);
if (aim)
{
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle);
if (picker.AnimController.IsHoldingToRope && GetRope() is { Snapped: false } rope)
{
Vector2 targetPos = Submarine.GetRelativeSimPosition(picker, rope.Item);
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: aimPos, aim: true, holdAngle, aimAngle, targetPos: targetPos);
}
else
{
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: aimPos + swingPos, aim: true, holdAngle, aimAngle);
}
}
else
{
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle);
var rope = GetRope();
if (rope != null && rope.SnapWhenNotAimed && rope.Item.ParentInventory == null)
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: holdPos + swingPos, aim: false, holdAngle);
if (GetRope() is { SnapWhenNotAimed: true } rope)
{
rope.Snap();
if (rope.Item.ParentInventory == null)
{
rope.Snap();
}
}
}
}
@@ -1054,15 +1063,15 @@ namespace Barotrauma.Items.Components
}
var tempMsg = DisplayMsg;
var tempRequiredItems = requiredItems;
var tempRequiredItems = RequiredItems;
DisplayMsg = prevMsg;
requiredItems = prevRequiredItems;
RequiredItems = prevRequiredItems;
XElement saveElement = base.Save(parentElement);
DisplayMsg = tempMsg;
requiredItems = tempRequiredItems;
RequiredItems = tempRequiredItems;
return saveElement;
}
@@ -126,7 +126,7 @@ namespace Barotrauma.Items.Components
return;
}
holdable.Reattachable = false;
if (requiredItems.Any())
if (RequiredItems.Any())
{
holdable.PickingTime = float.MaxValue;
}
@@ -91,7 +91,9 @@ namespace Barotrauma.Items.Components
public override void Equip(Character character)
{
base.Equip(character);
reloadTimer = Math.Min(reload, 1.0f);
//force a wait of at least 1 second when equipping the weapon, so you can't "rapid-fire" by swapping between weapons
const float forcedDelayOnEquip = 1.0f;
reloadTimer = Math.Max(Math.Min(reload, forcedDelayOnEquip), reloadTimer);
IsActive = true;
}
@@ -218,13 +220,12 @@ namespace Barotrauma.Items.Components
AnimController ac = picker.AnimController;
if (!hitting)
{
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim &&
!UsageDisabledByRangedWeapon(picker);
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim && !UsageDisabledByRangedWeapon(picker);
if (aim)
{
UpdateSwingPos(deltaTime, out Vector2 swingPos);
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true);
ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos + swingPos, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true);
if (ac.InWater)
{
ac.LockFlipping();
@@ -233,7 +234,7 @@ namespace Barotrauma.Items.Components
else
{
hitPos = 0;
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle);
}
}
else
@@ -242,11 +243,11 @@ namespace Barotrauma.Items.Components
hitPos -= deltaTime * 15f;
if (Swing)
{
ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle);
ac.HoldItem(deltaTime, item, handlePos, itemPos: SwingPos, aim: false, hitPos, holdAngle);
}
else
{
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle);
}
if (hitPos < -MathHelper.Pi)
{
@@ -82,9 +82,9 @@ namespace Barotrauma.Items.Components
var abilityPickingTime = new AbilityItemPickingTime(PickingTime, item.Prefab);
picker.CheckTalents(AbilityEffectType.OnItemPicked, abilityPickingTime);
if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped))
if (RequiredItems.ContainsKey(RelatedItem.RelationType.Equipped))
{
foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped])
foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Equipped])
{
foreach (var heldItem in picker.HeldItems)
{
@@ -95,6 +95,20 @@ namespace Barotrauma.Items.Components
get;
private set;
}
[Serialize(defaultValue: 1f, IsPropertySaveable.Yes, description: "Penalty multiplier to reload time when dual-wielding.")]
public float DualWieldReloadTimePenaltyMultiplier
{
get;
private set;
}
[Serialize(defaultValue: 0f, IsPropertySaveable.Yes, description: "Additive penalty to accuracy (spread angle) when dual-wielding.")]
public float DualWieldAccuracyPenalty
{
get;
private set;
}
private readonly IReadOnlySet<Identifier> suitableProjectiles;
@@ -128,8 +142,11 @@ namespace Barotrauma.Items.Components
: base(item, element)
{
item.IsShootable = true;
// TODO: should define this in xml if we have ranged weapons that don't require aim to use
item.RequireAimToUse = true;
if (element.Parent is { } parent)
{
item.RequireAimToUse = parent.GetAttributeBool(nameof(item.RequireAimToUse), true);
}
characterUsable = true;
suitableProjectiles = element.GetAttributeIdentifierArray(nameof(suitableProjectiles), Array.Empty<Identifier>()).ToHashSet();
if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload)
@@ -140,7 +157,7 @@ namespace Barotrauma.Items.Components
InitProjSpecific(element);
}
partial void InitProjSpecific(ContentXElement element);
partial void InitProjSpecific(ContentXElement rangedWeaponElement);
public override void Equip(Character character)
{
@@ -194,9 +211,28 @@ namespace Barotrauma.Items.Components
float degreeOfFailure = MathHelper.Clamp(1.0f - DegreeOfSuccess(user), 0.0f, 1.0f);
degreeOfFailure *= degreeOfFailure;
float spread = MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure) / (1f + user.GetStatValue(StatTypes.RangedSpreadReduction));
if (user.IsDualWieldingRangedWeapons())
{
spread += Math.Max(0f, ApplyDualWieldPenaltyReduction(user, DualWieldAccuracyPenalty, neutralValue: 0f));
}
return MathHelper.ToRadians(spread);
}
/// <summary>
/// Lerps between the original penalty and a neutral value, which should be 1 for multipliers and 0 for additive penalties.
/// </summary>
/// <param name="character">The character to get stat values from</param>
/// <param name="originalPenalty">The original penalty value</param>
/// <param name="neutralValue">Neutral value to lerp towards. Should be 1 for multipliers and 0 for additives.</param>
/// <returns></returns>
private static float ApplyDualWieldPenaltyReduction(Character character, float originalPenalty, float neutralValue)
{
float statAdjustmentPrc = character.GetStatValue(StatTypes.DualWieldingPenaltyReduction);
statAdjustmentPrc = MathHelper.Clamp(statAdjustmentPrc, 0f, 1f);
float reducedPenaltyMultiplier = MathHelper.Lerp(originalPenalty, neutralValue, statAdjustmentPrc);
return reducedPenaltyMultiplier;
}
private readonly List<Body> ignoredBodies = new List<Body>();
public override bool Use(float deltaTime, Character character = null)
{
@@ -208,22 +244,27 @@ namespace Barotrauma.Items.Components
IsActive = true;
float baseReloadTime = reload;
float weaponSkill = character.GetSkillLevel("weapons");
if (ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement)
bool applyReloadFailure = ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement;
if (applyReloadFailure)
{
//Examples, assuming 40 weapon skill required: 1 - 40/40 = 0 ... 1 - 0/40 = 1 ... 1 - 20 / 40 = 0.5
float reloadFailure = MathHelper.Clamp(1 - (weaponSkill / ReloadSkillRequirement), 0, 1);
baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure);
}
if (character.IsDualWieldingRangedWeapons())
{
baseReloadTime *= Math.Max(1f, ApplyDualWieldPenaltyReduction(character, DualWieldReloadTimePenaltyMultiplier, neutralValue: 1f));
}
ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
ReloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.FiringRateMultiplier);
currentChargeTime = 0f;
if (character != null)
{
var abilityRangedWeapon = new AbilityRangedWeapon(item);
character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon);
}
var abilityRangedWeapon = new AbilityRangedWeapon(item);
character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon);
if (item.AiTarget != null)
{
@@ -90,12 +90,11 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")]
public bool RepairThroughHoles { get; set; }
[Serialize(100.0f, IsPropertySaveable.No, description: "How far two walls need to not be considered overlapping and to stop the ray.")]
public float MaxOverlappingWallDist
{
get; set;
}
public float MaxOverlappingWallDist { get; set; }
[Serialize(1.0f, IsPropertySaveable.No, description: "How fast the tool detaches level resources (e.g. minerals). Acts as a multiplier on the speed: with a value of 2, detaching an item whose DeattachDuration is set to 30 seconds would take 15 seconds.")]
public float DeattachSpeed { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Can the item hit doors.")]
public bool HitItems { get; set; }
@@ -171,7 +170,7 @@ namespace Barotrauma.Items.Components
}
}
item.IsShootable = true;
item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true);
item.RequireAimToUse = element.Parent.GetAttributeBool(nameof(item.RequireAimToUse), true);
InitProjSpecific(element);
}
@@ -321,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.CollisionRepair;
var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall;
if (!IgnoreCharacters)
{
collisionCategories |= Physics.CollisionCharacter;
@@ -414,7 +413,7 @@ namespace Barotrauma.Items.Components
break;
}
pickedPosition = rayStart + (rayEnd - rayStart) * thisBodyFraction;
if (FixBody(user, deltaTime, degreeOfSuccess, body))
if (FixBody(user, pickedPosition, deltaTime, degreeOfSuccess, body))
{
lastPickedFraction = thisBodyFraction;
if (bodyType != null) { lastHitType = bodyType; }
@@ -452,7 +451,7 @@ namespace Barotrauma.Items.Components
},
allowInsideFixture: true);
pickedPosition = Submarine.LastPickedPosition;
FixBody(user, deltaTime, degreeOfSuccess, pickedBody);
FixBody(user, pickedPosition, deltaTime, degreeOfSuccess, pickedBody);
lastPickedFraction = Submarine.LastPickedFraction;
}
@@ -543,7 +542,7 @@ namespace Barotrauma.Items.Components
}
}
private bool FixBody(Character user, float deltaTime, float degreeOfSuccess, Body targetBody)
private bool FixBody(Character user, Vector2 hitPosition, float deltaTime, float degreeOfSuccess, Body targetBody)
{
if (targetBody?.UserData == null) { return false; }
@@ -600,7 +599,7 @@ namespace Barotrauma.Items.Components
{
if (Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) is DestructibleLevelWall levelWall)
{
levelWall.AddDamage(-LevelWallFixAmount * deltaTime, item.WorldPosition);
levelWall.AddDamage(-LevelWallFixAmount * deltaTime, ConvertUnits.ToDisplayUnits(hitPosition));
}
return true;
}
@@ -661,10 +660,13 @@ namespace Barotrauma.Items.Components
var levelResource = targetItem.GetComponent<LevelResource>();
if (levelResource != null && levelResource.Attached &&
levelResource.requiredItems.Any() &&
levelResource.RequiredItems.Any() &&
levelResource.HasRequiredItems(user, addMessage: false))
{
float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier));
float addedDetachTime = deltaTime *
DeattachSpeed *
(1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) *
(1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier));
levelResource.DeattachTimer += addedDetachTime;
#if CLIENT
if (targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
@@ -149,7 +149,7 @@ namespace Barotrauma.Items.Components
if (aim || throwState == ThrowState.Initiated)
{
throwAngle = System.Math.Min(throwAngle + deltaTime * 8.0f, ThrowAngleEnd);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle);
ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, throwAngle);
if (throwAngle >= ThrowAngleEnd && throwState == ThrowState.Initiated)
{
throwState = ThrowState.Throwing;
@@ -158,13 +158,13 @@ namespace Barotrauma.Items.Components
else
{
throwAngle = ThrowAngleStart;
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, holdAngle);
}
}
else
{
throwAngle = MathUtils.WrapAnglePi(throwAngle - deltaTime * 15.0f);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle);
ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, throwAngle);
if (throwAngle < 0)
{
@@ -50,10 +50,10 @@ namespace Barotrauma.Items.Components
public readonly Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
public Dictionary<RelatedItem.RelationType, List<RelatedItem>> requiredItems;
public Dictionary<RelatedItem.RelationType, List<RelatedItem>> RequiredItems;
public readonly List<RelatedItem> DisabledRequiredItems = new List<RelatedItem>();
public List<Skill> requiredSkills;
public readonly List<Skill> RequiredSkills = new List<Skill>();
private ItemComponent parent;
public ItemComponent Parent
@@ -288,9 +288,7 @@ namespace Barotrauma.Items.Components
originalElement = element;
name = element.Name.ToString();
SerializableProperties = SerializableProperty.GetProperties(this);
requiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>();
requiredSkills = new List<Skill>();
RequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>();
#if CLIENT
hasSoundsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length];
sounds = new Dictionary<ActionType, List<ItemSound>>();
@@ -338,7 +336,7 @@ namespace Barotrauma.Items.Components
}
else
{
requiredSkills = component.requiredSkills;
RequiredSkills = component.RequiredSkills;
}
}
@@ -391,7 +389,7 @@ namespace Barotrauma.Items.Components
}
Identifier skillIdentifier = subElement.GetAttributeIdentifier("identifier", "");
requiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0)));
RequiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0)));
break;
case "statuseffect":
statusEffectLists ??= new Dictionary<ActionType, List<StatusEffect>>();
@@ -416,7 +414,7 @@ namespace Barotrauma.Items.Components
void LoadStatusEffect(ContentXElement subElement)
{
var statusEffect = StatusEffect.Load(subElement, item.Name);
var statusEffect = StatusEffect.Load(subElement, item.Name + ", " + GetType().Name);
if (!statusEffectLists.TryGetValue(statusEffect.type, out List<StatusEffect> effectList))
{
effectList = new List<StatusEffect>();
@@ -446,11 +444,11 @@ namespace Barotrauma.Items.Components
}
else
{
if (!requiredItems.ContainsKey(ri.Type))
if (!RequiredItems.ContainsKey(ri.Type))
{
requiredItems.Add(ri.Type, new List<RelatedItem>());
RequiredItems.Add(ri.Type, new List<RelatedItem>());
}
requiredItems[ri.Type].Add(ri);
RequiredItems[ri.Type].Add(ri);
}
}
else if (!allowEmpty)
@@ -677,7 +675,7 @@ namespace Barotrauma.Items.Components
public bool HasRequiredSkills(Character character, out Skill insufficientSkill)
{
foreach (Skill skill in requiredSkills)
foreach (Skill skill in RequiredSkills)
{
float characterLevel = character.GetSkillLevel(skill.Identifier);
if (characterLevel < skill.Level * GetSkillMultiplier())
@@ -698,7 +696,7 @@ namespace Barotrauma.Items.Components
/// <returns>0.5f if all the skills meet the skill requirements exactly, 1.0f if they're way above and 0.0f if way less</returns>
public float DegreeOfSuccess(Character character)
{
return DegreeOfSuccess(character, requiredSkills);
return DegreeOfSuccess(character, RequiredSkills);
}
/// <summary>
@@ -733,16 +731,18 @@ namespace Barotrauma.Items.Components
public virtual void FlipY(bool relativeToSub) { }
/// <summary>
/// Shorthand for !HasRequiredContainedItems()
/// Returns true if the item is lacking required contained items, or if there's nothing with a non-zero condition inside.
/// </summary>
public bool IsEmpty(Character user) => !HasRequiredContainedItems(user, addMessage: false);
public bool IsEmpty(Character user) =>
!HasRequiredContainedItems(user, addMessage: false) ||
(Item.OwnInventory != null && !Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null)
{
if (!requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; }
if (!RequiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; }
if (item.OwnInventory == null) { return false; }
foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Contained])
foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Contained])
{
if (!ri.CheckRequirements(user, item))
{
@@ -767,8 +767,8 @@ namespace Barotrauma.Items.Components
{
if (character.IsBot && item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (requiredItems.Count == 0) { return true; }
if (character.Inventory != null && requiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List<RelatedItem> relatedItems))
if (RequiredItems.Count == 0) { return true; }
if (character.Inventory != null && RequiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List<RelatedItem> relatedItems))
{
foreach (RelatedItem relatedItem in relatedItems)
{
@@ -813,13 +813,13 @@ namespace Barotrauma.Items.Components
public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
if (requiredItems.None()) { return true; }
if (RequiredItems.None()) { return true; }
if (character.Inventory == null) { return false; }
bool hasRequiredItems = false;
bool canContinue = true;
if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped))
if (RequiredItems.ContainsKey(RelatedItem.RelationType.Equipped))
{
foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped])
foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Equipped])
{
canContinue = CheckItems(ri, character.HeldItems);
if (!canContinue) { break; }
@@ -827,9 +827,9 @@ namespace Barotrauma.Items.Components
}
if (canContinue)
{
if (requiredItems.ContainsKey(RelatedItem.RelationType.Picked))
if (RequiredItems.ContainsKey(RelatedItem.RelationType.Picked))
{
foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Picked])
foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Picked])
{
if (!CheckItems(ri, character.Inventory.AllItems)) { break; }
}
@@ -1058,7 +1058,7 @@ namespace Barotrauma.Items.Components
{
XElement componentElement = new XElement(name);
foreach (var kvp in requiredItems)
foreach (var kvp in RequiredItems)
{
foreach (RelatedItem ri in kvp.Value)
{
@@ -1091,8 +1091,8 @@ namespace Barotrauma.Items.Components
private void OverrideRequiredItems(ContentXElement element)
{
var prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
requiredItems.Clear();
var prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(RequiredItems);
RequiredItems.Clear();
bool returnEmptyRequirements = false;
#if CLIENT
@@ -1117,11 +1117,11 @@ namespace Barotrauma.Items.Components
newRequiredItem.IgnoreInEditor = prevRequiredItem.IgnoreInEditor;
}
if (!requiredItems.ContainsKey(newRequiredItem.Type))
if (!RequiredItems.ContainsKey(newRequiredItem.Type))
{
requiredItems[newRequiredItem.Type] = new List<RelatedItem>();
RequiredItems[newRequiredItem.Type] = new List<RelatedItem>();
}
requiredItems[newRequiredItem.Type].Add(newRequiredItem);
RequiredItems[newRequiredItem.Type].Add(newRequiredItem);
break;
}
}
@@ -673,7 +673,7 @@ namespace Barotrauma.Items.Components
return false;
}
}
if (AutoInteractWithContained && character.SelectedItem == null)
if (AutoInteractWithContained && character.SelectedItem == null && Screen.Selected is not { IsEditor: true })
{
foreach (Item contained in Inventory.AllItems)
{
@@ -708,7 +708,7 @@ namespace Barotrauma.Items.Components
return false;
}
}
if (AutoInteractWithContained)
if (AutoInteractWithContained && Screen.Selected is not { IsEditor: true })
{
foreach (Item contained in Inventory.AllItems)
{
@@ -34,10 +34,16 @@ namespace Barotrauma.Items.Components
get { return outputContainer; }
}
/// <summary>
/// Should the output items left in the deconstructor be automatically moved to the main sub at the end of the round
/// if the deconstructor is not in the main sub?
/// </summary>
public bool RelocateOutputToMainSub;
[Serialize(false, IsPropertySaveable.Yes)]
public bool DeconstructItemsSimultaneously { get; set; }
[Editable, Serialize(1.0f, IsPropertySaveable.Yes)]
[Editable(MinValueFloat = 0.1f, MaxValueFloat = 1000), Serialize(1.0f, IsPropertySaveable.Yes)]
public float DeconstructionSpeed { get; set; }
public Deconstructor(Item item, ContentXElement element)
@@ -290,6 +296,10 @@ namespace Barotrauma.Items.Components
spawnedItem.AllowStealing = targetItem.AllowStealing;
spawnedItem.OriginalOutpost = targetItem.OriginalOutpost;
spawnedItem.SpawnedInCurrentOutpost = targetItem.SpawnedInCurrentOutpost;
if (RelocateOutputToMainSub && user is { AIController: HumanAIController humanAi })
{
humanAi.HandleRelocation(spawnedItem);
}
for (int i = 0; i < outputContainer.Capacity; i++)
{
var containedItem = outputContainer.Inventory.GetItemAt(i);
@@ -318,7 +328,12 @@ namespace Barotrauma.Items.Components
}
}
GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier);
if (targetItem.Prefab.ContentPackage == ContentPackageManager.VanillaCorePackage &&
/* we don't need info of every item, we can get a good sample size just by logging 5% */
Rand.Range(0.0f, 1.0f) < 0.05f)
{
GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier);
}
bool? result = GameMain.LuaCs.Hook.Call<bool?>("item.deconstructed", targetItem, this, user, allowRemove);
if (result == true) { return; }
@@ -332,6 +347,10 @@ namespace Barotrauma.Items.Components
foreach (Item outputItem in ic.Inventory.AllItemsMod)
{
tryPutInOutputSlots(outputItem);
if (RelocateOutputToMainSub && user != null && user.AIController is HumanAIController humanAi)
{
humanAi.HandleRelocation(outputItem);
}
}
}
inputContainer.Inventory.RemoveItem(targetItem);
@@ -439,11 +458,12 @@ namespace Barotrauma.Items.Components
}
}
private void SetActive(bool active, Character user = null)
public void SetActive(bool active, Character user = null, bool createNetworkEvent = false)
{
PutItemsToLinkedContainer();
this.user = user;
RelocateOutputToMainSub = false;
if (inputContainer.Inventory.IsEmpty()) { active = false; }
@@ -456,6 +476,10 @@ namespace Barotrauma.Items.Components
{
GameServer.Log(GameServer.CharacterLogName(user) + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction);
}
if (createNetworkEvent)
{
item.CreateServerEvent(this);
}
#endif
if (!IsActive)
{
@@ -465,7 +489,11 @@ namespace Barotrauma.Items.Components
#if CLIENT
else
{
HintManager.OnStartDeconstructing(user, this);
HintManager.OnStartDeconstructing(user, this);
if (Item.Submarine is { Info.IsOutpost: true } && user is { IsBot: true })
{
HintManager.OnItemMarkedForRelocation();
}
}
#endif
@@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components
private ItemContainer inputContainer, outputContainer;
[Serialize(1.0f, IsPropertySaveable.Yes)]
[Editable(MinValueFloat = 0.1f, MaxValueFloat = 1000), Serialize(1.0f, IsPropertySaveable.Yes)]
public float FabricationSpeed { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes)]
@@ -469,7 +469,10 @@ namespace Barotrauma.Items.Components
character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount);
}
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount);
quality = GetFabricatedItemQuality(fabricatedItem, user).RollQuality();
quality =
fabricatedItem.TargetItem.MaxStackSize > 1 ?
GetFabricatedItemQuality(fabricatedItem, user).Quality :
GetFabricatedItemQuality(fabricatedItem, user).RollQuality();
}
int amount = (int)fabricationitemAmount.Value;
@@ -490,7 +493,12 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < amount; i++)
{
float outCondition = fabricatedItem.OutCondition;
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier);
if (fabricatedItem.TargetItem.ContentPackage == ContentPackageManager.VanillaCorePackage &&
/* we don't need info of every fabricated item, we can get a good sample size just by logging 5% */
Rand.Range(0.0f, 1.0f) < 0.05f)
{
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier);
}
if (i < amountFittingContainer)
{
Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality,
@@ -577,11 +585,11 @@ namespace Barotrauma.Items.Components
public static float CalculateBonusRollPercentage(float skillLevel, float target)
=> Math.Clamp((skillLevel - target) / (100f - target) * 100f, min: 0, max: 100);
public readonly record struct QualityResult(int Quality, float PlusOnePercentage, float PlusTwoPercentage)
public readonly record struct QualityResult(int Quality, bool HasRandomQuality, float PlusOnePercentage, float PlusTwoPercentage)
{
public static readonly QualityResult Empty = new QualityResult(0, 0, 0);
public static readonly QualityResult Empty = new QualityResult(0, true, 0, 0);
public bool HasRandomQualityRollChance => PlusOnePercentage > 0f || PlusTwoPercentage > 0f;
public bool HasRandomQualityRollChance => HasRandomQuality && (PlusOnePercentage > 0f || PlusTwoPercentage > 0f);
// The total real world percentage for a roll to succeed, taking into account that +1 needs to succeed for +2 to be attempted and
// that the chance for only +1 goes down as +2 increases since some of the +1's will turn into +2s
@@ -676,9 +684,23 @@ namespace Barotrauma.Items.Components
}
}
bool hasRandomQuality = !(fabricatedItem.TargetItem.MaxStackSize > 1); //don't randomise items with a stacksize > 1
float PlusOnePercentage = plusOne.Match(some: static f => f, none: static () => 0f);
float PlusTwoPercentage = plusTwo.Match(some: static f => f, none: static () => 0f);
if (!hasRandomQuality && PlusOnePercentage > 0)
{
quality++;
if (PlusTwoPercentage > 0)
{
quality++;
}
}
return new QualityResult(quality,
PlusOnePercentage: plusOne.Match(some: static f => f, none: static () => 0f),
PlusTwoPercentage: plusTwo.Match(some: static f => f, none: static () => 0f));
hasRandomQuality,
PlusOnePercentage,
PlusTwoPercentage);
}
partial void UpdateRequiredTimeProjSpecific();
@@ -690,6 +712,8 @@ namespace Barotrauma.Items.Components
GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier));
}
private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
{
if (fabricableItem == null) { return false; }
@@ -733,22 +757,26 @@ namespace Barotrauma.Items.Components
return false;
}
//maintain a list of used ingredients so we don't end up considering the same item a suitable for multiple required ingredients
usedIngredients.Clear();
return fabricableItem.RequiredItems.All(requiredItem =>
{
int availablePrefabsAmount = 0;
int availableItemsAmount = 0;
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
{
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
if (!availableIngredients.TryGetValue(requiredPrefab.Identifier, out var availableItems)) { continue; }
var availablePrefabs = availableIngredients[requiredPrefab.Identifier];
foreach (Item availablePrefab in availablePrefabs)
foreach (Item availableItem in availableItems)
{
if (requiredItem.IsConditionSuitable(availablePrefab.ConditionPercentage))
if (usedIngredients.Contains(availableItem)) { continue; }
if (requiredItem.IsConditionSuitable(availableItem.ConditionPercentage))
{
availablePrefabsAmount++;
usedIngredients.Add(availableItem);
availableItemsAmount++;
}
if (availablePrefabsAmount >= requiredItem.Amount)
if (availableItemsAmount >= requiredItem.Amount)
{
return true;
}
@@ -91,7 +91,7 @@ namespace Barotrauma.Items.Components
ventList.Clear();
foreach (MapEntity entity in item.linkedTo)
{
if (!(entity is Item linkedItem)) { continue; }
if (entity is not Item linkedItem) { continue; }
Vent vent = linkedItem.GetComponent<Vent>();
if (vent?.Item.CurrentHull == null) { continue; }
@@ -132,5 +132,19 @@ namespace Barotrauma.Items.Components
vent.IsActive = true;
}
}
public float GetVentOxygenFlow(Vent targetVent)
{
if (ventList == null)
{
GetVents();
}
foreach ((Vent vent, float hullVolume) in ventList)
{
if (vent != targetVent) { continue; }
return generatedAmount * 100.0f * (hullVolume / totalHullVolume);
}
return 0.0f;
}
}
}
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -215,10 +214,10 @@ namespace Barotrauma.Items.Components
activePings[currentPingIndex].Direction = pingDirection;
activePings[currentPingIndex].State = 0.0f;
activePings[currentPingIndex].PrevPingRadius = 0.0f;
if (item.AiTarget != null)
foreach (AITarget aiTarget in GetAITargets())
{
item.AiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f;
item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y);
aiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f;
aiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y);
}
item.Use(deltaTime);
}
@@ -231,10 +230,10 @@ namespace Barotrauma.Items.Components
for (var pingIndex = 0; pingIndex < activePingsCount;)
{
if (item.AiTarget != null)
foreach (AITarget aiTarget in GetAITargets())
{
float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom);
item.AiTarget.SoundRange = Math.Max(item.AiTarget.SoundRange, MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range));
float range = MathUtils.InverseLerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom);
aiTarget.SoundRange = Math.Max(aiTarget.SoundRange, MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range));
}
if (activePings[pingIndex].State > 1.0f)
{
@@ -254,6 +253,24 @@ namespace Barotrauma.Items.Components
}
}
private IEnumerable<AITarget> GetAITargets()
{
if (!UseTransducers)
{
if (item.AiTarget != null) { yield return item.AiTarget; }
}
else
{
foreach (var transducer in connectedTransducers)
{
if (transducer.Transducer.Item.AiTarget != null)
{
yield return transducer.Transducer.Item.AiTarget;
}
}
}
}
/// <summary>
/// Power consumption of the sonar. Only consume power when active and adjust the consumption based on the sonar mode.
/// </summary>
@@ -22,6 +22,11 @@ namespace Barotrauma.Items.Components
private const float AutoPilotMaxSpeed = 0.5f;
private const float AIPilotMaxSpeed = 1.0f;
/// <summary>
/// How many units before crush depth the pressure warning is shown
/// </summary>
public const float PressureWarningThreshold = 500.0f;
/// <summary>
/// How fast the steering vector adjusts when the nav terminal is operated by something else than a character (= signals)
/// </summary>
@@ -636,6 +636,8 @@ 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; }
@@ -689,6 +691,8 @@ namespace Barotrauma.Items.Components
}
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.Body.UserData is Item item)
{
if (item.Condition <= 0) { return -1; }
@@ -945,9 +949,15 @@ namespace Barotrauma.Items.Components
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir,
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir,
collisionCategory: Physics.CollisionWall);
Vector2 launchPosInCurrentCoordinateSpace = launchPos;
if (item.body.Submarine == null && LaunchSub != null)
{
launchPosInCurrentCoordinateSpace += ConvertUnits.ToSimUnits(LaunchSub.Position);
}
if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) &&
//ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction
Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0)
Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPosInCurrentCoordinateSpace, dir) > 0)
{
target = wallBody.FixtureList.First();
if (hits.Contains(target.Body))
@@ -144,8 +144,10 @@ namespace Barotrauma.Items.Components
private bool tinkeringPowersDevices;
public bool TinkeringPowersDevices => tinkeringPowersDevices;
public bool IsBelowRepairThreshold => item.ConditionPercentage <= RepairThreshold;
public bool IsBelowRepairIconThreshold => item.ConditionPercentage <= RepairThreshold / 2;
public bool IsBelowRepairThreshold => item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold;
public bool IsBelowRepairIconThreshold => item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold / 2;
public enum FixActions : int
{
@@ -186,6 +188,21 @@ namespace Barotrauma.Items.Components
}
}
// Modify damage (not stun) caused by repair failure based on campaign settings
if (GameMain.GameSession?.Campaign is CampaignMode campaign
&& statusEffectLists != null
&& statusEffectLists.TryGetValue(ActionType.OnFailure, out var onFailureEffects))
{
foreach (var effect in onFailureEffects)
{
foreach (Affliction affliction in effect.Afflictions)
{
if (affliction.Prefab.AfflictionType == Tags.Stun) { continue; }
affliction.Strength *= campaign.Settings.RepairFailMultiplier;
}
}
}
InitProjSpecific(element);
}
@@ -203,12 +220,12 @@ namespace Barotrauma.Items.Components
{
if (character == null) { return false; }
if (statusEffectLists == null || statusEffectLists.None(s => s.Key == ActionType.OnFailure)) { return true; }
if (statusEffectLists == null) { return true; }
if (bestRepairItem != null && bestRepairItem.Prefab.CannotRepairFail) { return true; }
// unpowered (electrical) items can be repaired without a risk of electrical shock
if (requiredSkills.Any(s => s != null && s.Identifier == "electrical"))
if (RequiredSkills.Any(s => s != null && s.Identifier == "electrical"))
{
if (item.GetComponent<Reactor>() is Reactor reactor)
{
@@ -216,18 +233,29 @@ namespace Barotrauma.Items.Components
}
else if (item.GetComponent<Powered>() is Powered powered && powered.Voltage < 0.1f)
{
return true;
return true;
}
}
if (Rand.Range(0.0f, 0.5f) < RepairDegreeOfSuccess(character, requiredSkills)) { return true; }
bool success = Rand.Range(0.0f, 0.5f) < RepairDegreeOfSuccess(character, RequiredSkills);
ActionType actionType = success ? ActionType.OnSuccess : ActionType.OnFailure;
ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
if (bestRepairItem != null && bestRepairItem.GetComponent<Holdable>() is Holdable h)
ApplyStatusEffectsAndCreateEntityEvent(this, actionType, character);
ApplyStatusEffectsAndCreateEntityEvent(this, ActionType.OnUse, character);
if (bestRepairItem != null && bestRepairItem.GetComponent<Holdable>() is Holdable holdable)
{
h.ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
ApplyStatusEffectsAndCreateEntityEvent(holdable, actionType, character);
ApplyStatusEffectsAndCreateEntityEvent(holdable, ActionType.OnUse, character);
}
return false;
static void ApplyStatusEffectsAndCreateEntityEvent(ItemComponent ic, ActionType actionType, Character character)
{
ic.ApplyStatusEffects(actionType, 1.0f, character);
if (GameMain.NetworkMember is { IsServer: true } && ic.statusEffectLists != null && ic.statusEffectLists.ContainsKey(actionType))
{
GameMain.NetworkMember.CreateEntityEvent(ic.Item, new Item.ApplyStatusEffectEventData(actionType, ic, character));
}
}
return success;
}
public override float GetSkillMultiplier()
@@ -251,9 +279,9 @@ namespace Barotrauma.Items.Components
if (CurrentFixer == null) { return; }
if (qteSuccess)
{
item.Condition += RepairDegreeOfSuccess(CurrentFixer, requiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f);
item.Condition += RepairDegreeOfSuccess(CurrentFixer, RequiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f);
}
else if (Rand.Range(0.0f, 2.0f) > RepairDegreeOfSuccess(CurrentFixer, requiredSkills))
else if (Rand.Range(0.0f, 2.0f) > RepairDegreeOfSuccess(CurrentFixer, RequiredSkills))
{
ApplyStatusEffects(ActionType.OnFailure, 1.0f, CurrentFixer);
#if SERVER
@@ -283,12 +311,6 @@ namespace Barotrauma.Items.Components
if (!CheckCharacterSuccess(character, bestRepairItem))
{
GameServer.Log($"{GameServer.CharacterLogName(character)} failed to {(action == FixActions.Sabotage ? "sabotage" : "repair")} {item.Name}", ServerLog.MessageType.ItemInteraction);
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, character));
if (bestRepairItem != null && bestRepairItem.GetComponent<Holdable>() is Holdable h)
{
GameMain.Server?.CreateEntityEvent(bestRepairItem, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, h, character));
}
return false;
}
@@ -449,7 +471,7 @@ namespace Barotrauma.Items.Components
return;
}
float successFactor = requiredSkills.Count == 0 ? 1.0f : RepairDegreeOfSuccess(CurrentFixer, requiredSkills);
float successFactor = RequiredSkills.Count == 0 ? 1.0f : RepairDegreeOfSuccess(CurrentFixer, RequiredSkills);
//item must have been below the repair threshold for the player to get an achievement or XP for repairing it
if (IsBelowRepairThreshold)
@@ -462,9 +484,16 @@ namespace Barotrauma.Items.Components
}
float talentMultiplier = CurrentFixer.GetStatValue(StatTypes.RepairSpeed);
if (requiredSkills.Any(static skill => skill.Identifier == "mechanical"))
foreach (Skill skill in RequiredSkills)
{
talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed);
if (skill.Identifier == "mechanical")
{
talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed);
}
else if (skill.Identifier == "electrical")
{
talentMultiplier += CurrentFixer.GetStatValue(StatTypes.ElectricalRepairSpeed);
}
}
float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor);
@@ -493,7 +522,7 @@ namespace Barotrauma.Items.Components
{
if (wasBroken)
{
foreach (Skill skill in requiredSkills)
foreach (Skill skill in RequiredSkills)
{
CurrentFixer.Info?.ApplySkillGain(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair);
}
@@ -527,7 +556,7 @@ namespace Barotrauma.Items.Components
{
if (wasGoodCondition)
{
foreach (Skill skill in requiredSkills)
foreach (Skill skill in RequiredSkills)
{
float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier);
CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier,
@@ -578,11 +607,11 @@ namespace Barotrauma.Items.Components
{
if (character == null) { return 1.0f; }
// kind of rough to keep this in update, but seems most robust
if (requiredSkills.Any(s => s != null && s.Identifier == "mechanical"))
if (RequiredSkills.Any(s => s != null && s.Identifier == "mechanical"))
{
return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierMechanical);
}
if (requiredSkills.Any(s => s != null && s.Identifier == "electrical"))
if (RequiredSkills.Any(s => s != null && s.Identifier == "electrical"))
{
return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierElectrical);
}
@@ -11,8 +11,8 @@ namespace Barotrauma.Items.Components
{
private ISpatialEntity source;
private Item target;
private Vector2? launchDir;
private float currentRopeLength;
private void SetSource(ISpatialEntity source)
{
@@ -73,6 +73,13 @@ namespace Barotrauma.Items.Components
get;
set;
}
[Serialize(200.0f, IsPropertySaveable.No, description: "At which distance the user stops pulling the target?")]
public float MinPullDistance
{
get;
set;
}
[Serialize(360.0f, IsPropertySaveable.No, description: "The maximum angle from the source to the target until the rope breaks.")]
public float MaxAngle
@@ -108,7 +115,8 @@ namespace Barotrauma.Items.Components
get;
set;
}
private bool isReelingIn;
private bool snapped;
public bool Snapped
{
@@ -134,6 +142,15 @@ namespace Barotrauma.Items.Components
{
snapTimer = 0;
}
else if (target != null && source != null && target != source)
{
#if CLIENT
// Play a sound at both ends. Initially tested playing the sound in the middle when the rope snaps in the middle,
// but I think it's more important to ensure that the players hear the sound.
PlaySound(snapSound, source.WorldPosition);
PlaySound(snapSound, target.WorldPosition);
#endif
}
}
}
@@ -156,14 +173,17 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition);
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
var user = item.GetComponent<Projectile>()?.User;
UpdateProjSpecific();
isReelingIn = false;
Character user = item.GetComponent<Projectile>()?.User;
if (source == null || target == null || target.Removed ||
(source is Entity sourceEntity && sourceEntity.Removed) ||
(source is Limb limb && limb.Removed) ||
(user != null && user.Removed))
source is Entity { Removed: true } ||
source is Limb { Removed: true } ||
user is null ||
user is { Removed: true })
{
ResetSource();
target = null;
@@ -191,11 +211,8 @@ namespace Barotrauma.Items.Components
if (MaxAngle < 180 && lengthSqr > 2500)
{
if (launchDir == null)
{
launchDir = diff;
}
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(launchDir.Value, diff));
launchDir ??= diff;
float angle = MathHelper.ToDegrees(launchDir.Value.Angle(diff));
if (angle > MaxAngle)
{
Snap();
@@ -262,8 +279,8 @@ namespace Barotrauma.Items.Components
}
Vector2 forceDir = diff;
float distance = diff.Length();
if (distance > 0.001f)
currentRopeLength = diff.Length();
if (currentRopeLength > 0.001f)
{
forceDir = Vector2.Normalize(forceDir);
}
@@ -277,87 +294,162 @@ namespace Barotrauma.Items.Components
{
float targetMass = float.MaxValue;
Character targetCharacter = null;
if (projectile.StickTarget.UserData is Limb targetLimb)
switch (projectile.StickTarget.UserData)
{
targetCharacter = targetLimb.character;
targetMass = targetLimb.ragdoll.Mass;
}
else if (projectile.StickTarget.UserData is Character character)
{
targetCharacter = character;
targetMass = character.Mass;
}
else if (projectile.StickTarget.UserData is Item item)
{
targetMass = projectile.StickTarget.Mass;
case Limb targetLimb:
targetCharacter = targetLimb.character;
targetMass = targetLimb.ragdoll.Mass;
break;
case Character character:
targetCharacter = character;
targetMass = character.Mass;
break;
case Item _:
targetMass = projectile.StickTarget.Mass;
break;
}
if (projectile.StickTarget.BodyType != BodyType.Dynamic)
{
targetMass = float.MaxValue;
}
if (targetMass > TargetMinMass)
if (!snapped)
{
if (Math.Abs(SourcePullForce) > 0.001f)
user.AnimController.HoldToRope();
if (targetCharacter != null)
{
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
targetCharacter.AnimController.DragWithRope();
}
if (user.InWater)
{
user.AnimController.HangWithRope();
}
}
if (Math.Abs(SourcePullForce) > 0.001f && targetMass > TargetMinMass)
{
// This should be the main collider.
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
{
isReelingIn = user.InWater && user.IsRagdolled || !user.InWater && targetCharacter is { IsIncapacitated: false };
if (isReelingIn)
{
if (user != null && user.InWater)
float pullForce = SourcePullForce;
if (!user.InWater)
{
// 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 (user.IsRagdolled)
{
// Reel in towards the target.
user.AnimController.Hang();
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce;
sourceBody.ApplyForce(forceDir * force);
}
// Take the target velocity into account.
if (targetCharacter != null)
{
var myCollider = user.AnimController.Collider;
var targetCollider = targetCharacter.AnimController.Collider;
if (myCollider.LinearVelocity != Vector2.Zero && targetCollider.LinearVelocity != Vector2.Zero)
if (targetBody.LinearVelocity != Vector2.Zero && sourceBody.LinearVelocity != Vector2.Zero)
{
if (Vector2.Dot(Vector2.Normalize(myCollider.LinearVelocity), Vector2.Normalize(targetCollider.LinearVelocity)) < 0)
Vector2 targetDir = Vector2.Normalize(targetBody.LinearVelocity);
float movementDot = Vector2.Dot(Vector2.Normalize(sourceBody.LinearVelocity), targetDir);
if (movementDot < 0)
{
myCollider.ApplyForce(targetCollider.LinearVelocity * targetCollider.Mass);
// 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);
}
}
}
else
{
var targetBody = GetBodyToPull(target);
if (targetBody != null)
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
}
}
}
}
if (Math.Abs(TargetPullForce) > 0.001f)
if (Math.Abs(TargetPullForce) > 0.001f && !user.IsRagdolled)
{
var targetBody = GetBodyToPull(target);
PhysicsBody targetBody = GetBodyToPull(target);
if (targetBody == null) { return; }
bool lerpForces = LerpForces;
if (!lerpForces && user != null && targetCharacter != null && !user.AnimController.InWater)
float maxVelocity = NetConfig.MaxPhysicsBodyVelocity * 0.25f;
// The distance where we start pulling with max force.
float maxPullDistance = MaxLength / 3;
float minPullDistance = MinPullDistance;
const float absoluteMinPullDistance = 50;
if (targetCharacter != null)
{
if ((forceDir.X < 0) != (user.AnimController.Dir < 0))
if (targetCharacter.IsRagdolled || targetCharacter.IsUnconscious)
{
// Prevents rubberbanding horizontally when dragging a corpse.
lerpForces = true;
if (!targetCharacter.InWater)
{
// Limits the velocity of ragdolled characters on ground/air, because otherwise they tend to move with too high forces.
maxVelocity = NetConfig.MaxPhysicsBodyVelocity * 0.075f;
}
}
else
{
// Target alive and kicking -> Use the absolute min pull distance and full forces to pull.
// Keep some lerping, because it results into smoothing when the target is close by.
minPullDistance = absoluteMinPullDistance;
maxPullDistance = 200;
}
}
float force = lerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce;
targetBody?.ApplyForce(-forceDir * force);
var targetRagdoll = targetCharacter?.AnimController;
if (targetRagdoll?.Collider != null && (targetRagdoll.InWater || targetRagdoll.OnGround))
minPullDistance = MathHelper.Max(minPullDistance, absoluteMinPullDistance);
if (currentRopeLength < minPullDistance) { return; }
maxPullDistance = MathHelper.Max(minPullDistance * 2, maxPullDistance);
float force = lerpForces
? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(minPullDistance, maxPullDistance, currentRopeLength))
: TargetPullForce;
targetBody.ApplyForce(-forceDir * force, maxVelocity);
AnimController targetRagdoll = targetCharacter?.AnimController;
if (targetRagdoll?.Collider != null)
{
targetRagdoll.Collider.ApplyForce(-forceDir * force * 3);
isReelingIn = true;
if (targetRagdoll.InWater || targetRagdoll.OnGround)
{
float forceMultiplier = 1;
if (!targetCharacter.IsRagdolled && !targetCharacter.IsIncapacitated)
{
// Pulling the main collider requires higher forces when the target is trying to move away.
Vector2 targetMovement = targetCharacter.AnimController.TargetMovement;
float dot = Vector2.Dot(Vector2.Normalize(targetMovement), forceDir);
if (dot > 0)
{
const float constMultiplier = 2.5f;
float targetVelocity = targetMovement.Length();
float massFactor = Math.Max((float)Math.Log(targetCharacter.Mass / 10), 1);
forceMultiplier = Math.Max(targetVelocity * massFactor * constMultiplier * dot, 1);
}
}
targetRagdoll.Collider.ApplyForce(-forceDir * force * forceMultiplier, maxVelocity);
}
}
}
}
}
partial void UpdateProjSpecific();
public override void UpdateBroken(float deltaTime, Camera cam)
{
@@ -409,32 +501,22 @@ namespace Barotrauma.Items.Components
{
if (target is Item targetItem)
{
if (targetItem.ParentInventory is CharacterInventory characterInventory &&
characterInventory.Owner is Character ownerCharacter)
if (targetItem.ParentInventory is CharacterInventory { Owner: Character ownerCharacter })
{
if (ownerCharacter.Removed) { return null; }
return ownerCharacter.AnimController.Collider;
}
var projectile = targetItem.GetComponent<Projectile>();
if (projectile != null && projectile.StickTarget != null)
if (projectile is { StickTarget: not null })
{
if (projectile.StickTarget.UserData is Structure structure)
return projectile.StickTarget.UserData switch
{
return structure.Submarine?.PhysicsBody;
}
else if (projectile.StickTarget.UserData is Submarine sub)
{
return sub.PhysicsBody;
}
else if (projectile.StickTarget.UserData is Item item)
{
return item.body;
}
else if (projectile.StickTarget.UserData is Limb limb)
{
return limb.body;
}
return null;
Structure structure => structure.Submarine?.PhysicsBody,
Submarine sub => sub.PhysicsBody,
Item item => item.body,
Limb limb => limb.body,
_ => null
};
}
if (targetItem.body != null) { return targetItem.body; }
}
@@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components
public readonly List<CircuitBoxInputOutputNode> InputOutputNodes = new();
public readonly List<CircuitBoxLabelNode> Labels = new();
public readonly List<CircuitBoxWire> Wires = new List<CircuitBoxWire>();
public override bool IsActive => true;
@@ -80,6 +82,9 @@ namespace Barotrauma.Items.Components
public bool IsFull => ComponentContainer?.Inventory is { } inventory && inventory.IsFull(true);
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Locked circuit boxes can only be viewed and not interacted with.")]
public bool Locked { get; set; }
public CircuitBox(Item item, ContentXElement element) : base(item, element)
{
containers = item.GetComponents<ItemContainer>().ToArray();
@@ -176,6 +181,9 @@ namespace Barotrauma.Items.Components
case "outputnode":
LoadFor(CircuitBoxInputOutputNode.Type.Output, subElement);
break;
case "label":
Labels.Add(CircuitBoxLabelNode.LoadFromXML(subElement, this));
break;
}
}
@@ -204,11 +212,14 @@ namespace Barotrauma.Items.Components
{
Components.Clear();
Wires.Clear();
Labels.Clear();
foreach (var origComp in original.Components)
foreach (var label in original.Labels)
{
var newComponent = new CircuitBoxComponent(origComp.ID, clonedContainedItems[origComp.Item.ID], origComp.Position, this, origComp.UsedResource);
Components.Add(newComponent);
var newLabel = new CircuitBoxLabelNode(label.ID, label.Color, label.Position, this);
newLabel.EditText(label.HeaderText, label.BodyText);
newLabel.ApplyResize(label.Size, label.Position);
Labels.Add(newLabel);
}
for (int ioIndex = 0; ioIndex < original.InputOutputNodes.Count; ioIndex++)
@@ -219,10 +230,19 @@ namespace Barotrauma.Items.Components
cloneNode.Position = origNode.Position;
}
if (!clonedContainedItems.Any()) { return; }
foreach (var origComp in original.Components)
{
if (!clonedContainedItems.TryGetValue(origComp.Item.ID, out var clonedItem)) { continue; }
var newComponent = new CircuitBoxComponent(origComp.ID, clonedItem, origComp.Position, this, origComp.UsedResource);
Components.Add(newComponent);
}
foreach (var origWire in original.Wires)
{
Option<CircuitBoxConnection> to = CircuitBoxConnectorIdentifier.FromConnection(origWire.To).FindConnection(this),
from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this);
from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this);
if (!to.TryUnwrap(out var toConn) || !from.TryUnwrap(out var fromConn))
{
@@ -230,7 +250,8 @@ namespace Barotrauma.Items.Components
continue;
}
var newWire = new CircuitBoxWire(this, origWire.ID, origWire.BackingWire.Select(w => clonedContainedItems[w.ID]), fromConn, toConn, origWire.UsedItemPrefab);
var wireItem = origWire.BackingWire.Select(w => clonedContainedItems[w.ID]);
var newWire = new CircuitBoxWire(this, origWire.ID, wireItem, fromConn, toConn, origWire.UsedItemPrefab);
Wires.Add(newWire);
}
}
@@ -254,6 +275,11 @@ namespace Barotrauma.Items.Components
componentElement.Add(wire.Save());
}
foreach (var label in Labels)
{
componentElement.Add(label.Save());
}
return componentElement;
}
@@ -324,6 +350,36 @@ namespace Barotrauma.Items.Components
return true;
}
private void AddLabelInternal(ushort id, Color color, Vector2 pos, NetLimitedString header, NetLimitedString body)
{
var newLabel = new CircuitBoxLabelNode(id, color, pos, this);
newLabel.EditText(header, body);
Labels.Add(newLabel);
OnViewUpdateProjSpecific();
}
private void RemoveLabelInternal(IReadOnlyCollection<ushort> ids)
{
foreach (CircuitBoxLabelNode node in Labels.ToImmutableArray())
{
if (!ids.Contains(node.ID)) { continue; }
Labels.Remove(node);
}
OnViewUpdateProjSpecific();
}
private void ResizeLabelInternal(ushort id, Vector2 pos, Vector2 size)
{
size = Vector2.Max(size, CircuitBoxLabelNode.MinSize);
foreach (CircuitBoxLabelNode node in Labels)
{
if (node.ID != id) { continue; }
node.ApplyResize(size, pos);
break;
}
OnViewUpdateProjSpecific();
}
private static bool IsExternalConnection(CircuitBoxConnection conn) => conn is (CircuitBoxInputConnection or CircuitBoxOutputConnection);
private void CreateWireWithoutItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort id, ItemPrefab prefab)
@@ -380,6 +436,18 @@ namespace Barotrauma.Items.Components
private void AddWireDirect(ushort id, ItemPrefab prefab, Option<Item> backingItem, CircuitBoxConnection one, CircuitBoxConnection two)
=> Wires.Add(new CircuitBoxWire(this, id, backingItem, one, two, prefab));
private void RenameLabelInternal(ushort id, Color color, NetLimitedString header, NetLimitedString body)
{
foreach (CircuitBoxLabelNode node in Labels)
{
if (node.ID != id) { continue; }
node.EditText(header, body);
node.Color = color;
break;
}
}
private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Character? user, Action<Item>? onItemSpawned)
{
if (id is ICircuitBoxIdentifiable.NullComponentID)
@@ -426,6 +494,21 @@ namespace Barotrauma.Items.Components
ClearSelectionFor(characterId, Components);
ClearSelectionFor(characterId, InputOutputNodes);
ClearSelectionFor(characterId, Wires);
ClearSelectionFor(characterId, Labels);
}
private void SelectLabelsInternal(IReadOnlyCollection<ushort> ids, ushort characterId, bool overwrite)
{
if (overwrite) { ClearSelectionFor(characterId, Labels); }
if (!ids.Any()) { return; }
foreach (CircuitBoxLabelNode node in Labels)
{
if (!ids.Contains(node.ID)) { continue; }
node.SetSelected(Option.Some(characterId));
}
}
private void SelectComponentsInternal(IReadOnlyCollection<ushort> ids, ushort characterId, bool overwrite)
@@ -444,7 +527,8 @@ namespace Barotrauma.Items.Components
private void UpdateSelections(ImmutableDictionary<ushort, Option<ushort>> nodeIds,
ImmutableDictionary<ushort, Option<ushort>> wireIds,
ImmutableDictionary<CircuitBoxInputOutputNode.Type, Option<ushort>> inputOutputs)
ImmutableDictionary<CircuitBoxInputOutputNode.Type, Option<ushort>> inputOutputs,
ImmutableDictionary<ushort, Option<ushort>> labels)
{
foreach (var wire in Wires)
{
@@ -474,6 +558,13 @@ namespace Barotrauma.Items.Components
node.SetSelected(selectedBy);
}
foreach (var node in Labels)
{
if (!labels.TryGetValue(node.ID, out var selectedBy)) { continue; }
node.SetSelected(selectedBy);
}
}
private void SelectWiresInternal(IReadOnlyCollection<ushort> ids, ushort characterId, bool overwrite)
@@ -555,6 +646,7 @@ namespace Barotrauma.Items.Components
private void MoveNodesInternal(IReadOnlyCollection<ushort> ids,
IReadOnlyCollection<CircuitBoxInputOutputNode.Type> ios,
IReadOnlyCollection<ushort> labels,
Vector2 moveAmount)
{
IEnumerable<CircuitBoxComponent> nodes = Components.Where(node => ids.Contains(node.ID));
@@ -563,6 +655,11 @@ namespace Barotrauma.Items.Components
node.Position += moveAmount;
}
foreach (var label in Labels.Where(n => labels.Contains(n.ID)))
{
label.Position += moveAmount;
}
foreach (var io in InputOutputNodes)
{
@@ -635,7 +732,7 @@ namespace Barotrauma.Items.Components
}
}
public static ImmutableArray<Item> GetSortedCircuitBoxSortedItemsFromPlayer(Character? character)
public static ImmutableArray<Item> GetSortedCircuitBoxItemsFromPlayer(Character? character)
=> character?.Inventory?.FindAllItems(predicate: CanItemBeAccessed, recursive: true)
.OrderBy(static i => i.Prefab.Identifier == Tags.FPGACircuit)
.ToImmutableArray() ?? ImmutableArray<Item>.Empty;
@@ -651,7 +748,7 @@ namespace Barotrauma.Items.Components
{
if (character is null) { return Option.None; }
return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxSortedItemsFromPlayer(character));
return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxItemsFromPlayer(character));
}
public static Option<Item> GetApplicableResourcePlayerHas(ItemPrefab prefab, ImmutableArray<Item> playerItems)
@@ -292,9 +292,8 @@ namespace Barotrauma.Items.Components
refSub = attachTarget?.Submarine;
}
Vector2 nodePos = refSub == null ?
newConnection.Item.Position :
newConnection.Item.Position - refSub.HiddenSubPosition;
Vector2 nodePos = RoundNode(newConnection.Item.Position);
if (refSub != null) { nodePos -= refSub.HiddenSubPosition; }
if (nodes.Count > 0 && nodes[0] == nodePos) { return; }
if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { return; }
@@ -469,9 +468,7 @@ namespace Barotrauma.Items.Components
Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition;
mouseDiff = mouseDiff.ClampLength(MaxAttachDistance);
return new Vector2(
MathUtils.RoundTowardsClosest(user.Position.X + mouseDiff.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(user.Position.Y + mouseDiff.Y, Submarine.GridSize.Y));
return RoundNode(user.Position + mouseDiff);
}
public override bool Use(float deltaTime, Character character = null)
@@ -662,11 +659,14 @@ namespace Barotrauma.Items.Components
Drawable = sections.Count > 0;
}
private Vector2 RoundNode(Vector2 position)
private static Vector2 RoundNode(Vector2 position)
{
position.X = MathUtils.Round(position.X, Submarine.GridSize.X / 2.0f);
position.Y = MathUtils.Round(position.Y, Submarine.GridSize.Y / 2.0f);
return position;
Vector2 halfGrid = Submarine.GridSize / 2;
position += halfGrid;
position.X = MathUtils.RoundTowardsClosest(position.X, Submarine.GridSize.X / 2.0f);
position.Y = MathUtils.RoundTowardsClosest(position.Y, Submarine.GridSize.Y / 2.0f);
return position - halfGrid;
}
public void SetConnectedDirty()
@@ -143,7 +143,7 @@ namespace Barotrauma.Items.Components
private bool OnCollision(Fixture sender, Fixture other, Contact contact)
{
if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; }
if (LevelTrigger.GetEntity(other) is not Entity entity) { return false; }
if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; }
triggerers.Add(entity);
return true;
@@ -151,7 +151,7 @@ namespace Barotrauma.Items.Components
private void OnSeparation(Fixture sender, Fixture other, Contact contact)
{
if (!(LevelTrigger.GetEntity(other) is Entity entity))
if (LevelTrigger.GetEntity(other) is not Entity entity)
{
return;
}
@@ -233,7 +233,17 @@ namespace Barotrauma.Items.Components
}
else if (triggerer is Character c)
{
ApplyForce(c.AnimController.Collider);
if (c.AnimController.Collider.BodyType == BodyType.Dynamic)
{
if (c.AnimController.Collider.Enabled)
{
ApplyForce(c.AnimController.Collider);
}
foreach (var limb in c.AnimController.Limbs)
{
ApplyForce(limb.body, multiplier: limb.Mass * c.AnimController.Collider.Mass / c.AnimController.Mass);
}
}
}
else if (triggerer is Submarine s)
{
@@ -248,13 +258,13 @@ namespace Barotrauma.Items.Components
item.SendSignal(IsActive ? "1" : "0", "state_out");
}
private void ApplyForce(PhysicsBody body)
private void ApplyForce(PhysicsBody body, float multiplier = 1.0f)
{
Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.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);
Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff) * multiplier;
if (force.LengthSquared() < 0.01f) { return; }
body.ApplyForce(force);
}
@@ -338,6 +338,11 @@ namespace Barotrauma.Items.Components
}
}
/// <summary>
/// How deep down does the item protect from pressure? Determined by status effects.
/// </summary>
public readonly float PressureProtection;
public Wearable(Item item, ContentXElement element) : base(item, element)
{
this.item = item;
@@ -415,6 +420,12 @@ namespace Barotrauma.Items.Components
WearableStatValues.TryAdd(statType, statValue);
}
break;
case "statuseffect":
if (subElement.GetAttributeString("Target", string.Empty).ToLowerInvariant().Contains("character"))
{
PressureProtection = Math.Max(subElement.GetAttributeFloat(nameof(PressureProtection), 0), PressureProtection);
}
break;
}
}
}