Unstable 0.1500.5.0 (almost forgor edition 💀)

This commit is contained in:
Markus Isberg
2021-10-01 23:56:14 +09:00
parent 3043a9a7bc
commit 08bdfc6cea
150 changed files with 5669 additions and 4403 deletions
@@ -79,6 +79,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "Use the hand rotation instead of torso rotation for the item hold angle. Enable this if you want the item just to follow with the arm when not aiming instead of forcing the arm to a hold pose.")]
public bool UseHandRotationForHoldAngle
{
get;
set;
}
[Serialize(false, false, description: "Can the item be attached to walls.")]
public bool Attachable
{
@@ -93,6 +100,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "Can the item only be attached in limited amount? Uses permanent stat values to check for legibility.")]
public bool LimitedAttachable
{
get;
set;
}
[Serialize(false, false, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")]
public bool AttachedByDefault
{
@@ -154,7 +168,8 @@ namespace Barotrauma.Items.Components
BodyType = BodyType.Dynamic,
CollidesWith = Physics.CollisionCharacter,
CollisionCategories = Physics.CollisionItemBlocking,
Enabled = false
Enabled = false,
UserData = "Holdable.Pusher"
};
Pusher.FarseerBody.OnCollision += OnPusherCollision;
Pusher.FarseerBody.FixedRotation = false;
@@ -205,7 +220,6 @@ namespace Barotrauma.Items.Components
}
}
}
characterUsable = element.GetAttributeBool("characterusable", true);
}
@@ -247,6 +261,7 @@ namespace Barotrauma.Items.Components
private void Drop(bool dropConnectedWires, Character dropper)
{
GetRope()?.Snap();
if (dropConnectedWires)
{
DropConnectedWires(dropper);
@@ -558,6 +573,15 @@ namespace Barotrauma.Items.Components
PickKey = InputType.Select;
}
public override void ParseMsg()
{
base.ParseMsg();
if (Attachable)
{
prevMsg = DisplayMsg;
}
}
public override bool Use(float deltaTime, Character character = null)
{
if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); }
@@ -567,6 +591,25 @@ namespace Barotrauma.Items.Components
if (!character.IsKeyDown(InputType.Aim)) { return false; }
if (!CanBeAttached(character)) { return false; }
if (LimitedAttachable)
{
if (character?.Info == null)
{
DebugConsole.AddWarning("Character without CharacterInfo attempting to attach a limited attachable item!");
return false;
}
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int currentlyAttachedCount = Item.ItemList.Count(
i => i.Submarine == item.Submarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier);
if (currentlyAttachedCount >= maxAttachableCount)
{
#if CLIENT
GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
#endif
return false;
}
}
if (GameMain.NetworkMember != null)
{
if (character != Character.Controlled)
@@ -666,6 +709,20 @@ namespace Barotrauma.Items.Components
Update(deltaTime, cam);
}
public Rope GetRope()
{
var rangedWeapon = Item.GetComponent<RangedWeapon>();
if (rangedWeapon != null)
{
var lastProjectile = rangedWeapon.LastProjectile;
if (lastProjectile != null)
{
return lastProjectile.Item.GetComponent<Rope>();
}
}
return null;
}
public override void Update(float deltaTime, Camera cam)
{
if (attachTargetCell != null)
@@ -720,9 +777,18 @@ namespace Barotrauma.Items.Components
scaledHandlePos[1] = handlePos[1] * item.Scale;
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim;
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle);
if (!aim)
{
var rope = GetRope();
if (rope != null && rope.SnapWhenNotAimed)
{
rope.Snap();
}
}
}
else
{
GetRope()?.Snap();
Limb equipLimb = null;
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Headset) || picker.Inventory.IsInLimbSlot(item, InvSlotType.Head))
{
@@ -792,7 +858,7 @@ namespace Barotrauma.Items.Components
attachTargetCell = null;
if (Pusher != null)
{
GameMain.World.Remove(Pusher.FarseerBody);
Pusher.Remove();
Pusher = null;
}
body = null;
@@ -31,7 +31,7 @@ namespace Barotrauma.Items.Components
public void Initialize(CharacterInfo info)
{
if (info == null) return;
if (info == null) { return; }
if (info.Job?.Prefab != null)
{
@@ -42,20 +42,22 @@ namespace Barotrauma.Items.Components
var head = info.Head;
if (info != null && head != null)
{
item.AddTag("gender:" + head.gender.ToString().ToLowerInvariant());
item.AddTag("race:" + head.race.ToString());
item.AddTag("headspriteid:" + info.HeadSpriteId.ToString());
item.AddTag("hairindex:" + head.HairIndex);
item.AddTag("beardindex:" + head.BeardIndex);
item.AddTag("moustacheindex:" + head.MoustacheIndex);
item.AddTag("faceattachmentindex:" + head.FaceAttachmentIndex);
if (head == null) { return; }
if (info.HasGenders) { item.AddTag($"gender:{head.gender.ToString().ToLowerInvariant()}"); }
if (info.HasRaces) { item.AddTag($"race:{head.race}"); }
item.AddTag($"headspriteid:{info.HeadSpriteId}");
item.AddTag($"hairindex:{head.HairIndex}");
item.AddTag($"beardindex:{head.BeardIndex}");
item.AddTag($"moustacheindex:{head.MoustacheIndex}");
item.AddTag($"faceattachmentindex:{head.FaceAttachmentIndex}");
item.AddTag($"haircolor:{head.HairColor.ToStringHex()}");
item.AddTag($"facialhaircolor:{head.FacialHairColor.ToStringHex()}");
item.AddTag($"skincolor:{head.SkinColor.ToStringHex()}");
if (head.SheetIndex != null)
{
item.AddTag("sheetindex:" + head.SheetIndex.Value.X + ";" + head.SheetIndex.Value.Y);
}
if (head.SheetIndex != null)
{
item.AddTag($"sheetindex:{head.SheetIndex.Value.X};{head.SheetIndex.Value.Y}");
}
}
@@ -50,6 +50,16 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize(true, false)]
public bool Swing { get; set; }
[Editable, Serialize("2.0, 0.0", false)]
public Vector2 SwingPos { get; set; }
[Editable, Serialize("3.0, -1.0", false)]
public Vector2 SwingForce { get; set; }
/// <summary>
/// Defines items that boost the weapon functionality, like battery cell for stun batons.
/// </summary>
@@ -67,8 +77,6 @@ namespace Barotrauma.Items.Components
};
}
item.IsShootable = true;
// TODO: should define this in xml if we have melee weapons that don't require aim to use
item.RequireAimToUse = true;
PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true);
}
@@ -82,6 +90,9 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || reloadTimer > 0.0f) { return false; }
#if CLIENT
if (!Item.RequireAimToUse && character.IsPlayer && (GUI.MouseOn != null || character.Inventory.visualSlots.Any(s => s.MouseOn()) || Inventory.DraggingItems.Any())) { return false; }
#endif
if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; }
//don't allow hitting if the character is already hitting with another weapon
@@ -94,7 +105,7 @@ namespace Barotrauma.Items.Components
SetUser(character);
if (hitPos < MathHelper.PiOver4) { return false; }
if (Item.RequireAimToUse && hitPos < MathHelper.PiOver4) { return false; }
ActivateNearbySleepingCharacters();
reloadTimer = reload / (1 + character.GetStatValue(StatTypes.MeleeAttackSpeed));
@@ -105,20 +116,28 @@ namespace Barotrauma.Items.Components
item.body.FarseerBody.IsBullet = true;
item.body.PhysEnabled = true;
if (!character.AnimController.InWater)
if (Swing && !character.AnimController.InWater)
{
foreach (Limb l in character.AnimController.Limbs)
{
if (l.IsSevered) { continue; }
if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; }
if (l.type == LimbType.Head || l.type == LimbType.Torso)
Vector2 force = new Vector2(character.AnimController.Dir * SwingForce.X, SwingForce.Y) * l.Mass;
switch (l.type)
{
l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 7.0f, -4.0f));
case LimbType.Torso:
force *= 2;
break;
case LimbType.Legs:
case LimbType.LeftFoot:
case LimbType.LeftThigh:
case LimbType.LeftLeg:
case LimbType.RightFoot:
case LimbType.RightThigh:
case LimbType.RightLeg:
force = Vector2.Zero;
break;
}
else
{
l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 5.0f, -2.0f));
}
l.body.ApplyLinearImpulse(force);
}
}
@@ -154,9 +173,16 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (!item.body.Enabled) { impactQueue.Clear(); return; }
if (picker == null && !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; }
if (!item.body.Enabled)
{
impactQueue.Clear();
return;
}
if (picker == null && !picker.HeldItems.Contains(item))
{
impactQueue.Clear();
IsActive = false;
}
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
@@ -164,37 +190,47 @@ namespace Barotrauma.Items.Components
}
//in case handling the impact does something to the picker
if (picker == null) { return; }
reloadTimer -= deltaTime;
if (reloadTimer < 0) { reloadTimer = 0; }
if (!picker.IsKeyDown(InputType.Aim) && !hitting) { hitPos = 0.0f; }
if (reloadTimer < 0)
{
reloadTimer = 0;
}
if (!picker.IsKeyDown(InputType.Aim) && !hitting)
{
hitPos = 0.0f;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); }
if (item.body.Dir != picker.AnimController.Dir)
{
item.FlipX(relativeToSub: false);
}
AnimController ac = picker.AnimController;
//TODO: refactor the hitting logic (get rid of the magic numbers, make it possible to use different kinds of animations for different items)
if (!hitting)
{
bool aim = picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
if (aim)
{
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 5f, MathHelper.PiOver4));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, hitPos, holdAngle + hitPos, aimingMelee: true);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
}
else
{
hitPos = 0;
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle);
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
}
}
else
{
// TODO: We might want to make this configurable
hitPos = MathUtils.WrapAnglePi(hitPos - deltaTime * 15f);
ac.HoldItem(deltaTime, item, handlePos, new Vector2(2, 0), Vector2.Zero, false, hitPos, holdAngle + hitPos); // aimPos not used -> zero (new Vector2(-0.3f, 0.2f)), holdPos new Vector2(0.6f, -0.1f)
if (Swing)
{
ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos);
}
else
{
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
}
if (hitPos < -MathHelper.PiOver2)
{
RestoreCollision();
@@ -1,7 +1,6 @@
using Barotrauma.Abilities;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Collision;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
@@ -80,6 +79,9 @@ namespace Barotrauma.Items.Components
}
}
public Projectile LastProjectile { get; private set; }
private float currentChargeTime;
private bool tryingToCharge;
@@ -196,6 +198,7 @@ namespace Barotrauma.Items.Components
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
float spread = GetSpread(character) * Rand.Range(-0.5f, 0.5f);
LastProjectile?.Item.GetComponent<Rope>()?.Snap();
float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.AttackMultiplier);
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier);
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
@@ -206,6 +209,7 @@ namespace Barotrauma.Items.Components
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f));
Item.RemoveContained(projectile.Item);
}
LastProjectile = projectile;
}
LaunchProjSpecific();
@@ -123,18 +123,18 @@ namespace Barotrauma.Items.Components
if (aim)
{
throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos);
}
else
{
throwPos = 0;
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle);
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
}
}
else
{
throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos);
if (throwPos < 0)
{
@@ -169,8 +169,8 @@ namespace Barotrauma.Items.Components
item.body.FarseerBody.IsBullet = true;
midAir = true;
ac.GetLimb(LimbType.Head).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
ac.GetLimb(LimbType.Torso).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
ac.GetLimb(LimbType.Head)?.body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
ac.GetLimb(LimbType.Torso)?.body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
Limb rightHand = ac.GetLimb(LimbType.RightHand);
item.body.AngularVelocity = rightHand.body.AngularVelocity;
@@ -767,7 +767,7 @@ namespace Barotrauma.Items.Components
}
}
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null)
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float applyOnUserFraction = 0.0f)
{
if (statusEffectLists == null) { return; }
@@ -779,7 +779,13 @@ namespace Barotrauma.Items.Components
{
if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; }
if (user != null) { effect.SetUser(user); }
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, false, false, worldPosition);
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.AfflictionMultiplier = applyOnUserFraction;
item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition);
effect.AfflictionMultiplier = 1.0f;
}
reducesCondition |= effect.ReducesItemCondition();
}
//if any of the effects reduce the item's condition, set the user for OnBroken effects as well
@@ -959,7 +965,7 @@ namespace Barotrauma.Items.Components
}
}
public void ParseMsg()
public virtual void ParseMsg()
{
string msg = TextManager.Get(Msg, true);
if (msg != null)
@@ -220,6 +220,12 @@ namespace Barotrauma.Items.Components
if (targetItem == otherItem) { continue; }
if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r.Equals(otherItem.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)))
{
user.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined);
foreach (Character character in Character.GetFriendlyCrew(user))
{
character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined);
}
var geneticMaterial1 = targetItem.GetComponent<GeneticMaterial>();
var geneticMaterial2 = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial1 != null && geneticMaterial2 != null)
@@ -344,22 +344,27 @@ namespace Barotrauma.Items.Components
var tempUser = user;
for (int i = 0; i < (int)fabricationValueItem.Value; i++)
{
float outCondition = fabricatedItem.OutCondition;
if (i < amountFittingContainer)
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition,
onSpawned: (Item spawnedItem) =>
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition,
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
spawnedItem.Quality = quality;
//reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers
spawnedItem.Condition = spawnedItem.MaxCondition * outCondition;
});
}
else
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition,
onSpawned: (Item spawnedItem) =>
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition,
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
spawnedItem.Quality = quality;
//reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers
spawnedItem.Condition = spawnedItem.MaxCondition * outCondition;
});
}
}
@@ -137,6 +137,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "Can the item stick even to deflective targets.")]
public bool StickToDeflective
{
get;
set;
}
[Serialize(false, false, 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
@@ -231,7 +238,10 @@ namespace Barotrauma.Items.Components
{
Item.body.ResetDynamics();
Item.SetTransform(simPosition, rotation);
Attack.DamageMultiplier = damageMultiplier;
if (Attack != null)
{
Attack.DamageMultiplier = damageMultiplier;
}
// Set user for hitscan projectiles to work properly.
User = user;
// Need to set null for non-characterusable items.
@@ -356,6 +366,7 @@ namespace Barotrauma.Items.Components
{
float rotation = item.body.Rotation;
Vector2 simPositon = item.SimPosition;
Vector2 rayStartWorld = item.WorldPosition;
item.Drop(null);
item.body.Enabled = true;
@@ -367,7 +378,6 @@ namespace Barotrauma.Items.Components
Vector2 rayStart = simPositon;
Vector2 rayEnd = rayStart + dir * 500.0f;
Vector2 rayStartWorld = item.WorldPosition;
float worldDist = 1000.0f;
#if CLIENT
worldDist = Screen.Selected?.Cam?.WorldView.Width ?? GameMain.GraphicsWidth;
@@ -579,7 +589,8 @@ namespace Barotrauma.Items.Components
if (!removePending)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
Entity useTarget = lastTarget?.Body.UserData is Limb limb ? limb.character : lastTarget?.Body.UserData as Entity;
ApplyStatusEffects(ActionType.OnActive, deltaTime, useTarget: useTarget, user: _user);
}
if (item.body != null && item.body.FarseerBody.IsBullet)
@@ -624,7 +635,6 @@ namespace Barotrauma.Items.Components
return false;
}
private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact)
{
if (User != null && User.Removed) { User = null; return false; }
@@ -695,7 +705,8 @@ namespace Barotrauma.Items.Components
}
}
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
private Fixture lastTarget;
private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity)
{
@@ -706,6 +717,7 @@ namespace Barotrauma.Items.Components
{
return false;
}
lastTarget = target;
float projectileNewSpeed = 0.5f;
float projectileDeflectedNewSpeed = 0.1f;
@@ -836,14 +848,16 @@ namespace Barotrauma.Items.Components
}
if (attackResult.AppliedDamageModifiers != null &&
attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles))
(attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective))
{
item.body.LinearVelocity *= projectileDeflectedNewSpeed;
}
else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit &&
else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick
//Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f &&
hits.Count() >= MaxTargetsToHit &&
target.Body.Mass > item.body.Mass * 0.5f &&
(DoesStick ||
(StickToCharacters && target.Body.UserData is Limb) ||
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
(StickToStructures && target.Body.UserData is Structure) ||
(StickToItems && target.Body.UserData is Item)))
{
@@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components
private int qualityLevel;
[Serialize(0, false)]
[Serialize(0, true)]
public int QualityLevel
{
get { return qualityLevel; }
@@ -406,7 +406,15 @@ namespace Barotrauma.Items.Components
fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f;
fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed);
item.MaxRepairConditionMultiplier = 1 + CurrentFixer.GetStatValue(StatTypes.MaxRepairConditionMultiplier);
// kind of rough to keep this in update, but seems most robust
if (requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical", StringComparison.OrdinalIgnoreCase)))
{
item.MaxRepairConditionMultiplier = 1 + CurrentFixer.GetStatValue(StatTypes.MaxRepairConditionMultiplierMechanical);
}
if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)))
{
item.MaxRepairConditionMultiplier = 1 + CurrentFixer.GetStatValue(StatTypes.MaxRepairConditionMultiplierElectrical);
}
if (currentFixerAction == FixActions.Repair)
{
@@ -3,7 +3,6 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -54,6 +53,27 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, false, description: "Should the rope snap when the character drops the aim?")]
public bool SnapWhenNotAimed
{
get;
set;
}
[Serialize(30.0f, false, 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
{
get;
set;
}
[Serialize(false, false)]
public bool LerpForces
{
get;
set;
}
private bool snapped;
public bool Snapped
{
@@ -85,6 +105,7 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(XElement element);
public void Snap() => Snapped = true;
public void Attach(ISpatialEntity source, Item target)
{
@@ -118,14 +139,15 @@ namespace Barotrauma.Items.Components
Vector2 diff = target.WorldPosition - source.WorldPosition;
if (diff.LengthSquared() > MaxLength * MaxLength)
{
Snapped = true;
Snap();
return;
}
#if CLIENT
item.ResetCachedVisibleSize();
#endif
var projectile = target.GetComponent<Projectile>();
if (projectile == null) { return; }
if (SnapOnCollision)
{
raycastTimer += deltaTime;
@@ -135,28 +157,24 @@ namespace Barotrauma.Items.Components
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
customPredicate: (Fixture f) =>
{
var projectile = target?.GetComponent<Projectile>();
if (projectile != null)
foreach (Body body in projectile.Hits)
{
foreach (Body body in projectile.Hits)
Submarine alreadyHitSub = null;
if (body.UserData is Structure hitStructure)
{
Submarine alreadyHitSub = null;
if (body.UserData is Structure hitStructure)
{
alreadyHitSub = hitStructure.Submarine;
}
else if (body.UserData is Submarine hitSub)
{
alreadyHitSub = hitSub;
}
if (alreadyHitSub != null)
{
if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; }
if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; }
}
alreadyHitSub = hitStructure.Submarine;
}
else if (body.UserData is Submarine hitSub)
{
alreadyHitSub = hitSub;
}
if (alreadyHitSub != null)
{
if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; }
if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; }
}
}
Submarine targetSub = target?.GetComponent<Projectile>()?.StickTarget?.UserData as Submarine ?? target.Submarine;
Submarine targetSub = projectile.StickTarget?.UserData as Submarine ?? target.Submarine;
if (f.Body?.UserData is MapEntity mapEntity && mapEntity.Submarine != null)
{
@@ -175,7 +193,7 @@ namespace Barotrauma.Items.Components
return true;
}) != null)
{
Snapped = true;
Snap();
return;
}
raycastTimer = 0.0f;
@@ -183,27 +201,107 @@ namespace Barotrauma.Items.Components
}
Vector2 forceDir = diff;
if (forceDir.LengthSquared() > 0.01f)
float distance = diff.Length();
if (distance > 0.001f)
{
forceDir = Vector2.Normalize(forceDir);
}
if (Math.Abs(ProjectilePullForce) > 0.001f)
{
var projectile = target.GetComponent<Projectile>();
projectile?.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce);
projectile.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce);
}
if (Math.Abs(SourcePullForce) > 0.001f)
if (projectile.StickTarget != null)
{
var sourceBody = GetBodyToPull(source);
sourceBody?.ApplyForce(forceDir * SourcePullForce);
}
if (Math.Abs(TargetPullForce) > 0.001f)
{
var targetBody = GetBodyToPull(target);
targetBody?.ApplyForce(-forceDir * TargetPullForce);
float targetMass = float.MaxValue;
Character targetCharacter = null;
if (projectile.StickTarget.UserData is Limb targetLimb)
{
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;
}
if (projectile.StickTarget.BodyType != BodyType.Dynamic)
{
targetMass = float.MaxValue;
}
var user = item.GetComponent<Projectile>()?.User;
if (targetMass > TargetMinMass)
{
if (Math.Abs(SourcePullForce) > 0.001f)
{
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
{
var targetBody = GetBodyToPull(target);
if (targetBody != null && !(targetBody.UserData is Character))
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
float forceMultiplier = 1;
if (user != null)
{
user.AnimController.Hang();
if (user.InWater)
{
if (user.IsRagdolled)
{
forceMultiplier = 0;
}
}
else
{
forceMultiplier = user.IsRagdolled ? 0.1f : 0.4f;
// Prevents too easy smashing to the walls
forceDir.X /= 4;
// Prevents rubberbanding up and down
if (forceDir.Y < 0)
{
forceDir.Y = 0;
}
}
if (targetCharacter != null)
{
var myCollider = user.AnimController.Collider;
var targetCollider = targetCharacter.AnimController.Collider;
if (myCollider.LinearVelocity != Vector2.Zero && targetCollider.LinearVelocity != Vector2.Zero)
{
if (Vector2.Dot(Vector2.Normalize(myCollider.LinearVelocity), Vector2.Normalize(targetCollider.LinearVelocity)) < 0)
{
myCollider.ApplyForce(targetCollider.LinearVelocity * targetCollider.Mass);
}
}
}
}
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) * forceMultiplier : SourcePullForce * forceMultiplier;
sourceBody.ApplyForce(forceDir * force);
}
}
}
if (Math.Abs(TargetPullForce) > 0.001f)
{
var targetBody = GetBodyToPull(target);
if (user != null && targetCharacter != null && !user.AnimController.InWater)
{
// Prevents rubberbanding horizontally when dragging a corpse.
if ((forceDir.X < 0) != (user.AnimController.Dir < 0))
{
forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f);
}
}
float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance)) : TargetPullForce;
targetBody?.ApplyForce(-forceDir * force);
targetCharacter?.AnimController.Collider.ApplyForce(-forceDir * force * 3);
}
}
}
@@ -231,19 +329,23 @@ namespace Barotrauma.Items.Components
return ownerCharacter.AnimController.Collider;
}
var projectile = targetItem.GetComponent<Projectile>();
if (projectile != null)
if (projectile != null && projectile.StickTarget != null)
{
if (projectile.StickTarget?.UserData is Structure structure)
if (projectile.StickTarget.UserData is Structure structure)
{
return structure.Submarine?.PhysicsBody;
}
else if (projectile.StickTarget?.UserData is Submarine sub)
else if (projectile.StickTarget.UserData is Submarine sub)
{
return sub?.PhysicsBody;
return sub.PhysicsBody;
}
else if (projectile.StickTarget?.UserData is Character character)
else if (projectile.StickTarget.UserData is Item item)
{
return character.AnimController.Collider;
return item.body;
}
else if (projectile.StickTarget.UserData is Limb limb)
{
return limb.body;
}
return null;
}
@@ -0,0 +1,90 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Scanner : ItemComponent
{
[Serialize(1.0f, false, description: "How long it takes for the scan to be completed.")]
public float ScanDuration { get; set; }
[Serialize(0.0f, false, description: "How far along the scan is. When the timer goes above ScanDuration, the scan is completed.")]
public float ScanTimer
{
get
{
return scanTimer;
}
set
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Holdable == null) { return; }
bool wasScanCompletedPreviously = IsScanCompleted;
scanTimer = Math.Max(0.0f, value);
if (!wasScanCompletedPreviously && IsScanCompleted)
{
OnScanCompleted?.Invoke(this);
}
#if SERVER
if (wasScanCompletedPreviously != IsScanCompleted || Math.Abs(LastSentScanTimer - scanTimer) > 0.1f)
{
item.CreateServerEvent(this);
LastSentScanTimer = scanTimer;
}
#endif
}
}
[Serialize(1.0f, false, description: "How far the scanner can be from the target for the scan to be successful.")]
public float ScanRadius { get; set; }
[Serialize(true, false, description: "Should the progress bar always be displayed when the item has been attached.")]
public bool AlwaysDisplayProgressBar { get; set; }
private Holdable Holdable { get; set; }
/// <summary>
/// Should the progress bar be displayed. Use when AlwaysDisplayProgressBar is set to false.
/// </summary>
public bool DisplayProgressBar { get; set; } = false;
private bool IsScanCompleted => scanTimer >= ScanDuration;
private float scanTimer;
public Action<Scanner> OnScanStarted, OnScanCompleted;
public Scanner(Item item, XElement element) : base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
if (Holdable != null && Holdable.Attachable && Holdable.Attached)
{
if (ScanTimer <= 0.0f)
{
OnScanStarted?.Invoke(this);
}
ScanTimer += deltaTime;
item.AiTarget?.IncreaseSoundRange(deltaTime, speed: 2.0f);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
}
else
{
ScanTimer = 0.0f;
DisplayProgressBar = false;
}
UpdateProjSpecific();
}
partial void UpdateProjSpecific();
public override void OnItemLoaded()
{
base.OnItemLoaded();
Holdable = item.GetComponent<Holdable>();
if (Holdable == null || !Holdable.Attachable)
{
DebugConsole.ThrowError("Error in initializing a Scanner component: an attachable Holdable component is required on the same item and none was found");
IsActive = false;
}
}
}
}
@@ -0,0 +1,119 @@
using Barotrauma.Extensions;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class ButtonTerminal : ItemComponent
{
[Editable, Serialize(new string[0], true, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)]
public string[] Signals { get; set; }
[Editable, Serialize("", true, 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 ItemContainer Container { get; set; }
private HashSet<ItemPrefab> ActivatingItemPrefabs { get; set; } = new HashSet<ItemPrefab>();
private bool AllowUsingButtons => ActivatingItemPrefabs.None() || Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab));
public ButtonTerminal(Item item, XElement element) : base(item, element)
{
IsActive = true;
RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null);
if (RequiredSignalCount < 1)
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!");
}
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
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.Equals(activatingItem, StringComparison.OrdinalIgnoreCase)))
.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}\"");
}
}
var containers = item.GetComponents<ItemContainer>().ToList();
if (containers.Count != 1)
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": the ButtonTerminal component requires exactly one ItemContainer component!");
return;
}
Container = containers[0];
OnItemLoadedProjSpecific();
}
partial void OnItemLoadedProjSpecific();
private bool SendSignal(int signalIndex, bool isServerMessage = false)
{
if (!isServerMessage && !AllowUsingButtons) { return false; }
string signal = Signals[signalIndex];
string connectionName = $"signal_out{signalIndex + 1}";
item.SendSignal(signal, connectionName);
return true;
}
private void Write(IWriteMessage msg, object[] extraData)
{
if (extraData == null || extraData.Length < 3) { return; }
msg.WriteRangedInteger((int)extraData[2], 0, Signals.Length - 1);
}
}
}
@@ -101,7 +101,7 @@ namespace Barotrauma.Items.Components
foreach (XElement connectionElement in subElement.Elements())
{
string prefabConnectionName = element.GetAttributeString("name", null);
string prefabConnectionName = connectionElement.GetAttributeString("name", null);
if (prefabConnectionName == Name)
{
displayNameTag = connectionElement.GetAttributeString("displayname", "");
@@ -0,0 +1,185 @@
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class TriggerComponent : ItemComponent
{
[Editable, Serialize(0.0f, true, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
public float Force { get; set; }
public PhysicsBody PhysicsBody { get; private set; }
private float Radius { get; set; }
private float RadiusInDisplayUnits { get; set; }
private bool TriggeredOnce { get; set; }
public bool TriggerActive { get; private set; }
private readonly LevelTrigger.TriggererType triggeredBy;
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
private readonly bool triggerOnce;
private readonly List<ISerializableEntity> statusEffectTargets = new List<ISerializableEntity>();
/// <summary>
/// Effects applied to entities inside the trigger
/// </summary>
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
/// <summary>
/// Attacks applied to entities inside the trigger
/// </summary>
private readonly List<Attack> attacks = new List<Attack>();
public TriggerComponent(Item item, XElement element) : base(item, element)
{
string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character");
if (!Enum.TryParse(triggeredByAttribute, out triggeredBy))
{
DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.");
}
triggerOnce = element.GetAttributeBool("triggeronce", false);
string parentDebugName = $"TriggerComponent in {item.Name}";
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
LevelTrigger.LoadStatusEffect(statusEffects, subElement, parentDebugName);
break;
case "attack":
case "damage":
LevelTrigger.LoadAttack(subElement, parentDebugName, triggerOnce, attacks);
break;
}
}
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 = BodyType.Static,
CollidesWith = LevelTrigger.GetCollisionCategories(triggeredBy),
CollisionCategories = Physics.CollisionWall,
UserData = item
};
PhysicsBody.FarseerBody.SetIsSensor(true);
PhysicsBody.FarseerBody.OnCollision += OnCollision;
PhysicsBody.FarseerBody.OnSeparation += OnSeparation;
RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.radius);
}
public override void OnMapLoaded()
{
base.OnMapLoaded();
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
PhysicsBody.Submarine = item.Submarine;
}
private bool OnCollision(Fixture sender, Fixture other, Contact contact)
{
if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; }
if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (true, item.Submarine))) { return false; }
triggerers.Add(entity);
return true;
}
private void OnSeparation(Fixture sender, Fixture other, Contact contact)
{
if (!(LevelTrigger.GetEntity(other) is Entity entity))
{
return;
}
if (entity is Character character && (!character.Enabled || character.Removed) && triggerers.Contains(entity))
{
triggerers.Remove(entity);
return;
}
if (LevelTrigger.CheckContactsForOtherFixtures(PhysicsBody, other, entity))
{
return;
}
triggerers.Remove(entity);
}
public override void Update(float deltaTime, Camera cam)
{
triggerers.RemoveWhere(t => t.Removed);
LevelTrigger.RemoveDistantTriggerers(PhysicsBody, triggerers, item.WorldPosition);
if (triggerOnce)
{
if (TriggeredOnce) { return; }
if (triggerers.Count > 0)
{
TriggeredOnce = true;
IsActive = false;
triggerers.Clear();
}
}
TriggerActive = triggerers.Any();
foreach (Entity triggerer in triggerers)
{
LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets);
if (triggerer is IDamageable damageable)
{
LevelTrigger.ApplyAttacks(attacks, damageable, item.WorldPosition, deltaTime);
}
else if (triggerer is Submarine submarine)
{
LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime);
}
if (Force < 0.01f)
{
// Just ignore very minimal forces
continue;
}
else if (triggerer is Character c)
{
ApplyForce(c.AnimController.Collider);
}
else if (triggerer is Submarine s)
{
ApplyForce(s.SubBody.Body);
}
else if (triggerer is Item i && i.body != null)
{
ApplyForce(i.body);
}
}
}
private void ApplyForce(PhysicsBody body)
{
Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition);
if (diff.LengthSquared() < 0.0001f) { return; }
float distanceFactor = LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits);
if (distanceFactor <= 0.0f) { return; }
Vector2 force = distanceFactor * Force * Vector2.Normalize(diff);
if (force.LengthSquared() < 0.01f) { return; }
body.ApplyForce(force);
}
public override void Move(Vector2 amount)
{
base.Move(amount);
if (PhysicsBody != null)
{
PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
PhysicsBody.Submarine = item.Submarine;
}
}
}
}
@@ -413,19 +413,19 @@ namespace Barotrauma.Items.Components
return i1.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(i2.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes));
});
}
#if CLIENT
equipLimb.UpdateWearableTypesToHide();
#endif
}
character.OnWearablesChanged();
}
public override void Drop(Character dropper)
{
Character previousPicker = picker;
Unequip(picker);
base.Drop(dropper);
previousPicker?.OnWearablesChanged();
picker = null;
IsActive = false;
}