using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Linq; namespace Barotrauma.Items.Components { class Throwable : Holdable { enum ThrowState { None, Initiated, Throwing } private const float ThrowAngleStart = -MathHelper.PiOver2, ThrowAngleEnd = MathHelper.PiOver2; private float throwAngle = ThrowAngleStart; private bool midAir; private ThrowState throwState; //continuous collision detection is used while the item is moving faster than this const float ContinuousCollisionThreshold = 5.0f; public Character CurrentThrower { get; private set; } [Serialize(1.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")] public float ThrowForce { get; set; } public Throwable(Item item, ContentXElement element) : base(item, element) { if (aimPos == Vector2.Zero) { aimPos = new Vector2(0.45f, 0.1f); } } public const float WaterDragCoefficient = 0.5f; public override bool Use(float deltaTime, Character character = null) { //actual throwing logic is handled in Update return (characterUsable && !UsageDisabledByRangedWeapon(character)) || character == null; } public override bool SecondaryUse(float deltaTime, Character character = null) { //actual throwing logic is handled in Update - SecondaryUse only triggers when the item is thrown return false; } public override void Drop(Character dropper, bool setTransform = true) { base.Drop(dropper, setTransform); throwState = ThrowState.None; throwAngle = ThrowAngleStart; Item.ResetWaterDragCoefficient(); } public override void UpdateBroken(float deltaTime, Camera cam) { Update(deltaTime, cam); } public override void Update(float deltaTime, Camera cam) { if (!item.body.Enabled) { return; } if (midAir) { if (item.body.FarseerBody.IsBullet) { if (item.body.LinearVelocity.LengthSquared() < ContinuousCollisionThreshold * ContinuousCollisionThreshold) { item.body.FarseerBody.IsBullet = false; } } if (item.body.LinearVelocity.LengthSquared() < 0.01f) { CurrentThrower = null; if (statusEffectLists?.ContainsKey(ActionType.OnImpact) ?? false) { foreach (var statusEffect in statusEffectLists[ActionType.OnImpact]) { statusEffect.SetUser(null); } } if (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false) { foreach (var statusEffect in statusEffectLists[ActionType.OnBroken]) { statusEffect.SetUser(null); } } item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; midAir = false; Item.ResetWaterDragCoefficient(); } return; } if (picker == null || picker.Removed || !picker.HeldItems.Contains(item)) { IsActive = false; return; } bool aim = false; if (!UsageDisabledByRangedWeapon(picker)) { if (throwState != ThrowState.Throwing) { if (picker.IsKeyDown(InputType.Aim)) { if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; } } else if (throwState != ThrowState.Initiated) { throwAngle = ThrowAngleStart; } } aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim; } if (picker.IsDead || !picker.AllowInput) { throwState = ThrowState.None; aim = false; } ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); //return if the status effect got rid of the picker somehow if (picker == null || picker.Removed || !picker.HeldItems.Contains(item)) { IsActive = false; return; } if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); } AnimController ac = picker.AnimController; item.Submarine = picker.Submarine; if (throwState != ThrowState.Throwing) { if (aim || throwState == ThrowState.Initiated) { throwAngle = System.Math.Min(throwAngle + deltaTime * 8.0f, ThrowAngleEnd); ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, throwAngle); if (throwAngle >= ThrowAngleEnd && throwState == ThrowState.Initiated) { throwState = ThrowState.Throwing; } } else { throwAngle = ThrowAngleStart; ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle); } } else { throwAngle = MathUtils.WrapAnglePi(throwAngle - deltaTime * 15.0f); ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, throwAngle); if (throwAngle < 0) { Vector2 throwVector = Vector2.Normalize(picker.CursorWorldPosition - picker.WorldPosition); //throw upwards if cursor is at the position of the character if (!MathUtils.IsValid(throwVector)) { throwVector = Vector2.UnitY; } #if SERVER GameServer.Log(GameServer.CharacterLogName(picker) + " threw " + item.Name, ServerLog.MessageType.ItemInteraction); #endif CurrentThrower = picker; if (statusEffectLists?.ContainsKey(ActionType.OnImpact) ?? false) { foreach (var statusEffect in statusEffectLists[ActionType.OnImpact]) { statusEffect.SetUser(CurrentThrower); } } if (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false) { foreach (var statusEffect in statusEffectLists[ActionType.OnBroken]) { statusEffect.SetUser(CurrentThrower); } } item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); item.WaterDragCoefficient = WaterDragCoefficient; float throwForce = ThrowForce; //Reduce force when aiming down float downwardsDotProduct = Vector2.Dot(-Vector2.UnitY, throwVector); //1 when pointing directly down, 0 when sideways, -1 when up if (downwardsDotProduct > 0) { throwForce *= (1.0f - downwardsDotProduct * 0.7f); } item.body.ApplyLinearImpulse(throwVector * throwForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); //disable platform collisions until the item comes back to rest again item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; 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); Limb rightHand = ac.GetLimb(LimbType.RightHand); item.body.AngularVelocity = rightHand.body.AngularVelocity; throwAngle = ThrowAngleStart; IsActive = true; if (GameMain.NetworkMember is { IsServer: true }) { GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, targetCharacter: CurrentThrower)); } if (!(GameMain.NetworkMember is { IsClient: true })) { //Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse" ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: CurrentThrower, user: CurrentThrower); } throwState = ThrowState.None; } } } } }