1410 lines
58 KiB
C#
1410 lines
58 KiB
C#
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics;
|
|
using FarseerPhysics.Dynamics.Contacts;
|
|
using FarseerPhysics.Dynamics.Joints;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using Voronoi2;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
partial class Projectile : ItemComponent, IServerSerializable
|
|
{
|
|
private static readonly ImmutableArray<float> spreadPool;
|
|
static Projectile()
|
|
{
|
|
MTRandom random = new MTRandom(0);
|
|
spreadPool = Enumerable.Range(0, byte.MaxValue + 1).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
|
|
}
|
|
|
|
public static byte SpreadCounter { get; private set; }
|
|
|
|
public static void ResetSpreadCounter()
|
|
{
|
|
SpreadCounter = 0;
|
|
}
|
|
|
|
readonly struct HitscanResult
|
|
{
|
|
public readonly Fixture Fixture;
|
|
public readonly Vector2 Point;
|
|
public readonly Vector2 Normal;
|
|
public readonly float Fraction;
|
|
public readonly Submarine Submarine;
|
|
|
|
public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction, Submarine sub)
|
|
{
|
|
Fixture = fixture;
|
|
Point = point;
|
|
Normal = normal;
|
|
Fraction = fraction;
|
|
Submarine = sub;
|
|
}
|
|
}
|
|
|
|
struct Impact
|
|
{
|
|
public Fixture Fixture;
|
|
public Vector2 Normal;
|
|
public Vector2 LinearVelocity;
|
|
|
|
public Impact(Fixture fixture, Vector2 normal, Vector2 velocity)
|
|
{
|
|
Fixture = fixture;
|
|
Normal = normal;
|
|
LinearVelocity = velocity;
|
|
}
|
|
}
|
|
|
|
enum StickTargetType
|
|
{
|
|
Structure,
|
|
Limb,
|
|
Item,
|
|
Submarine,
|
|
LevelWall,
|
|
Unknown
|
|
}
|
|
|
|
public const float WaterDragCoefficient = 0.1f;
|
|
|
|
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
|
|
|
|
private bool removePending;
|
|
|
|
private byte spreadIndex;
|
|
|
|
//continuous collision detection is used while the projectile is moving faster than this
|
|
const float ContinuousCollisionThreshold = 5.0f;
|
|
|
|
private Joint stickJoint;
|
|
private Vector2 jointAxis;
|
|
|
|
public Attack Attack { get; private set; }
|
|
|
|
private Vector2 launchPos;
|
|
public Submarine LaunchSub;
|
|
|
|
private readonly HashSet<Body> hits = new HashSet<Body>();
|
|
|
|
public List<Body> IgnoredBodies;
|
|
|
|
/// <summary>
|
|
/// The item that launched this projectile (if any)
|
|
/// </summary>
|
|
public Item Launcher;
|
|
|
|
private Character stickTargetCharacter;
|
|
|
|
private Character _user;
|
|
public Character User
|
|
{
|
|
get { return _user; }
|
|
set
|
|
{
|
|
_user = value;
|
|
Attack?.SetUser(_user);
|
|
}
|
|
}
|
|
|
|
public Character Attacker { get; set; }
|
|
|
|
public IEnumerable<Body> Hits
|
|
{
|
|
get { return hits; }
|
|
}
|
|
|
|
[Serialize(10.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")]
|
|
public float LaunchImpulse { get; set; }
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.No, description: "The random percentage modifier used to add variance to the launch impulse.")]
|
|
public float ImpulseSpread { get; set; }
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.No, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")]
|
|
|
|
public float LaunchRotation
|
|
{
|
|
get { return MathHelper.ToDegrees(LaunchRotationRadians); }
|
|
set { LaunchRotationRadians = MathHelper.ToRadians(value); }
|
|
}
|
|
|
|
public float LaunchRotationRadians
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "When set to true, the item can stick to any target it hits.")]
|
|
//backwards compatibility, can stick to anything
|
|
public bool DoesStick
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to characters.")]
|
|
public bool StickToCharacters
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to walls.")]
|
|
public bool StickToStructures
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to items.")]
|
|
public bool StickToItems
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to doors. Caution: may cause issues.")]
|
|
public bool StickToDoors
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick even to deflective targets.")]
|
|
public bool StickToDeflective
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "")]
|
|
public bool StickToLightTargets
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "")]
|
|
public bool GoThroughLightTargets
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(-1f, IsPropertySaveable.No, description: $"Minimum mass of targets to stick to when {nameof(StickToLightTargets)} is disabled. Defaults to half of the projectile's mass.")]
|
|
public float LightTargetMassThreshold
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+
|
|
"It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")]
|
|
public bool Hitscan
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(1, IsPropertySaveable.No, description: "How many hitscans should be done when the projectile is launched. "
|
|
+ "Multiple hitscans can be used to simulate weapons that fire multiple projectiles at the same time" +
|
|
" without having to actually use multiple projectile items, for example shotguns.")]
|
|
public int HitScanCount
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(1, IsPropertySaveable.No, description: "How many targets the projectile can hit before it stops.")]
|
|
public int MaxTargetsToHit
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Should the item be deleted when it hits something.")]
|
|
public bool RemoveOnHit
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the launch angle of the projectile (in degrees).")]
|
|
public float Spread
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; projectiles are launched with an equal amount of angle between them. Only applies when firing multiple projectiles.")]
|
|
public bool StaticSpread
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(true, IsPropertySaveable.No)]
|
|
public bool FriendlyFire
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
private float deactivationTimer;
|
|
|
|
[Serialize(0f, IsPropertySaveable.No)]
|
|
public float DeactivationTime
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
private float stickTimer;
|
|
[Serialize(0f, IsPropertySaveable.No)]
|
|
public float StickDuration
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(-1f, IsPropertySaveable.No)]
|
|
public float MaxJointTranslation
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
private float maxJointTranslationInSimUnits = -1;
|
|
|
|
[Serialize(1000.0f, IsPropertySaveable.No)]
|
|
public float JointBreakPoint
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(true, IsPropertySaveable.No)]
|
|
public bool Prismatic
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description:"Enable only if you want to make the projectile ignore collisions with other projectiles when it's shot. Doesn't have any effect, if the item is not set to be damaged by projectiles.")]
|
|
public bool IgnoreProjectilesWhileActive
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public Body StickTarget
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No)]
|
|
public bool DamageDoors
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile hit the user? Should generally be disabled, unless the projectile is for example something like shrapnel launched by a projectile impact.")]
|
|
public bool DamageUser
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public bool IsStuckToTarget => StickTarget != null;
|
|
|
|
private Category originalCollisionCategories;
|
|
private Category originalCollisionTargets;
|
|
|
|
public Projectile(Item item, ContentXElement element)
|
|
: base (item, element)
|
|
{
|
|
IgnoredBodies = new List<Body>();
|
|
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
|
|
Attack = new Attack(subElement, item.Name + ", Projectile", item);
|
|
}
|
|
|
|
if (item.body == null)
|
|
{
|
|
DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!",
|
|
contentPackage: element.ContentPackage);
|
|
return;
|
|
}
|
|
|
|
spreadIndex = SpreadCounter;
|
|
SpreadCounter++;
|
|
|
|
InitProjSpecific(element);
|
|
}
|
|
partial void InitProjSpecific(ContentXElement element);
|
|
|
|
public override void OnItemLoaded()
|
|
{
|
|
if (item.body == null) { return; }
|
|
if (Attack != null && Attack.DamageRange <= 0.0f)
|
|
{
|
|
switch (item.body.BodyShape)
|
|
{
|
|
case PhysicsBody.Shape.Circle:
|
|
Attack.DamageRange = item.body.Radius;
|
|
break;
|
|
case PhysicsBody.Shape.Capsule:
|
|
Attack.DamageRange = item.body.Height / 2 + item.body.Radius;
|
|
break;
|
|
case PhysicsBody.Shape.Rectangle:
|
|
Attack.DamageRange = new Vector2(item.body.Width / 2.0f, item.body.Height / 2.0f).Length();
|
|
break;
|
|
}
|
|
Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange);
|
|
}
|
|
originalCollisionCategories = item.body.CollisionCategories;
|
|
originalCollisionTargets = item.body.CollidesWith;
|
|
}
|
|
|
|
public float GetSpreadFromPool()
|
|
{
|
|
spreadIndex = (byte)MathUtils.PositiveModulo(spreadIndex, spreadPool.Length);
|
|
return spreadPool[spreadIndex];
|
|
}
|
|
|
|
private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
|
|
{
|
|
if (Item.body == null) { return; }
|
|
Item.body.ResetDynamics();
|
|
Item.SetTransform(simPosition, rotation);
|
|
if (Attack != null)
|
|
{
|
|
Attack.DamageMultiplier = damageMultiplier;
|
|
}
|
|
// Set user for hitscan projectiles to work properly.
|
|
User = user;
|
|
// Need to set null for non-characterusable items.
|
|
Use(character: null, launchImpulseModifier);
|
|
// Set user for normal projectiles to work properly.
|
|
User = user;
|
|
if (Item.Removed) { return; }
|
|
launchPos = simPosition;
|
|
LaunchSub = item.Submarine;
|
|
//set the rotation of the projectile again because dropping the projectile in Use
|
|
//resets the rotation and moves it to the position of the parent item
|
|
Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians), findNewHull: false);
|
|
if (DeactivationTime > 0)
|
|
{
|
|
deactivationTimer = DeactivationTime;
|
|
}
|
|
}
|
|
|
|
public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List<Body> ignoredBodies, bool createNetworkEvent, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
|
|
{
|
|
//add the limbs of the shooter to the list of bodies to be ignored
|
|
//so that the player can't shoot himself
|
|
IgnoredBodies = ignoredBodies;
|
|
Vector2 projectilePos = weaponPos;
|
|
//make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel
|
|
if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
|
|
customPredicate: (Fixture f) => { return IgnoredBodies == null || !IgnoredBodies.Contains(f.Body); }) == null)
|
|
{
|
|
//no obstacles -> we can spawn the projectile at the barrel
|
|
projectilePos = spawnPos;
|
|
}
|
|
else if ((weaponPos - spawnPos).LengthSquared() > 0.0001f)
|
|
{
|
|
//spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle
|
|
Vector2 newPos = weaponPos - Vector2.Normalize(spawnPos - projectilePos) * Math.Max(Item.body.GetMaxExtent(), 0.1f);
|
|
if (MathUtils.IsValid(newPos))
|
|
{
|
|
projectilePos = newPos;
|
|
}
|
|
}
|
|
Launch(user, projectilePos, rotation, damageMultiplier, launchImpulseModifier);
|
|
if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
#if SERVER
|
|
launchRot = rotation;
|
|
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(spreadIndex - 1)));
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public bool Use(Character character = null, float launchImpulseModifier = 0f)
|
|
{
|
|
if (character != null && !characterUsable) { return false; }
|
|
if (item.body == null) { return false; }
|
|
//can't launch if already launched
|
|
if (StickTarget != null || IsActive) { return false; }
|
|
|
|
#if SERVER
|
|
var owner = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == User);
|
|
if (owner != null)
|
|
{
|
|
Limb.SetLagCompensatedBodyPositions(owner);
|
|
}
|
|
#endif
|
|
|
|
float initialRotation = item.body.Rotation;
|
|
//if the item is being launched from an inventory, assume it's being fired by a gun that handles setting the rotation correctly
|
|
//but if the item is e.g. being thrown by a character, we need to take the direction into account
|
|
if (item.body.Dir < 0 && item.ParentInventory is not ItemInventory)
|
|
{
|
|
initialRotation -= MathHelper.Pi;
|
|
}
|
|
for (int i = 0; i < HitScanCount; i++)
|
|
{
|
|
float launchAngle;
|
|
if (StaticSpread)
|
|
{
|
|
launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread;
|
|
}
|
|
else
|
|
{
|
|
launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool());
|
|
}
|
|
spreadIndex++;
|
|
|
|
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
|
|
Vector2 prevSimpos = item.SimPosition;
|
|
item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle);
|
|
if (Hitscan)
|
|
{
|
|
DoHitscan(launchDir);
|
|
if (i < HitScanCount - 1)
|
|
{
|
|
item.SetTransform(prevSimpos, item.body.Rotation);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
|
|
DoLaunch(launchDir * modifiedLaunchImpulse);
|
|
//needs to be set after DoLaunch, because dropping the item resets the rotation and dir
|
|
float afterLaunchAngle = launchAngle + (item.body.Dir * LaunchRotationRadians);
|
|
if (item.body.Dir < 0)
|
|
{
|
|
afterLaunchAngle -= MathHelper.Pi;
|
|
}
|
|
item.SetTransform(item.body.SimPosition, afterLaunchAngle, findNewHull: false);
|
|
}
|
|
}
|
|
User = character;
|
|
ApplyStatusEffects(ActionType.OnUse, 1.0f, User, user: User);
|
|
return true;
|
|
}
|
|
|
|
public override bool Use(float deltaTime, Character character = null) => Use(character);
|
|
|
|
private void DoLaunch(Vector2 impulse)
|
|
{
|
|
hits.Clear();
|
|
|
|
Vector2 prevVelocity = item.body.LinearVelocity;
|
|
|
|
if (item.AiTarget != null)
|
|
{
|
|
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
|
|
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
|
|
}
|
|
|
|
//do not create a network event about dropping the projectile at this point,
|
|
//otherwise clients would fail to launch the correct projectile when firing the weapon
|
|
var prevInventory = item.ParentInventory;
|
|
if (prevInventory != null && GameMain.NetworkMember is { IsServer: true })
|
|
{
|
|
//update the state of the inventory after a delay,
|
|
//in case a client failed to launch the projectile for some reason
|
|
CoroutineManager.Invoke(() =>
|
|
{
|
|
if (item.Removed) { return; }
|
|
prevInventory.CreateNetworkEvent();
|
|
}, delay: CorrectionDelay / 2);
|
|
}
|
|
item.Drop(null, createNetworkEvent: false);
|
|
|
|
Item.WaterDragCoefficient = WaterDragCoefficient;
|
|
|
|
launchPos = item.SimPosition;
|
|
LaunchSub = item.Submarine;
|
|
|
|
item.body.Enabled = true;
|
|
if (item.body.BodyType == BodyType.Kinematic)
|
|
{
|
|
item.body.LinearVelocity = impulse;
|
|
}
|
|
else if (impulse.LengthSquared() > 0.001f)
|
|
{
|
|
impulse *= item.body.Mass;
|
|
item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f);
|
|
}
|
|
else
|
|
{
|
|
//if no impulse is defined, maintain the projectile's original velocity (if any)
|
|
//can be used to make throwable items behave as projectiles
|
|
item.body.LinearVelocity = prevVelocity;
|
|
}
|
|
|
|
item.body.FarseerBody.OnCollision += OnProjectileCollision;
|
|
item.body.FarseerBody.IsBullet = true;
|
|
|
|
EnableProjectileCollisions();
|
|
|
|
IsActive = true;
|
|
|
|
if (stickJoint == null) { return; }
|
|
|
|
StickTarget = null;
|
|
GameMain.World.Remove(stickJoint);
|
|
stickJoint = null;
|
|
}
|
|
|
|
private void DoHitscan(Vector2 dir)
|
|
{
|
|
float rotation = item.body.Rotation;
|
|
Vector2 simPositon = item.SimPosition;
|
|
Vector2 rayStartWorld = item.WorldPosition;
|
|
item.Drop(null, createNetworkEvent: false);
|
|
Item.WaterDragCoefficient = WaterDragCoefficient;
|
|
|
|
item.body.Enabled = true;
|
|
//set the velocity of the body because the OnProjectileCollision method
|
|
//uses it to determine the direction from which the projectile hit
|
|
item.body.LinearVelocity = dir;
|
|
IsActive = true;
|
|
|
|
Vector2 rayStart = simPositon;
|
|
Vector2 rayEnd = rayStart + dir * 500.0f;
|
|
|
|
float worldDist = 1000.0f;
|
|
#if CLIENT
|
|
worldDist = Screen.Selected?.Cam?.WorldView.Width ?? GameMain.GraphicsWidth;
|
|
#endif
|
|
Vector2 rayEndWorld = rayStartWorld + dir * worldDist;
|
|
|
|
List<HitscanResult> hits = new List<HitscanResult>();
|
|
hits.AddRange(DoRayCast(rayStart, rayEnd, submarine: item.Submarine));
|
|
|
|
if (item.Submarine != null)
|
|
{
|
|
//shooting indoors, do a hitscan outside as well
|
|
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition, submarine: null));
|
|
//do a hitscan in other subs' coordinate spaces
|
|
RayCastInOtherSubs(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition);
|
|
}
|
|
else
|
|
{
|
|
RayCastInOtherSubs(rayStart, rayEnd);
|
|
}
|
|
|
|
void RayCastInOtherSubs(Vector2 rayStart, Vector2 rayEnd)
|
|
{
|
|
//shooting outdoors, see if we can hit anything inside a sub
|
|
foreach (Submarine submarine in Submarine.Loaded)
|
|
{
|
|
if (submarine == item.Submarine) { continue; }
|
|
var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition, submarine);
|
|
//transform back to world coordinates
|
|
for (int i = 0; i < inSubHits.Count; i++)
|
|
{
|
|
inSubHits[i] = new HitscanResult(
|
|
inSubHits[i].Fixture,
|
|
inSubHits[i].Point + submarine.SimPosition,
|
|
inSubHits[i].Normal,
|
|
inSubHits[i].Fraction,
|
|
sub: null);
|
|
}
|
|
hits.AddRange(inSubHits);
|
|
}
|
|
}
|
|
|
|
int hitCount = 0;
|
|
Vector2 lastHitPos = item.WorldPosition;
|
|
hits = hits.OrderBy(h => h.Fraction).ToList();
|
|
for (int i = 0; i < hits.Count; i++)
|
|
{
|
|
var h = hits[i];
|
|
item.SetTransform(h.Point, rotation);
|
|
item.Submarine = h.Submarine;
|
|
item.UpdateTransform();
|
|
if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
|
|
{
|
|
hitCount++;
|
|
if (hitCount >= MaxTargetsToHit || i == hits.Count - 1)
|
|
{
|
|
LaunchProjSpecific(rayStartWorld, item.WorldPosition);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
//the raycast didn't hit anything (or didn't hit enough targets to stop the projectile) -> the projectile flew somewhere outside the level and is permanently lost
|
|
if (hitCount < MaxTargetsToHit)
|
|
{
|
|
item.body.SetTransformIgnoreContacts(item.body.SimPosition, rotation);
|
|
LaunchProjSpecific(rayStartWorld, rayEndWorld);
|
|
if (Entity.Spawner == null)
|
|
{
|
|
item.Remove();
|
|
}
|
|
else
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
|
{
|
|
//clients aren't allowed to remove items by themselves, so lets hide the projectile until the server tells us to remove it
|
|
item.HiddenInGame = Hitscan;
|
|
}
|
|
else
|
|
{
|
|
Entity.Spawner.AddItemToRemoveQueue(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<HitscanResult> DoRayCast(Vector2 rayStart, Vector2 rayEnd, Submarine submarine)
|
|
{
|
|
List<HitscanResult> hits = new List<HitscanResult>();
|
|
|
|
Vector2 dir = rayEnd - rayStart;
|
|
dir = dir.LengthSquared() < 0.00001f ? Vector2.UnitY : Vector2.Normalize(dir);
|
|
|
|
//do an AABB query first to see if the start of the ray is inside a fixture
|
|
var aabb = new FarseerPhysics.Collision.AABB(rayStart - Vector2.One * 0.001f, rayStart + Vector2.One * 0.001f);
|
|
GameMain.World.QueryAABB((fixture) =>
|
|
{
|
|
if (fixture?.Body.UserData is LevelObject levelObj)
|
|
{
|
|
if (!levelObj.Prefab.TakeLevelWallDamage) { return true; }
|
|
}
|
|
else if (fixture?.Body == null || fixture.IsSensor)
|
|
{
|
|
//ignore sensors
|
|
return true;
|
|
}
|
|
if (fixture.Body.UserData is VineTile) { return true; }
|
|
if (fixture.CollidesWith == Category.None) { return true; }
|
|
|
|
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; }
|
|
|
|
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
|
|
if (submarine != null)
|
|
{
|
|
if (fixture.Body.UserData is VoronoiCell) { return true; }
|
|
if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return true; }
|
|
}
|
|
|
|
if (fixture.Body.UserData is VoronoiCell && (this.item.Submarine != null || submarine != null)) { return true; }
|
|
|
|
if (fixture.Body.UserData is Item item)
|
|
{
|
|
if (item == Item) { return true; }
|
|
if (item.Condition <= 0) { return true; }
|
|
if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return true; }
|
|
}
|
|
else if (fixture.Body.UserData is Gap)
|
|
{
|
|
//an "outsideCollisionBlocker" created by a gap, should never collide
|
|
return true;
|
|
}
|
|
else if (fixture.Body.UserData is Holdable { CanPush: false })
|
|
{
|
|
// Ignore holdables that can't push -> shouldn't block
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// TODO: This might make us ignore something we don't want to ignore?
|
|
// Not item -> ignore everything else than characters, sub walls and level walls
|
|
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) &&
|
|
!fixture.CollisionCategories.HasFlag(Physics.CollisionWall) &&
|
|
!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return true; }
|
|
}
|
|
|
|
fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
|
|
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
|
|
|
|
hits.Add(new HitscanResult(fixture, rayStart, -dir, 0.0f, submarine));
|
|
return true;
|
|
}, ref aabb);
|
|
|
|
GameMain.World.RayCast((fixture, point, normal, fraction) =>
|
|
{
|
|
//ignore sensors and items
|
|
if (fixture?.Body.UserData is LevelObject levelObj)
|
|
{
|
|
if (!levelObj.Prefab.TakeLevelWallDamage) { return -1; }
|
|
}
|
|
else if (fixture?.Body == null || fixture.IsSensor)
|
|
{
|
|
//ignore sensors
|
|
return -1;
|
|
}
|
|
if (fixture.Body.UserData is VineTile) { return -1; }
|
|
if (fixture.CollidesWith == Category.None && fixture.CollisionCategories != Physics.CollisionLagCompensationBody) { return -1; }
|
|
if (fixture.Body.UserData is Item item)
|
|
{
|
|
if (item.Condition <= 0) { return -1; }
|
|
if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return -1; }
|
|
}
|
|
else if (fixture.Body.UserData is Gap)
|
|
{
|
|
//an "outsideCollisionBlocker" created by a gap, should never collide
|
|
return -1;
|
|
}
|
|
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; }
|
|
|
|
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
|
|
if (submarine != null)
|
|
{
|
|
if (fixture.Body.UserData is VoronoiCell) { return -1; }
|
|
if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; }
|
|
if (fixture.Body.UserData is Limb limb && limb.character?.Submarine != submarine) { return -1; }
|
|
if (fixture.Body == Level.Loaded?.TopBarrier || fixture.Body == Level.Loaded?.BottomBarrier) { return -1; }
|
|
}
|
|
|
|
// Ignore holdables that can't push -> shouldn't block
|
|
if (fixture.Body.UserData is Holdable { CanPush: false })
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
//ignore level cells if the item and the point of impact are inside a sub
|
|
if (fixture.Body.UserData is VoronoiCell)
|
|
{
|
|
if (Hull.FindHull(ConvertUnits.ToDisplayUnits(point), this.item.CurrentHull) != null && this.item.Submarine != null)
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (hits.Count > 50)
|
|
{
|
|
float furthestHit = 0.0f;
|
|
int furthestHitIndex = -1;
|
|
for (int i = 0; i < hits.Count; i++)
|
|
{
|
|
if (hits[i].Fraction > furthestHit)
|
|
{
|
|
furthestHitIndex = i;
|
|
furthestHit = hits[i].Fraction;
|
|
}
|
|
}
|
|
if (furthestHitIndex > -1)
|
|
{
|
|
hits.RemoveAt(furthestHitIndex);
|
|
}
|
|
}
|
|
|
|
hits.Add(new HitscanResult(fixture, point, normal, fraction, submarine));
|
|
|
|
return 1;
|
|
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile | Physics.CollisionLagCompensationBody);
|
|
|
|
return hits;
|
|
}
|
|
|
|
public override void Drop(Character dropper, bool setTransform = true)
|
|
{
|
|
Item.ResetWaterDragCoefficient();
|
|
if (dropper != null)
|
|
{
|
|
DisableProjectileCollisions();
|
|
Unstick();
|
|
}
|
|
base.Drop(dropper, setTransform);
|
|
}
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
if (DeactivationTime > 0)
|
|
{
|
|
deactivationTimer -= deltaTime;
|
|
if (deactivationTimer < 0)
|
|
{
|
|
DisableProjectileCollisions();
|
|
}
|
|
}
|
|
while (impactQueue.Count > 0)
|
|
{
|
|
var impact = impactQueue.Dequeue();
|
|
HandleProjectileCollision(impact.Fixture, impact.Normal, impact.LinearVelocity);
|
|
}
|
|
|
|
if (!removePending)
|
|
{
|
|
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)
|
|
{
|
|
if (item.body.LinearVelocity.LengthSquared() < ContinuousCollisionThreshold * ContinuousCollisionThreshold)
|
|
{
|
|
item.body.FarseerBody.IsBullet = false;
|
|
}
|
|
}
|
|
//projectiles with a stickjoint don't become inactive until the stickjoint is detached
|
|
if (stickJoint == null && !item.body.FarseerBody.IsBullet)
|
|
{
|
|
IsActive = false;
|
|
if (DeactivationTime > 0 && deactivationTimer > 0)
|
|
{
|
|
DisableProjectileCollisions();
|
|
}
|
|
}
|
|
|
|
if (stickJoint == null) { return; }
|
|
|
|
if (StickDuration > 0 && stickTimer > 0)
|
|
{
|
|
stickTimer -= deltaTime;
|
|
return;
|
|
}
|
|
|
|
float absoluteMaxTranslation = 100;
|
|
// Update the item's transform to make sure it's inside the same sub as the target (or outside)
|
|
if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || stickJoint is PrismaticJoint prismaticJoint && Math.Abs(prismaticJoint.JointTranslation) > absoluteMaxTranslation)
|
|
{
|
|
item.UpdateTransform();
|
|
}
|
|
|
|
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
|
{
|
|
if (StickTargetRemoved() || stickJoint is PrismaticJoint pJoint && Math.Abs(pJoint.JointTranslation) > maxJointTranslationInSimUnits || !stickJoint.Enabled)
|
|
{
|
|
Unstick();
|
|
#if SERVER
|
|
item.CreateServerEvent(this, new EventData(launch: false));
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool StickTargetRemoved()
|
|
{
|
|
if (StickTarget == null) { return true; }
|
|
if (StickTarget.UserData is Limb limb) { return limb.character.Removed; }
|
|
if (StickTarget.UserData is Entity entity) { return entity.Removed; }
|
|
return false;
|
|
}
|
|
|
|
private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact)
|
|
{
|
|
if (User != null && User.Removed) { User = null; return false; }
|
|
if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; }
|
|
if (originalCollisionCategories == Category.None && originalCollisionTargets == Category.None) { return false; }
|
|
//ignore character colliders (the projectile only hits limbs)
|
|
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
|
|
{
|
|
return false;
|
|
}
|
|
if (GoThroughLightTargets && target.Body.Mass < GetLightTargetMassThreshold()) { return false; }
|
|
if (target.IsSensor) { return false; }
|
|
if (hits.Contains(target.Body)) { return false; }
|
|
if (target.Body.UserData is Submarine)
|
|
{
|
|
if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; }
|
|
}
|
|
else if (target.Body.UserData is Limb limb)
|
|
{
|
|
if (limb.IsSevered)
|
|
{
|
|
//push the severed limb around a bit, but let the projectile pass through it
|
|
limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition);
|
|
return false;
|
|
}
|
|
if (ShouldIgnoreCharacterCollision(limb.character)) { return false; }
|
|
}
|
|
else if (target.Body.UserData is Item item)
|
|
{
|
|
if (item.Condition <= 0.0f) { return false; }
|
|
if (!item.Prefab.DamagedByProjectiles)
|
|
{
|
|
if (item.GetComponent<Door>() == null)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else if (target.Body.UserData is Holdable { CanPush: false })
|
|
{
|
|
// Ignore holdables that can't push -> shouldn't block
|
|
return false;
|
|
}
|
|
|
|
//ignore character colliders (the projectile only hits limbs)
|
|
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
hits.Add(target.Body);
|
|
impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal, item.body.LinearVelocity));
|
|
IsActive = true;
|
|
if (RemoveOnHit)
|
|
{
|
|
item.body.FarseerBody.ResetDynamics();
|
|
}
|
|
if (hits.Count >= MaxTargetsToHit || target.Body.UserData is VoronoiCell)
|
|
{
|
|
DisableProjectileCollisions();
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool ShouldIgnoreCharacterCollision(Character character)
|
|
{
|
|
//don't hit characters "attached" to the projectile (e.g. inside a boarding pod)
|
|
if (item.GetComponent<Controller>() is { } controller && controller.User == character && controller.IsAttachedUser(controller.User))
|
|
{
|
|
return true;
|
|
}
|
|
if (!FriendlyFire && User != null && character.IsFriendly(User))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Should the collision with the target submarine be ignored (e.g. did the projectile collide with the wall behind the turret when being launched)
|
|
/// </summary>
|
|
/// <param name="target">Fixture the projectile hit</param>
|
|
/// <param name="contact">Contact between the projectile and the target</param>
|
|
/// <returns>True if the target isn't a submarine or if the collision happened behind the launch position of the projectile</returns>
|
|
public bool ShouldIgnoreSubmarineCollision(Fixture target, Contact contact)
|
|
{
|
|
return ShouldIgnoreSubmarineCollision(ref target, contact);
|
|
}
|
|
|
|
private bool ShouldIgnoreSubmarineCollision(ref Fixture target, Contact contact)
|
|
{
|
|
//not in the projectile category: the projectile has not been launched (e.g. just dropped from an inventory)
|
|
if (item.body.CollisionCategories != Physics.CollisionProjectile)
|
|
{
|
|
return false;
|
|
}
|
|
if (target.Body.UserData is Submarine sub)
|
|
{
|
|
//hit an item in a different sub -> no need to ignore, we can process the impact with this info
|
|
//(if it wasn't, we'll move the projectile to that sub's coordinate space and let it hit what it hits there)
|
|
if (Launcher?.Submarine != sub && target.UserData is Item)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Vector2 normalizedVel;
|
|
Vector2 dir;
|
|
if (item.body.LinearVelocity.LengthSquared() < 0.001f)
|
|
{
|
|
normalizedVel = Vector2.Zero;
|
|
dir = contact.Manifold.LocalNormal;
|
|
}
|
|
else
|
|
{
|
|
normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity);
|
|
}
|
|
|
|
//do a raycast in the sub's coordinate space to see if it hit a structure
|
|
var wallBody = Submarine.PickBody(
|
|
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir,
|
|
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir,
|
|
collisionCategory: Physics.CollisionWall,
|
|
customPredicate: (Fixture f) => IgnoredBodies == null || !IgnoredBodies.Contains(f.Body));
|
|
|
|
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) - launchPosInCurrentCoordinateSpace, dir) > 0)
|
|
{
|
|
target = wallBody.FixtureList.First();
|
|
if (hits.Contains(target.Body))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
|
private Fixture lastTarget;
|
|
|
|
private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity)
|
|
{
|
|
if (User != null && User.Removed) { User = null; }
|
|
if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; }
|
|
//ignore character colliders (the projectile only hits limbs)
|
|
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
|
|
{
|
|
return false;
|
|
}
|
|
lastTarget = target;
|
|
|
|
int remainingHits = Math.Max(MaxTargetsToHit - hits.Count, 0);
|
|
float speedMultiplier = Math.Min(0.4f + remainingHits * 0.1f, 1.0f);
|
|
float deflectedSpeedMultiplier = 0.1f;
|
|
|
|
AttackResult attackResult = new AttackResult();
|
|
Character character = null;
|
|
if (target.Body.UserData is Submarine submarine && target.UserData is not Barotrauma.Item)
|
|
{
|
|
item.Move(-submarine.Position, ignoreContacts: false);
|
|
item.Submarine = submarine;
|
|
item.body.Submarine = submarine;
|
|
return !Hitscan;
|
|
}
|
|
else if (target.Body.UserData is Limb limb)
|
|
{
|
|
// when hitting limbs with piercing ammo, don't lose as much speed
|
|
if (MaxTargetsToHit > 1)
|
|
{
|
|
speedMultiplier = 1f;
|
|
deflectedSpeedMultiplier = 0.8f;
|
|
}
|
|
if (limb.IsSevered || limb.character == null || limb.character.Removed) { return false; }
|
|
if (ShouldIgnoreCharacterCollision(limb.character)) { return false; }
|
|
|
|
limb.character.LastDamageSource = item;
|
|
if (Attack != null) { attackResult = Attack.DoDamageToLimb(User ?? Attacker, limb, item.WorldPosition, 1.0f); }
|
|
if (limb.character != null) { character = limb.character; }
|
|
}
|
|
else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item ?? target.UserData as Item) is Item targetItem)
|
|
{
|
|
if (targetItem.Removed) { return false; }
|
|
//hit the external collider of an item (turret?) of the same sub -> ignore
|
|
if (target.UserData is Item && targetItem.Submarine != null && targetItem.Submarine == Launcher?.Submarine) { return false; }
|
|
if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent<Door>() != null) && targetItem.Condition > 0)
|
|
{
|
|
attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f);
|
|
#if CLIENT
|
|
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
|
|
(User == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
|
|
{
|
|
Character.Controlled.UpdateHUDProgressBar(targetItem,
|
|
targetItem.WorldPosition,
|
|
targetItem.Condition / targetItem.MaxCondition,
|
|
emptyColor: GUIStyle.HealthBarColorLow,
|
|
fullColor: GUIStyle.HealthBarColorHigh,
|
|
textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
else if (target.Body.UserData is IDamageable damageable)
|
|
{
|
|
if (Attack != null)
|
|
{
|
|
if (item.Submarine == null && damageable is Structure structure && structure.Submarine != null && Vector2.DistanceSquared(item.WorldPosition, structure.WorldPosition) > 10000.0f * 10000.0f)
|
|
{
|
|
item.Submarine = structure.Submarine;
|
|
}
|
|
Vector2 pos = item.WorldPosition;
|
|
attackResult = Attack.DoDamage(User ?? Attacker, damageable, pos, 1.0f);
|
|
}
|
|
}
|
|
else if (target.Body.UserData is VoronoiCell voronoiCell && voronoiCell.IsDestructible && Attack != null && Math.Abs(Attack.LevelWallDamage) > 0.0f)
|
|
{
|
|
if (Level.Loaded?.ExtraWalls.Find(w => w.Body == target.Body) is DestructibleLevelWall destructibleWall)
|
|
{
|
|
attackResult = Attack.DoDamage(User ?? Attacker, destructibleWall, item.WorldPosition, 1.0f);
|
|
}
|
|
}
|
|
|
|
if (character != null) { character.LastDamageSource = item; }
|
|
|
|
ActionType conditionalActionType = ActionType.OnSuccess;
|
|
if (User != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(User))
|
|
{
|
|
conditionalActionType = ActionType.OnFailure;
|
|
}
|
|
#if CLIENT
|
|
PlaySound(conditionalActionType, user: User);
|
|
PlaySound(ActionType.OnImpact, user: User);
|
|
#endif
|
|
|
|
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
|
{
|
|
if (target.Body.UserData is Limb targetLimb)
|
|
{
|
|
ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: User);
|
|
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, useTarget: character, user: User);
|
|
var attack = targetLimb.attack;
|
|
if (attack != null)
|
|
{
|
|
// Apply the status effects defined in the limb's attack that was hit
|
|
foreach (var effect in attack.StatusEffects)
|
|
{
|
|
if (effect.type == ActionType.OnImpact)
|
|
{
|
|
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
|
{
|
|
effect.Apply(effect.type, 1.0f, User, User);
|
|
}
|
|
if (effect.HasTargetType(StatusEffect.TargetType.Character) || effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
|
{
|
|
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character);
|
|
}
|
|
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
|
{
|
|
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb);
|
|
}
|
|
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
|
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
|
{
|
|
targets.Clear();
|
|
effect.AddNearbyTargets(targetLimb.WorldPosition, targets);
|
|
effect.Apply(effect.type, 1.0f, targetLimb.character, targets);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (GameMain.NetworkMember is { IsServer: true } server)
|
|
{
|
|
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition));
|
|
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ApplyStatusEffects(conditionalActionType, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
|
|
ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
|
|
if (GameMain.NetworkMember is { IsServer: true } server)
|
|
{
|
|
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition));
|
|
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition));
|
|
}
|
|
}
|
|
}
|
|
|
|
target.Body.ApplyLinearImpulse(velocity * item.body.Mass);
|
|
target.Body.LinearVelocity = target.Body.LinearVelocity.ClampLength(NetConfig.MaxPhysicsBodyVelocity * 0.5f);
|
|
|
|
if (hits.Count >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell)
|
|
{
|
|
DisableProjectileCollisions();
|
|
}
|
|
|
|
if (attackResult.AppliedDamageModifiers != null && attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)
|
|
{
|
|
item.body.LinearVelocity *= deflectedSpeedMultiplier;
|
|
}
|
|
else if ( remainingHits <= 0 &&
|
|
stickJoint == null && StickTarget == null &&
|
|
StickToStructures && target.Body.UserData is Structure ||
|
|
((StickToLightTargets || target.Body.Mass >= GetLightTargetMassThreshold()) &&
|
|
(DoesStick ||
|
|
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
|
|
(target.Body.UserData is Item i && (i.GetComponent<Door>() != null ? StickToDoors : StickToItems)))))
|
|
{
|
|
Vector2 dir = new Vector2(
|
|
(float)Math.Cos(item.body.Rotation),
|
|
(float)Math.Sin(item.body.Rotation));
|
|
|
|
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
|
{
|
|
if (target.Body.UserData is Structure structure && structure.Submarine != item.Submarine && structure.Submarine != null)
|
|
{
|
|
StickToTarget(structure.Submarine.PhysicsBody.FarseerBody, dir);
|
|
}
|
|
else
|
|
{
|
|
StickToTarget(target.Body, dir);
|
|
}
|
|
}
|
|
#if SERVER
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
item.CreateServerEvent(this, new EventData(launch: false));
|
|
}
|
|
#endif
|
|
item.body.LinearVelocity *= speedMultiplier;
|
|
|
|
return Hitscan;
|
|
}
|
|
else
|
|
{
|
|
item.body.LinearVelocity *= speedMultiplier;
|
|
}
|
|
|
|
var containedItems = item.OwnInventory?.AllItems;
|
|
if (containedItems != null)
|
|
{
|
|
foreach (Item contained in containedItems)
|
|
{
|
|
if (contained.body != null)
|
|
{
|
|
contained.SetTransform(item.SimPosition, contained.body.Rotation);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (RemoveOnHit)
|
|
{
|
|
removePending = true;
|
|
item.HiddenInGame = true;
|
|
item.body.FarseerBody.Enabled = false;
|
|
//delete with a brief delay so we don't delete the item
|
|
//before a client has managed to launch it (can happen with hitscan projectile)
|
|
CoroutineManager.Invoke(() =>
|
|
{
|
|
if (item.Removed) { return; }
|
|
Entity.Spawner?.AddItemToRemoveQueue(item);
|
|
}, delay: 0.5f);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private float GetLightTargetMassThreshold()
|
|
{
|
|
return LightTargetMassThreshold < 0 ? item.body.Mass * 0.5f : LightTargetMassThreshold;
|
|
}
|
|
|
|
private void EnableProjectileCollisions()
|
|
{
|
|
if (item.body.CollisionCategories != Category.None)
|
|
{
|
|
item.body.CollisionCategories = Physics.CollisionProjectile;
|
|
item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking;
|
|
}
|
|
if (item.Prefab.DamagedByProjectiles && !IgnoreProjectilesWhileActive)
|
|
{
|
|
if (item.body.CollisionCategories == Category.None) { item.body.CollisionCategories = Physics.CollisionCharacter; }
|
|
item.body.CollidesWith |= Physics.CollisionProjectile;
|
|
}
|
|
}
|
|
|
|
private void DisableProjectileCollisions()
|
|
{
|
|
if (item?.body?.FarseerBody == null) { return; }
|
|
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
|
|
if (originalCollisionCategories != Category.None && originalCollisionTargets != Category.None)
|
|
{
|
|
item.body.CollisionCategories = originalCollisionCategories;
|
|
item.body.CollidesWith = originalCollisionTargets;
|
|
}
|
|
else
|
|
{
|
|
if ((item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) && item.Condition > 0)
|
|
{
|
|
item.body.CollisionCategories = Physics.CollisionCharacter;
|
|
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
|
|
}
|
|
else
|
|
{
|
|
item.body.CollisionCategories = Physics.CollisionItem;
|
|
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
|
|
}
|
|
}
|
|
IgnoredBodies?.Clear();
|
|
}
|
|
|
|
public bool IsAttachedTo(PhysicsBody body)
|
|
{
|
|
return stickJoint != null && (stickJoint.BodyA == body?.FarseerBody || stickJoint.BodyB == body?.FarseerBody);
|
|
}
|
|
|
|
private void StickToTarget(Body targetBody, Vector2 axis)
|
|
{
|
|
if (stickJoint != null) { return; }
|
|
jointAxis = axis;
|
|
item.body.ResetDynamics();
|
|
if (Prismatic)
|
|
{
|
|
stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, useWorldCoordinates: true)
|
|
{
|
|
MotorEnabled = true,
|
|
MaxMotorForce = 30.0f,
|
|
LimitEnabled = true,
|
|
Breakpoint = JointBreakPoint,
|
|
};
|
|
|
|
if (maxJointTranslationInSimUnits == -1)
|
|
{
|
|
if (item.Sprite != null && MaxJointTranslation < 0)
|
|
{
|
|
MaxJointTranslation = item.Sprite.size.X / 2 * item.Scale;
|
|
}
|
|
MaxJointTranslation = Math.Min(MaxJointTranslation, 1000);
|
|
maxJointTranslationInSimUnits = ConvertUnits.ToSimUnits(MaxJointTranslation);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
stickJoint = new WeldJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, item.body.SimPosition, useWorldCoordinates: true)
|
|
{
|
|
FrequencyHz = 10.0f,
|
|
DampingRatio = 0.5f
|
|
};
|
|
}
|
|
stickTimer = StickDuration;
|
|
StickTarget = targetBody;
|
|
GameMain.World.Add(stickJoint);
|
|
IsActive = true;
|
|
if (targetBody.UserData is Limb limb)
|
|
{
|
|
stickTargetCharacter = limb.character;
|
|
stickTargetCharacter.AttachedProjectiles.Add(this);
|
|
}
|
|
}
|
|
|
|
public void Unstick()
|
|
{
|
|
StickTarget = null;
|
|
if (stickJoint != null)
|
|
{
|
|
if (GameMain.World.JointList.Contains(stickJoint))
|
|
{
|
|
GameMain.World.Remove(stickJoint);
|
|
}
|
|
stickJoint = null;
|
|
}
|
|
if (!item.body.FarseerBody.IsBullet)
|
|
{
|
|
IsActive = false;
|
|
if (DeactivationTime > 0 && deactivationTimer > 0)
|
|
{
|
|
DisableProjectileCollisions();
|
|
}
|
|
}
|
|
item.GetComponent<Rope>()?.Snap();
|
|
if (stickTargetCharacter != null)
|
|
{
|
|
stickTargetCharacter.AttachedProjectiles.Remove(this);
|
|
stickTargetCharacter = null;
|
|
}
|
|
}
|
|
|
|
protected override void RemoveComponentSpecific()
|
|
{
|
|
base.RemoveComponentSpecific();
|
|
if (IsStuckToTarget || stickJoint != null || stickTargetCharacter != null)
|
|
{
|
|
Unstick();
|
|
}
|
|
}
|
|
partial void LaunchProjSpecific(Vector2 startLocation, Vector2 endLocation);
|
|
}
|
|
}
|