(61d00a474) v0.9.7.1
This commit is contained in:
@@ -0,0 +1,669 @@
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Holdable : Pickable, IServerSerializable, IClientSerializable
|
||||
{
|
||||
const float MaxAttachDistance = 150.0f;
|
||||
|
||||
//the position(s) in the item that the Character grabs
|
||||
protected Vector2[] handlePos;
|
||||
private readonly Vector2[] scaledHandlePos;
|
||||
|
||||
private InputType prevPickKey;
|
||||
private string prevMsg;
|
||||
private Dictionary<RelatedItem.RelationType, List<RelatedItem>> prevRequiredItems;
|
||||
|
||||
//the distance from the holding characters elbow to center of the physics body of the item
|
||||
protected Vector2 holdPos;
|
||||
|
||||
protected Vector2 aimPos;
|
||||
|
||||
private float swingState;
|
||||
|
||||
private bool attachable, attached, attachedByDefault;
|
||||
private readonly PhysicsBody body;
|
||||
public PhysicsBody Pusher
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//the angle in which the Character holds the item
|
||||
protected float holdAngle;
|
||||
|
||||
public PhysicsBody Body
|
||||
{
|
||||
get { return item.body ?? body; }
|
||||
}
|
||||
|
||||
[Serialize(false, true, description: "Is the item currently attached to a wall (only valid if Attachable is set to true).")]
|
||||
public bool Attached
|
||||
{
|
||||
get { return attached && item.ParentInventory == null; }
|
||||
set
|
||||
{
|
||||
attached = value;
|
||||
item.SetActiveSprite();
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(true, true, description: "Can the item be pointed to a specific direction or do the characters always hold it in a static pose.")]
|
||||
public bool Aimable
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Should the character adjust its pose when aiming with the item. Most noticeable underwater, where the character will rotate its entire body to face the direction the item is aimed at.")]
|
||||
public bool ControlPose
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Can the item be attached to walls.")]
|
||||
public bool Attachable
|
||||
{
|
||||
get { return attachable; }
|
||||
set { attachable = value; }
|
||||
}
|
||||
|
||||
[Serialize(true, false, description: "Can the item be reattached to walls after it has been deattached (only valid if Attachable is set to true).")]
|
||||
public bool Reattachable
|
||||
{
|
||||
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
|
||||
{
|
||||
get { return attachedByDefault; }
|
||||
set { attachedByDefault = value; }
|
||||
}
|
||||
|
||||
[Editable, Serialize("0.0,0.0", false, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+
|
||||
" For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")]
|
||||
public Vector2 HoldPos
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(holdPos); }
|
||||
set { holdPos = ConvertUnits.ToSimUnits(value); }
|
||||
}
|
||||
|
||||
[Serialize("0.0,0.0", false, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+
|
||||
" Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")]
|
||||
public Vector2 AimPos
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(aimPos); }
|
||||
set { aimPos = ConvertUnits.ToSimUnits(value); }
|
||||
}
|
||||
|
||||
[Editable, Serialize(0.0f, false, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")]
|
||||
public float HoldAngle
|
||||
{
|
||||
get { return MathHelper.ToDegrees(holdAngle); }
|
||||
set { holdAngle = MathHelper.ToRadians(value); }
|
||||
}
|
||||
|
||||
private Vector2 swingAmount;
|
||||
[Editable, Serialize("0.0,0.0", false, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")]
|
||||
public Vector2 SwingAmount
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(swingAmount); }
|
||||
set { swingAmount = ConvertUnits.ToSimUnits(value); }
|
||||
}
|
||||
|
||||
[Editable, Serialize(0.0f, false, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")]
|
||||
public float SwingSpeed { get; set; }
|
||||
|
||||
[Editable, Serialize(false, false, description: "Should the item swing around when it's being held.")]
|
||||
public bool SwingWhenHolding { get; set; }
|
||||
[Editable, Serialize(false, false, description: "Should the item swing around when it's being aimed.")]
|
||||
public bool SwingWhenAiming { get; set; }
|
||||
[Editable, Serialize(false, false, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")]
|
||||
public bool SwingWhenUsing { get; set; }
|
||||
|
||||
public Holdable(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
body = item.body;
|
||||
|
||||
Pusher = null;
|
||||
if (element.GetAttributeBool("blocksplayers", false))
|
||||
{
|
||||
Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, item.body.Density)
|
||||
{
|
||||
BodyType = BodyType.Dynamic,
|
||||
CollidesWith = Physics.CollisionCharacter,
|
||||
CollisionCategories = Physics.CollisionItemBlocking,
|
||||
Enabled = false
|
||||
};
|
||||
Pusher.FarseerBody.OnCollision += OnPusherCollision;
|
||||
Pusher.FarseerBody.FixedRotation = false;
|
||||
Pusher.FarseerBody.IgnoreGravity = true;
|
||||
}
|
||||
|
||||
handlePos = new Vector2[2];
|
||||
scaledHandlePos = new Vector2[2];
|
||||
Vector2 previousValue = Vector2.Zero;
|
||||
for (int i = 1; i < 3; i++)
|
||||
{
|
||||
int index = i - 1;
|
||||
string attributeName = "handle" + i;
|
||||
var attribute = element.Attribute(attributeName);
|
||||
// If no value is defind for handle2, use the value of handle1.
|
||||
var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue;
|
||||
handlePos[index] = value;
|
||||
previousValue = value;
|
||||
}
|
||||
|
||||
canBePicked = true;
|
||||
|
||||
if (attachable)
|
||||
{
|
||||
prevMsg = DisplayMsg;
|
||||
prevPickKey = PickKey;
|
||||
prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
|
||||
|
||||
if (item.Submarine != null)
|
||||
{
|
||||
if (item.Submarine.Loading)
|
||||
{
|
||||
AttachToWall();
|
||||
Attached = false;
|
||||
}
|
||||
else //the submarine is not being loaded, which means we're either in the sub editor or the item has been spawned mid-round
|
||||
{
|
||||
if (Screen.Selected == GameMain.SubEditorScreen)
|
||||
{
|
||||
//in the sub editor, attach
|
||||
AttachToWall();
|
||||
}
|
||||
else
|
||||
{
|
||||
//spawned mid-round, deattach
|
||||
DeattachFromWall();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool OnPusherCollision(Fixture sender, Fixture other, Contact contact)
|
||||
{
|
||||
if (other.Body.UserData is Character character)
|
||||
{
|
||||
if (!IsActive) { return false; }
|
||||
return character != picker;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Load(XElement componentElement, bool usePrefabValues)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues);
|
||||
|
||||
if (usePrefabValues)
|
||||
{
|
||||
//this needs to be loaded regardless
|
||||
Attached = componentElement.GetAttributeBool("attached", attached);
|
||||
}
|
||||
|
||||
if (attachable)
|
||||
{
|
||||
prevMsg = DisplayMsg;
|
||||
prevPickKey = PickKey;
|
||||
prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Drop(Character dropper)
|
||||
{
|
||||
Drop(true, dropper);
|
||||
}
|
||||
|
||||
private void Drop(bool dropConnectedWires, Character dropper)
|
||||
{
|
||||
if (dropConnectedWires)
|
||||
{
|
||||
DropConnectedWires(dropper);
|
||||
}
|
||||
|
||||
if (attachable)
|
||||
{
|
||||
DeattachFromWall();
|
||||
|
||||
if (body != null)
|
||||
{
|
||||
item.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
if (Pusher != null) { Pusher.Enabled = false; }
|
||||
if (item.body != null) { item.body.Enabled = true; }
|
||||
IsActive = false;
|
||||
|
||||
if (picker == null)
|
||||
{
|
||||
if (dropper == null) { return; }
|
||||
picker = dropper;
|
||||
}
|
||||
if (picker.Inventory == null) { return; }
|
||||
|
||||
item.Submarine = picker.Submarine;
|
||||
|
||||
if (item.body != null)
|
||||
{
|
||||
if (item.body.Removed)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
"Failed to drop the Holdable component of the item \"" + item.Name + "\" (body has been removed"
|
||||
+ (item.Removed ? ", item has been removed)" : ")"));
|
||||
}
|
||||
else
|
||||
{
|
||||
item.body.ResetDynamics();
|
||||
Limb heldHand, arm;
|
||||
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
|
||||
{
|
||||
heldHand = picker.AnimController.GetLimb(LimbType.LeftHand);
|
||||
arm = picker.AnimController.GetLimb(LimbType.LeftArm);
|
||||
}
|
||||
else
|
||||
{
|
||||
heldHand = picker.AnimController.GetLimb(LimbType.RightHand);
|
||||
arm = picker.AnimController.GetLimb(LimbType.RightArm);
|
||||
}
|
||||
if (heldHand != null && arm != null)
|
||||
{
|
||||
//hand simPosition is actually in the wrist so need to move the item out from it slightly
|
||||
Vector2 diff = new Vector2(
|
||||
(heldHand.SimPosition.X - arm.SimPosition.X) / 2f,
|
||||
(heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f);
|
||||
item.SetTransform(heldHand.SimPosition + diff, 0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.SetTransform(picker.SimPosition, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
picker.DeselectItem(item);
|
||||
picker.Inventory.RemoveItem(item);
|
||||
picker = null;
|
||||
}
|
||||
|
||||
public override void Equip(Character character)
|
||||
{
|
||||
picker = character;
|
||||
|
||||
if (character != null) item.Submarine = character.Submarine;
|
||||
|
||||
if (item.body == null)
|
||||
{
|
||||
if (body != null)
|
||||
{
|
||||
item.body = body;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.body.Enabled)
|
||||
{
|
||||
Limb rightHand = picker.AnimController.GetLimb(LimbType.RightHand);
|
||||
item.SetTransform(rightHand.SimPosition, 0.0f);
|
||||
}
|
||||
|
||||
bool alreadyEquipped = character.HasEquippedItem(item);
|
||||
bool canSelect = picker.TrySelectItem(item);
|
||||
|
||||
if (canSelect || picker.HasEquippedItem(item))
|
||||
{
|
||||
if (!canSelect)
|
||||
{
|
||||
character.DeselectItem(item);
|
||||
}
|
||||
|
||||
item.body.Enabled = true;
|
||||
item.body.PhysEnabled = false;
|
||||
IsActive = true;
|
||||
|
||||
#if SERVER
|
||||
if (!alreadyEquipped) GameServer.Log(character.LogName + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unequip(Character character)
|
||||
{
|
||||
if (picker == null) return;
|
||||
|
||||
picker.DeselectItem(item);
|
||||
#if SERVER
|
||||
GameServer.Log(character.LogName + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction);
|
||||
#endif
|
||||
|
||||
item.body.PhysEnabled = true;
|
||||
item.body.Enabled = false;
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
public bool CanBeAttached()
|
||||
{
|
||||
if (!attachable || !Reattachable) return false;
|
||||
|
||||
//can be attached anywhere in sub editor
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) return true;
|
||||
|
||||
//can be attached anywhere inside hulls
|
||||
if (item.CurrentHull != null) return true;
|
||||
|
||||
return Structure.GetAttachTarget(item.WorldPosition) != null;
|
||||
}
|
||||
|
||||
public bool CanBeDeattached()
|
||||
{
|
||||
if (!attachable || !attached) return true;
|
||||
|
||||
//allow deattaching everywhere in sub editor
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) return true;
|
||||
|
||||
//don't allow deattaching if part of a sub and outside hulls
|
||||
return item.Submarine == null || item.CurrentHull != null;
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
if (!attachable)
|
||||
{
|
||||
return base.Pick(picker);
|
||||
}
|
||||
|
||||
if (!CanBeDeattached()) return false;
|
||||
|
||||
if (Attached)
|
||||
{
|
||||
return base.Pick(picker);
|
||||
}
|
||||
else
|
||||
{
|
||||
//not attached -> pick the item instantly, ignoring picking time
|
||||
return OnPicked(picker);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool OnPicked(Character picker)
|
||||
{
|
||||
if (base.OnPicked(picker))
|
||||
{
|
||||
DeattachFromWall();
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && attachable)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
if (picker != null)
|
||||
{
|
||||
GameServer.Log(picker.LogName + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void AttachToWall()
|
||||
{
|
||||
if (!attachable) return;
|
||||
|
||||
//outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub
|
||||
if (item.CurrentHull == null && item.Submarine == null)
|
||||
{
|
||||
Structure attachTarget = Structure.GetAttachTarget(item.WorldPosition);
|
||||
if (attachTarget != null)
|
||||
{
|
||||
if (attachTarget.Submarine != null)
|
||||
{
|
||||
//set to submarine-relative position
|
||||
item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false);
|
||||
}
|
||||
item.Submarine = attachTarget.Submarine;
|
||||
}
|
||||
}
|
||||
|
||||
var containedItems = item.ContainedItems;
|
||||
if (containedItems != null)
|
||||
{
|
||||
foreach (Item contained in containedItems)
|
||||
{
|
||||
if (contained.body == null) continue;
|
||||
contained.SetTransform(item.SimPosition, contained.body.Rotation);
|
||||
}
|
||||
}
|
||||
|
||||
body.Enabled = false;
|
||||
item.body = null;
|
||||
|
||||
DisplayMsg = prevMsg;
|
||||
PickKey = prevPickKey;
|
||||
requiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(prevRequiredItems);
|
||||
|
||||
Attached = true;
|
||||
}
|
||||
|
||||
public void DeattachFromWall()
|
||||
{
|
||||
if (!attachable) return;
|
||||
|
||||
Attached = false;
|
||||
|
||||
//make the item pickable with the default pick key and with no specific tools/items when it's deattached
|
||||
requiredItems.Clear();
|
||||
DisplayMsg = "";
|
||||
PickKey = InputType.Select;
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (!attachable || item.body == null) { return character == null || character.IsKeyDown(InputType.Aim); }
|
||||
if (character != null)
|
||||
{
|
||||
if (!character.IsKeyDown(InputType.Aim)) { return false; }
|
||||
if (!CanBeAttached()) { return false; }
|
||||
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
if (character != Character.Controlled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if CLIENT
|
||||
Vector2 attachPos = ConvertUnits.ToSimUnits(GetAttachPosition(character));
|
||||
GameMain.Client.CreateEntityEvent(item, new object[]
|
||||
{
|
||||
NetEntityEvent.Type.ComponentState,
|
||||
item.GetComponentIndex(this),
|
||||
attachPos
|
||||
});
|
||||
#endif
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Drop(character);
|
||||
item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
AttachToWall();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Vector2 GetAttachPosition(Character user)
|
||||
{
|
||||
if (user == null) { return item.Position; }
|
||||
|
||||
Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition;
|
||||
mouseDiff = mouseDiff.ClampLength(MaxAttachDistance);
|
||||
|
||||
return new Vector2(
|
||||
MathUtils.RoundTowardsClosest(user.Position.X + mouseDiff.X, Submarine.GridSize.X),
|
||||
MathUtils.RoundTowardsClosest(user.Position.Y + mouseDiff.Y, Submarine.GridSize.Y));
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
{
|
||||
Update(deltaTime, cam);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (item.body == null || !item.body.Enabled) return;
|
||||
if (picker == null || !picker.HasEquippedItem(item))
|
||||
{
|
||||
if (Pusher != null) { Pusher.Enabled = false; }
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 swing = Vector2.Zero;
|
||||
if (swingAmount != Vector2.Zero)
|
||||
{
|
||||
swingState += deltaTime;
|
||||
swingState %= 1.0f;
|
||||
if (SwingWhenHolding ||
|
||||
(SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) ||
|
||||
(SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot)))
|
||||
{
|
||||
swing = swingAmount * new Vector2(
|
||||
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f,
|
||||
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
|
||||
|
||||
if (item.body.Dir != picker.AnimController.Dir) Flip();
|
||||
|
||||
item.Submarine = picker.Submarine;
|
||||
|
||||
if (picker.HasSelectedItem(item))
|
||||
{
|
||||
scaledHandlePos[0] = handlePos[0] * item.Scale;
|
||||
scaledHandlePos[1] = handlePos[1] * item.Scale;
|
||||
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && (picker.SelectedConstruction == null || picker.SelectedConstruction.GetComponent<Ladder>() != null);
|
||||
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle);
|
||||
}
|
||||
else
|
||||
{
|
||||
Limb equipLimb = null;
|
||||
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Headset) || picker.Inventory.IsInLimbSlot(item, InvSlotType.Head))
|
||||
{
|
||||
equipLimb = picker.AnimController.GetLimb(LimbType.Head);
|
||||
}
|
||||
else if (picker.Inventory.IsInLimbSlot(item, InvSlotType.InnerClothes) ||
|
||||
picker.Inventory.IsInLimbSlot(item, InvSlotType.OuterClothes))
|
||||
{
|
||||
equipLimb = picker.AnimController.GetLimb(LimbType.Torso);
|
||||
}
|
||||
|
||||
if (equipLimb != null)
|
||||
{
|
||||
float itemAngle = (equipLimb.Rotation + holdAngle * picker.AnimController.Dir);
|
||||
|
||||
Matrix itemTransfrom = Matrix.CreateRotationZ(equipLimb.Rotation);
|
||||
Vector2 transformedHandlePos = Vector2.Transform(handlePos[0] * item.Scale, itemTransfrom);
|
||||
|
||||
item.body.ResetDynamics();
|
||||
item.SetTransform(equipLimb.SimPosition - transformedHandlePos, itemAngle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Flip()
|
||||
{
|
||||
handlePos[0].X = -handlePos[0].X;
|
||||
handlePos[1].X = -handlePos[1].X;
|
||||
item.body.Dir = -item.body.Dir;
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
if (item.Submarine != null && item.Submarine.Loading) return;
|
||||
OnMapLoaded();
|
||||
item.SetActiveSprite();
|
||||
}
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
if (!attachable) return;
|
||||
|
||||
if (Attached)
|
||||
{
|
||||
AttachToWall();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
if (body != null)
|
||||
{
|
||||
item.body = body;
|
||||
body.Enabled = false;
|
||||
}
|
||||
}
|
||||
DeattachFromWall();
|
||||
}
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
if (!attachable)
|
||||
{
|
||||
return base.Save(parentElement);
|
||||
}
|
||||
|
||||
var tempMsg = DisplayMsg;
|
||||
var tempPickKey = PickKey;
|
||||
var tempRequiredItems = requiredItems;
|
||||
|
||||
DisplayMsg = prevMsg;
|
||||
PickKey = prevPickKey;
|
||||
requiredItems = prevRequiredItems;
|
||||
|
||||
XElement saveElement = base.Save(parentElement);
|
||||
|
||||
DisplayMsg = tempMsg;
|
||||
PickKey = tempPickKey;
|
||||
requiredItems = tempRequiredItems;
|
||||
|
||||
return saveElement;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class LevelResource : ItemComponent, IServerSerializable
|
||||
{
|
||||
private PhysicsBody trigger;
|
||||
|
||||
private Holdable holdable;
|
||||
|
||||
private float deattachTimer;
|
||||
|
||||
[Serialize(1.0f, false, description: "How long it takes to deattach the item from the level walls (in seconds).")]
|
||||
public float DeattachDuration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false, description: "How far along the item is to being deattached. When the timer goes above DeattachDuration, the item is deattached.")]
|
||||
public float DeattachTimer
|
||||
{
|
||||
get { return deattachTimer; }
|
||||
set
|
||||
{
|
||||
//clients don't deattach the item until the server says so (handled in ClientRead)
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
deattachTimer = Math.Max(0.0f, value);
|
||||
#if SERVER
|
||||
if (deattachTimer >= DeattachDuration)
|
||||
{
|
||||
if (holdable.Attached) { item.CreateServerEvent(this); }
|
||||
holdable.DeattachFromWall();
|
||||
}
|
||||
else if (Math.Abs(lastSentDeattachTimer - deattachTimer) > 0.1f)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
lastSentDeattachTimer = deattachTimer;
|
||||
}
|
||||
#else
|
||||
if (deattachTimer >= DeattachDuration)
|
||||
{
|
||||
holdable.DeattachFromWall();
|
||||
trigger.Enabled = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public bool Attached
|
||||
{
|
||||
get { return holdable == null ? false : holdable.Attached; }
|
||||
}
|
||||
|
||||
public LevelResource(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (!holdable.Attached)
|
||||
{
|
||||
trigger.Enabled = false;
|
||||
IsActive = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f)
|
||||
{
|
||||
trigger.SetTransform(item.SimPosition, 0.0f);
|
||||
}
|
||||
IsActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
holdable = item.GetComponent<Holdable>();
|
||||
if (holdable == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while initializing item \"" + item.Name + "\". Level resources require a Holdable component.");
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
holdable.Reattachable = false;
|
||||
if (requiredItems.Any())
|
||||
{
|
||||
holdable.PickingTime = float.MaxValue;
|
||||
}
|
||||
|
||||
var body = item.body ?? holdable.Body;
|
||||
|
||||
if (body != null)
|
||||
{
|
||||
trigger = new PhysicsBody(body.width, body.height, body.radius, body.Density)
|
||||
{
|
||||
UserData = item
|
||||
};
|
||||
trigger.FarseerBody.SetIsSensor(true);
|
||||
trigger.FarseerBody.BodyType = BodyType.Static;
|
||||
trigger.FarseerBody.CollisionCategories = Physics.CollisionWall;
|
||||
trigger.FarseerBody.CollidesWith = Physics.CollisionNone;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
if (trigger != null)
|
||||
{
|
||||
trigger.Remove();
|
||||
trigger = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class MeleeWeapon : Holdable
|
||||
{
|
||||
private float hitPos;
|
||||
|
||||
private bool hitting;
|
||||
|
||||
private float range;
|
||||
private float reload;
|
||||
|
||||
private float reloadTimer;
|
||||
|
||||
private readonly Attack attack;
|
||||
|
||||
private readonly HashSet<Entity> hitTargets = new HashSet<Entity>();
|
||||
|
||||
private readonly Queue<Fixture> impactQueue = new Queue<Fixture>();
|
||||
|
||||
public Character User { get; private set; }
|
||||
|
||||
[Serialize(0.0f, false, description: "An estimation of how close the item has to be to the target for it to hit. Used by AI characters to determine when they're close enough to hit a target.")]
|
||||
public float Range
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(range); }
|
||||
set { range = ConvertUnits.ToSimUnits(value); }
|
||||
}
|
||||
|
||||
[Serialize(0.5f, false, description: "How long the user has to wait before they can hit with the weapon again (in seconds).")]
|
||||
public float Reload
|
||||
{
|
||||
get { return reload; }
|
||||
set { reload = Math.Max(0.0f, value); }
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Can the weapon hit multiple targets per swing.")]
|
||||
public bool AllowHitMultiple
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public MeleeWeapon(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
attack = new Attack(subElement, item.Name + ", MeleeWeapon");
|
||||
}
|
||||
item.IsShootable = true;
|
||||
// TODO: should define this in xml if we have melee weapons that don't require aim to use
|
||||
item.RequireAimToUse = true;
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (character == null || reloadTimer > 0.0f) { return false; }
|
||||
if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; }
|
||||
|
||||
//don't allow hitting if the character is already hitting with another weapon
|
||||
for (int i = 0; i < 2; i++ )
|
||||
{
|
||||
if (character.SelectedItems[i] == null || character.SelectedItems[i] == Item) { continue; }
|
||||
|
||||
var otherWeapon = character.SelectedItems[i].GetComponent<MeleeWeapon>();
|
||||
if (otherWeapon == null) { continue; }
|
||||
if (otherWeapon.hitting) { return false; }
|
||||
}
|
||||
|
||||
SetUser(character);
|
||||
|
||||
if (hitPos < MathHelper.PiOver4) { return false; }
|
||||
|
||||
ActivateNearbySleepingCharacters();
|
||||
reloadTimer = reload;
|
||||
|
||||
item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
|
||||
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall;
|
||||
item.body.FarseerBody.OnCollision += OnCollision;
|
||||
item.body.FarseerBody.IsBullet = true;
|
||||
item.body.PhysEnabled = true;
|
||||
|
||||
if (!character.AnimController.InWater)
|
||||
{
|
||||
foreach (Limb l in character.AnimController.Limbs)
|
||||
{
|
||||
if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; }
|
||||
if (l.type == LimbType.Head || l.type == LimbType.Torso)
|
||||
{
|
||||
l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 7.0f, -4.0f));
|
||||
}
|
||||
else
|
||||
{
|
||||
l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 5.0f, -2.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hitting = true;
|
||||
hitTargets.Clear();
|
||||
|
||||
IsActive = true;
|
||||
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
|
||||
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void Drop(Character dropper)
|
||||
{
|
||||
base.Drop(dropper);
|
||||
hitting = false;
|
||||
hitPos = 0.0f;
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
{
|
||||
Update(deltaTime, cam);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (!item.body.Enabled) { impactQueue.Clear(); return; }
|
||||
if (!picker.HasSelectedItem(item)) { impactQueue.Clear(); IsActive = false; }
|
||||
|
||||
while (impactQueue.Count > 0)
|
||||
{
|
||||
var impact = impactQueue.Dequeue();
|
||||
HandleImpact(impact.Body);
|
||||
}
|
||||
|
||||
reloadTimer -= deltaTime;
|
||||
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) { Flip(); }
|
||||
|
||||
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.SelectedConstruction == null || picker.SelectedConstruction.GetComponent<Ladder>() != null);
|
||||
if (aim)
|
||||
{
|
||||
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 5f, MathHelper.PiOver4));
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, hitPos, holdAngle + hitPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
hitPos = 0;
|
||||
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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 (hitPos < -MathHelper.PiOver2)
|
||||
{
|
||||
RestoreCollision();
|
||||
hitting = false;
|
||||
hitTargets.Clear();
|
||||
hitPos = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activate sleeping ragdolls that are close enough to hit with the weapon (otherwise the collision will not be registered)
|
||||
/// </summary>
|
||||
private void ActivateNearbySleepingCharacters()
|
||||
{
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!c.Enabled || !c.AnimController.BodyInRest) { continue; }
|
||||
//do a broad check first
|
||||
if (Math.Abs(c.WorldPosition.X - item.WorldPosition.X) > 1000.0f) { continue; }
|
||||
if (Math.Abs(c.WorldPosition.Y - item.WorldPosition.Y) > 1000.0f) { continue; }
|
||||
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
{
|
||||
float hitRange = 2.0f;
|
||||
if (Vector2.DistanceSquared(limb.SimPosition, item.SimPosition) < hitRange * hitRange)
|
||||
{
|
||||
c.AnimController.BodyInRest = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetUser(Character character)
|
||||
{
|
||||
if (User == character) { return; }
|
||||
if (User != null && User.Removed) { User = null; }
|
||||
|
||||
User = character;
|
||||
}
|
||||
|
||||
private void RestoreCollision()
|
||||
{
|
||||
impactQueue.Clear();
|
||||
item.body.FarseerBody.OnCollision -= OnCollision;
|
||||
item.body.CollisionCategories = Physics.CollisionItem;
|
||||
item.body.CollidesWith = Physics.CollisionWall;
|
||||
item.body.FarseerBody.IsBullet = false;
|
||||
item.body.PhysEnabled = false;
|
||||
}
|
||||
|
||||
|
||||
private bool OnCollision(Fixture f1, Fixture f2, Contact contact)
|
||||
{
|
||||
if (User == null || User.Removed)
|
||||
{
|
||||
impactQueue.Enqueue(f2);
|
||||
return true;
|
||||
}
|
||||
|
||||
//ignore collision if there's a wall between the user and the weapon to prevent hitting through walls
|
||||
if (Submarine.PickBody(User.AnimController.AimSourceSimPos,
|
||||
item.SimPosition,
|
||||
collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
|
||||
allowInsideFixture: true,
|
||||
customPredicate: (Fixture fixture) => { return fixture.CollidesWith.HasFlag(Physics.CollisionItem); }) != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Character targetCharacter = null;
|
||||
Limb targetLimb = null;
|
||||
Structure targetStructure = null;
|
||||
Item targetItem = null;
|
||||
|
||||
if (f2.Body.UserData is Limb)
|
||||
{
|
||||
targetLimb = (Limb)f2.Body.UserData;
|
||||
if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; }
|
||||
targetCharacter = targetLimb.character;
|
||||
if (targetCharacter == picker) { return false; }
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetCharacter)) { return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Character)) { return false; }
|
||||
}
|
||||
hitTargets.Add(targetCharacter);
|
||||
}
|
||||
else if (f2.Body.UserData is Character)
|
||||
{
|
||||
targetCharacter = (Character)f2.Body.UserData;
|
||||
if (targetCharacter == picker || targetCharacter == User) { return false; }
|
||||
targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetCharacter)) { return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Character)) { return false; }
|
||||
}
|
||||
hitTargets.Add(targetCharacter);
|
||||
}
|
||||
else if (f2.Body.UserData is Structure)
|
||||
{
|
||||
targetStructure = (Structure)f2.Body.UserData;
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetStructure)) { return true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Structure)) { return true; }
|
||||
}
|
||||
hitTargets.Add(targetStructure);
|
||||
}
|
||||
else if (f2.Body.UserData is Item)
|
||||
{
|
||||
targetItem = (Item)f2.Body.UserData;
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetItem)) { return true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Item)) { return true; }
|
||||
}
|
||||
hitTargets.Add(targetItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attack != null)
|
||||
{
|
||||
if (targetLimb == null && targetCharacter == null && targetStructure == null && (targetItem == null || ! targetItem.Prefab.DamagedByMeleeWeapons))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetLimb != null)
|
||||
{
|
||||
targetLimb.character.LastDamageSource = item;
|
||||
attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetCharacter != null)
|
||||
{
|
||||
targetCharacter.LastDamageSource = item;
|
||||
attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetStructure != null)
|
||||
{
|
||||
attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons)
|
||||
{
|
||||
attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return true; }
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits
|
||||
{
|
||||
|
||||
GameMain.Server.CreateEntityEvent(item, new object[]
|
||||
{
|
||||
Networking.NetEntityEvent.Type.ApplyStatusEffect,
|
||||
ActionType.OnUse,
|
||||
null, //itemcomponent
|
||||
targetCharacter.ID, targetLimb
|
||||
});
|
||||
|
||||
string logStr = picker?.LogName + " used " + item.Name;
|
||||
if (item.ContainedItems != null && item.ContainedItems.Any())
|
||||
{
|
||||
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
|
||||
}
|
||||
logStr += " on " + targetCharacter.LogName + ".";
|
||||
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User);
|
||||
}
|
||||
|
||||
if (DeleteOnUse)
|
||||
{
|
||||
Entity.Spawner.AddToRemoveQueue(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleImpact(Body target)
|
||||
{
|
||||
if (User == null || User.Removed || target == null)
|
||||
{
|
||||
RestoreCollision();
|
||||
hitting = false;
|
||||
User = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Limb targetLimb = target.UserData as Limb;
|
||||
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
|
||||
Structure targetStructure = target.UserData as Structure;
|
||||
Item targetItem = target.UserData as Item;
|
||||
|
||||
if (attack != null)
|
||||
{
|
||||
attack.SetUser(User);
|
||||
|
||||
if (targetLimb != null)
|
||||
{
|
||||
if (targetLimb.character.Removed) { return; }
|
||||
targetLimb.character.LastDamageSource = item;
|
||||
attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetCharacter != null)
|
||||
{
|
||||
if (targetCharacter.Removed) { return; }
|
||||
targetCharacter.LastDamageSource = item;
|
||||
attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetStructure != null)
|
||||
{
|
||||
if (targetStructure.Removed) { return; }
|
||||
attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons)
|
||||
{
|
||||
if (targetItem.Removed) { return; }
|
||||
attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits
|
||||
{
|
||||
GameMain.Server.CreateEntityEvent(item, new object[]
|
||||
{
|
||||
Networking.NetEntityEvent.Type.ApplyStatusEffect,
|
||||
ActionType.OnUse,
|
||||
null, //itemcomponent
|
||||
targetCharacter.ID, targetLimb
|
||||
});
|
||||
|
||||
string logStr = picker?.LogName + " used " + item.Name;
|
||||
if (item.ContainedItems != null && item.ContainedItems.Any())
|
||||
{
|
||||
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
|
||||
}
|
||||
logStr += " on " + targetCharacter.LogName + ".";
|
||||
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User);
|
||||
}
|
||||
|
||||
if (DeleteOnUse)
|
||||
{
|
||||
Entity.Spawner.AddToRemoveQueue(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class Pickable : ItemComponent, IServerSerializable
|
||||
{
|
||||
protected Character picker;
|
||||
|
||||
protected List<InvSlotType> allowedSlots;
|
||||
|
||||
private float pickTimer;
|
||||
|
||||
private Character activePicker;
|
||||
|
||||
private CoroutineHandle pickingCoroutine;
|
||||
|
||||
public List<InvSlotType> AllowedSlots
|
||||
{
|
||||
get { return allowedSlots; }
|
||||
}
|
||||
|
||||
public Character Picker
|
||||
{
|
||||
get { return picker; }
|
||||
}
|
||||
|
||||
public Pickable(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
allowedSlots = new List<InvSlotType>();
|
||||
|
||||
string slotString = element.GetAttributeString("slots", "Any");
|
||||
string[] slotCombinations = slotString.Split(',');
|
||||
foreach (string slotCombination in slotCombinations)
|
||||
{
|
||||
string[] slots = slotCombination.Split('+');
|
||||
InvSlotType allowedSlot = InvSlotType.None;
|
||||
foreach (string slot in slots)
|
||||
{
|
||||
switch (slot.ToLowerInvariant())
|
||||
{
|
||||
case "bothhands":
|
||||
allowedSlot = InvSlotType.LeftHand | InvSlotType.RightHand;
|
||||
break;
|
||||
default:
|
||||
allowedSlot = allowedSlot | (InvSlotType)Enum.Parse(typeof(InvSlotType), slot.Trim());
|
||||
break;
|
||||
}
|
||||
}
|
||||
allowedSlots.Add(allowedSlot);
|
||||
}
|
||||
|
||||
canBePicked = true;
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
//return if someone is already trying to pick the item
|
||||
if (pickTimer > 0.0f) return false;
|
||||
if (picker == null || picker.Inventory == null) return false;
|
||||
|
||||
if (PickingTime > 0.0f)
|
||||
{
|
||||
if (picker.PickingItem == null && PickingTime <= float.MaxValue)
|
||||
{
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
pickingCoroutine = CoroutineManager.StartCoroutine(WaitForPick(picker, PickingTime));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return OnPicked(picker);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool OnPicked(Character picker)
|
||||
{
|
||||
if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots))
|
||||
{
|
||||
if (!picker.HasSelectedItem(item) && item.body != null) item.body.Enabled = false;
|
||||
this.picker = picker;
|
||||
|
||||
for (int i = item.linkedTo.Count - 1; i >= 0; i--)
|
||||
{
|
||||
item.linkedTo[i].RemoveLinked(item);
|
||||
}
|
||||
item.linkedTo.Clear();
|
||||
|
||||
DropConnectedWires(picker);
|
||||
|
||||
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
|
||||
#if CLIENT
|
||||
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) GUI.PlayUISound(GUISoundType.PickItem);
|
||||
PlaySound(ActionType.OnPicked, picker);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) GUI.PlayUISound(GUISoundType.PickItemFail);
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<object> WaitForPick(Character picker, float requiredTime)
|
||||
{
|
||||
activePicker = picker;
|
||||
picker.PickingItem = item;
|
||||
|
||||
var leftHand = picker.AnimController.GetLimb(LimbType.LeftHand);
|
||||
var rightHand = picker.AnimController.GetLimb(LimbType.RightHand);
|
||||
|
||||
pickTimer = 0.0f;
|
||||
while (pickTimer < requiredTime && Screen.Selected != GameMain.SubEditorScreen)
|
||||
{
|
||||
//cancel if the item is currently selected
|
||||
//attempting to pick does not select the item, so if it is selected at this point, another ItemComponent
|
||||
//must have been selected and we should not keep deattaching (happens when for example interacting with
|
||||
//an electrical component while holding both a screwdriver and a wrench).
|
||||
if (picker.SelectedConstruction == item ||
|
||||
picker.IsKeyDown(InputType.Aim) ||
|
||||
!picker.CanInteractWith(item) ||
|
||||
item.Removed || item.ParentInventory != null)
|
||||
{
|
||||
StopPicking(picker);
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
Character.Controlled?.UpdateHUDProgressBar(
|
||||
this,
|
||||
item.WorldPosition,
|
||||
pickTimer / requiredTime,
|
||||
GUI.Style.Red, GUI.Style.Green);
|
||||
#endif
|
||||
|
||||
picker.AnimController.UpdateUseItem(true, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
|
||||
pickTimer += CoroutineManager.DeltaTime;
|
||||
|
||||
yield return CoroutineStatus.Running;
|
||||
}
|
||||
|
||||
StopPicking(picker);
|
||||
|
||||
bool isNotRemote = true;
|
||||
#if CLIENT
|
||||
isNotRemote = !picker.IsRemotePlayer;
|
||||
#endif
|
||||
if (isNotRemote) OnPicked(picker);
|
||||
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
protected void StopPicking(Character picker)
|
||||
{
|
||||
if (picker != null)
|
||||
{
|
||||
picker.AnimController.Anim = AnimController.Animation.None;
|
||||
picker.PickingItem = null;
|
||||
}
|
||||
if (pickingCoroutine != null)
|
||||
{
|
||||
CoroutineManager.StopCoroutines(pickingCoroutine);
|
||||
pickingCoroutine = null;
|
||||
}
|
||||
activePicker = null;
|
||||
pickTimer = 0.0f;
|
||||
}
|
||||
|
||||
protected void DropConnectedWires(Character character)
|
||||
{
|
||||
Vector2 pos = character == null ? item.SimPosition : character.SimPosition;
|
||||
|
||||
foreach (ConnectionPanel connectionPanel in item.GetComponents<ConnectionPanel>())
|
||||
{
|
||||
foreach (Connection c in connectionPanel.Connections)
|
||||
{
|
||||
foreach (Wire w in c.Wires)
|
||||
{
|
||||
if (w == null) continue;
|
||||
w.Item.Drop(character);
|
||||
w.Item.SetTransform(pos, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Drop(Character dropper)
|
||||
{
|
||||
if (picker == null)
|
||||
{
|
||||
picker = dropper;
|
||||
}
|
||||
|
||||
Vector2 bodyDropPos = Vector2.Zero;
|
||||
|
||||
if (picker == null || picker.Inventory == null)
|
||||
{
|
||||
if (item.ParentInventory != null && item.ParentInventory.Owner != null && !item.ParentInventory.Owner.Removed)
|
||||
{
|
||||
bodyDropPos = item.ParentInventory.Owner.SimPosition;
|
||||
|
||||
if (item.body != null) item.body.ResetDynamics();
|
||||
}
|
||||
}
|
||||
else if (!picker.Removed)
|
||||
{
|
||||
DropConnectedWires(picker);
|
||||
|
||||
item.Submarine = picker.Submarine;
|
||||
bodyDropPos = picker.SimPosition;
|
||||
|
||||
picker.Inventory.RemoveItem(item);
|
||||
picker = null;
|
||||
}
|
||||
|
||||
if (item.body != null && !item.body.Enabled)
|
||||
{
|
||||
if (item.body.Removed)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
"Failed to drop the Pickable component of the item \"" + item.Name + "\" (body has been removed"
|
||||
+ (item.Removed ? ", item has been removed)" : ")"));
|
||||
}
|
||||
else
|
||||
{
|
||||
item.body.ResetDynamics();
|
||||
item.SetTransform(bodyDropPos, 0.0f);
|
||||
item.body.Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
|
||||
{
|
||||
msg.Write(activePicker == null ? (ushort)0 : activePicker.ID);
|
||||
}
|
||||
|
||||
public virtual void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
|
||||
{
|
||||
ushort pickerID = msg.ReadUInt16();
|
||||
if (pickerID == 0)
|
||||
{
|
||||
StopPicking(activePicker);
|
||||
}
|
||||
else
|
||||
{
|
||||
Pick(Entity.FindEntityByID(pickerID) as Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Networking;
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Barotrauma.Particles;
|
||||
#endif
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class Propulsion : ItemComponent
|
||||
{
|
||||
public enum UseEnvironment
|
||||
{
|
||||
Air, Water, Both
|
||||
};
|
||||
|
||||
private float useState;
|
||||
|
||||
[Serialize(UseEnvironment.Both, false, description: "Can the item be used in air, underwater or both.")]
|
||||
public UseEnvironment UsableIn { get; set; }
|
||||
|
||||
[Serialize(0.0f, false, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
|
||||
public float Force { get; set; }
|
||||
|
||||
#if CLIENT
|
||||
private string particles;
|
||||
[Serialize("", false, description: "The name of the particle prefab the item emits when used.")]
|
||||
public string Particles
|
||||
{
|
||||
get { return particles; }
|
||||
set { particles = value; }
|
||||
}
|
||||
#endif
|
||||
|
||||
public Propulsion(Item item, XElement element)
|
||||
: base(item,element)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (character == null || character.Removed) return false;
|
||||
if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) return false;
|
||||
|
||||
IsActive = true;
|
||||
useState = 0.1f;
|
||||
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
if (UsableIn == UseEnvironment.Air) return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (UsableIn == UseEnvironment.Water) return true;
|
||||
}
|
||||
|
||||
Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position);
|
||||
//move upwards if the cursor is at the position of the character
|
||||
if (!MathUtils.IsValid(dir)) dir = Vector2.UnitY;
|
||||
|
||||
Vector2 propulsion = dir * Force;
|
||||
|
||||
if (character.AnimController.InWater) character.AnimController.TargetMovement = dir;
|
||||
|
||||
foreach (Limb limb in character.AnimController.Limbs)
|
||||
{
|
||||
if (limb.WearingItems.Find(w => w.WearableComponent.Item == this.item) == null) continue;
|
||||
limb.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
|
||||
character.AnimController.Collider.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
|
||||
if (character.SelectedItems[0] == item)
|
||||
{
|
||||
character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
if (character.SelectedItems[1] == item)
|
||||
{
|
||||
character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (!string.IsNullOrWhiteSpace(particles))
|
||||
{
|
||||
GameMain.ParticleManager.CreateParticle(particles, item.WorldPosition,
|
||||
item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi), 0.0f, item.CurrentHull);
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
useState -= deltaTime;
|
||||
|
||||
if (useState <= 0.0f)
|
||||
{
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
if (item.AiTarget != null && IsActive)
|
||||
{
|
||||
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Collision;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class RangedWeapon : ItemComponent
|
||||
{
|
||||
private float reload, reloadTimer;
|
||||
|
||||
private Vector2 barrelPos;
|
||||
|
||||
[Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels). Determines where the projectiles spawn.")]
|
||||
public string BarrelPos
|
||||
{
|
||||
get { return XMLExtensions.Vector2ToString(ConvertUnits.ToDisplayUnits(barrelPos)); }
|
||||
set { barrelPos = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(value)); }
|
||||
}
|
||||
|
||||
[Serialize(1.0f, false, description: "How long the user has to wait before they can fire the weapon again (in seconds).")]
|
||||
public float Reload
|
||||
{
|
||||
get { return reload; }
|
||||
set { reload = Math.Max(value, 0.0f); }
|
||||
}
|
||||
|
||||
[Serialize(1, false, description: "How projectiles the weapon launches when fired once.")]
|
||||
public int ProjectileCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with sufficient skills to use the weapon (in degrees).")]
|
||||
public float Spread
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with insufficient skills to use the weapon (in degrees).")]
|
||||
public float UnskilledSpread
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Vector2 TransformedBarrelPos
|
||||
{
|
||||
get
|
||||
{
|
||||
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation);
|
||||
Vector2 flippedPos = barrelPos;
|
||||
if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X;
|
||||
return Vector2.Transform(flippedPos, bodyTransform);
|
||||
}
|
||||
}
|
||||
|
||||
public RangedWeapon(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
item.IsShootable = true;
|
||||
// TODO: should define this in xml if we have ranged weapons that don't require aim to use
|
||||
item.RequireAimToUse = true;
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
reloadTimer -= deltaTime;
|
||||
|
||||
if (reloadTimer < 0.0f)
|
||||
{
|
||||
reloadTimer = 0.0f;
|
||||
IsActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
private float GetSpread(Character user)
|
||||
{
|
||||
float degreeOfFailure = 1.0f - DegreeOfSuccess(user);
|
||||
degreeOfFailure *= degreeOfFailure;
|
||||
return MathHelper.ToRadians(MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure));
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (character == null || character.Removed) { return false; }
|
||||
if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || reloadTimer > 0.0f) { return false; }
|
||||
|
||||
IsActive = true;
|
||||
reloadTimer = reload;
|
||||
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
|
||||
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
|
||||
}
|
||||
|
||||
List<Body> limbBodies = new List<Body>();
|
||||
foreach (Limb l in character.AnimController.Limbs)
|
||||
{
|
||||
limbBodies.Add(l.body.FarseerBody);
|
||||
}
|
||||
|
||||
float degreeOfFailure = 1.0f - DegreeOfSuccess(character);
|
||||
degreeOfFailure *= degreeOfFailure;
|
||||
if (degreeOfFailure > Rand.Range(0.0f, 1.0f))
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
|
||||
}
|
||||
|
||||
for (int i = 0; i < ProjectileCount; i++)
|
||||
{
|
||||
Projectile projectile = FindProjectile(triggerOnUseOnContainers: true);
|
||||
if (projectile == null) { return true; }
|
||||
|
||||
float spread = GetSpread(character);
|
||||
float rotation = (item.body.Dir == 1.0f) ? item.body.Rotation : item.body.Rotation - MathHelper.Pi;
|
||||
rotation += spread * Rand.Range(-0.5f, 0.5f);
|
||||
|
||||
projectile.User = character;
|
||||
//add the limbs of the shooter to the list of bodies to be ignored
|
||||
//so that the player can't shoot himself
|
||||
projectile.IgnoredBodies = new List<Body>(limbBodies);
|
||||
|
||||
Vector2 projectilePos = item.SimPosition;
|
||||
Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos;
|
||||
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
|
||||
//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(sourcePos, barrelPos, projectile.IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null)
|
||||
{
|
||||
//no obstacles -> we can spawn the projectile at the barrel
|
||||
projectilePos = barrelPos;
|
||||
}
|
||||
else if ((sourcePos - barrelPos).LengthSquared() > 0.0001f)
|
||||
{
|
||||
//spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle
|
||||
projectilePos = sourcePos - Vector2.Normalize(barrelPos - projectilePos) * Math.Max(projectile.Item.body.GetMaxExtent(), 0.1f);
|
||||
}
|
||||
|
||||
projectile.Item.body.ResetDynamics();
|
||||
projectile.Item.SetTransform(projectilePos, rotation);
|
||||
|
||||
projectile.Use(deltaTime);
|
||||
if (projectile.Item.Removed) { continue; }
|
||||
projectile.User = character;
|
||||
|
||||
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f));
|
||||
|
||||
//set the rotation of the projectile again because dropping the projectile resets the rotation
|
||||
projectile.Item.SetTransform(projectilePos,
|
||||
rotation + (projectile.Item.body.Dir * projectile.LaunchRotationRadians));
|
||||
|
||||
item.RemoveContained(projectile.Item);
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
//recoil
|
||||
item.body.ApplyLinearImpulse(
|
||||
new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * item.body.Mass * -50.0f,
|
||||
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
LaunchProjSpecific();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Projectile FindProjectile(bool triggerOnUseOnContainers = false)
|
||||
{
|
||||
var containedItems = item.ContainedItems;
|
||||
if (containedItems == null) { return null; }
|
||||
|
||||
foreach (Item item in containedItems)
|
||||
{
|
||||
Projectile projectile = item.GetComponent<Projectile>();
|
||||
if (projectile != null) { return projectile; }
|
||||
}
|
||||
|
||||
//projectile not found, see if one of the contained items contains projectiles
|
||||
foreach (Item item in containedItems)
|
||||
{
|
||||
var containedSubItems = item.ContainedItems;
|
||||
if (containedSubItems == null) { continue; }
|
||||
foreach (Item subItem in containedSubItems)
|
||||
{
|
||||
Projectile projectile = subItem.GetComponent<Projectile>();
|
||||
//apply OnUse statuseffects to the container in case it has to react to it somehow
|
||||
//(play a sound, spawn more projectiles, reduce condition...)
|
||||
if (triggerOnUseOnContainers && subItem.Condition > 0.0f)
|
||||
{
|
||||
subItem.GetComponent<ItemContainer>()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f);
|
||||
}
|
||||
if (projectile != null) { return projectile; }
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
partial void LaunchProjSpecific();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,670 @@
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class RepairTool : ItemComponent
|
||||
{
|
||||
public enum UseEnvironment
|
||||
{
|
||||
Air, Water, Both, None
|
||||
};
|
||||
|
||||
private readonly List<string> fixableEntities;
|
||||
private Vector2 pickedPosition;
|
||||
private float activeTimer;
|
||||
|
||||
private Vector2 debugRayStartPos, debugRayEndPos;
|
||||
|
||||
[Serialize("Both", false, description: "Can the item be used in air, water or both.")]
|
||||
public UseEnvironment UsableIn
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false, description: "The distance at which the item can repair targets.")]
|
||||
public float Range { get; set; }
|
||||
|
||||
[Serialize(0.0f, false, description: "Random spread applied to the firing angle when used by a character with sufficient skills to use the tool (in degrees).")]
|
||||
public float Spread
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false, description: "Random spread applied to the firing angle when used by a character with insufficient skills to use the tool (in degrees).")]
|
||||
public float UnskilledSpread
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false, description: "How many units of damage the item removes from structures per second.")]
|
||||
public float StructureFixAmount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[Serialize(0.0f, false, description: "How much the item decreases the size of fires per second.")]
|
||||
public float ExtinguishAmount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels).")]
|
||||
public Vector2 BarrelPos { get; set; }
|
||||
|
||||
[Serialize(false, false, description: "Can the item repair things through walls.")]
|
||||
public bool RepairThroughWalls { get; set; }
|
||||
|
||||
[Serialize(false, false, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")]
|
||||
public bool RepairMultiple { get; set; }
|
||||
|
||||
[Serialize(false, false, description: "Can the item repair things through holes in walls.")]
|
||||
public bool RepairThroughHoles { get; set; }
|
||||
|
||||
[Serialize(0.0f, false, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")]
|
||||
public float FireProbability { get; set; }
|
||||
|
||||
[Serialize(0.0f, false, description: "Force applied to the entity the ray hits.")]
|
||||
public float TargetForce { get; set; }
|
||||
|
||||
public Vector2 TransformedBarrelPos
|
||||
{
|
||||
get
|
||||
{
|
||||
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation);
|
||||
Vector2 flippedPos = BarrelPos;
|
||||
if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X;
|
||||
return (Vector2.Transform(flippedPos, bodyTransform));
|
||||
}
|
||||
}
|
||||
|
||||
public RepairTool(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
this.item = item;
|
||||
|
||||
if (element.Attribute("limbfixamount") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute.");
|
||||
}
|
||||
|
||||
fixableEntities = new List<string>();
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "fixable":
|
||||
if (subElement.Attribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities.");
|
||||
fixableEntities.Add(subElement.Attribute("name").Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
fixableEntities.Add(subElement.GetAttributeString("identifier", ""));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
item.IsShootable = true;
|
||||
// TODO: should define this in xml if we have repair tools that don't require aim to use
|
||||
item.RequireAimToUse = true;
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
activeTimer -= deltaTime;
|
||||
if (activeTimer <= 0.0f) IsActive = false;
|
||||
}
|
||||
|
||||
private List<Body> ignoredBodies = new List<Body>();
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (character == null || character.Removed) return false;
|
||||
if (item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) return false;
|
||||
|
||||
float degreeOfSuccess = DegreeOfSuccess(character);
|
||||
|
||||
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (UsableIn == UseEnvironment.None)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.InWater)
|
||||
{
|
||||
if (UsableIn == UseEnvironment.Air)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (UsableIn == UseEnvironment.Water)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 rayStart;
|
||||
Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos;
|
||||
Vector2 barrelPos = item.SimPosition + ConvertUnits.ToSimUnits(TransformedBarrelPos);
|
||||
//make sure there's no obstacles between the base of the item (or the shoulder of the character) and the end of the barrel
|
||||
if (Submarine.PickBody(sourcePos, barrelPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null)
|
||||
{
|
||||
//no obstacles -> we start the raycast at the end of the barrel
|
||||
rayStart = ConvertUnits.ToSimUnits(item.WorldPosition + TransformedBarrelPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
rayStart = Submarine.LastPickedPosition + Submarine.LastPickedNormal * 0.1f;
|
||||
if (item.Submarine != null) { rayStart += item.Submarine.SimPosition; }
|
||||
}
|
||||
|
||||
float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess));
|
||||
float angle = item.body.Rotation + spread * Rand.Range(-0.5f, 0.5f);
|
||||
Vector2 rayEnd = rayStart +
|
||||
ConvertUnits.ToSimUnits(new Vector2(
|
||||
(float)Math.Cos(angle),
|
||||
(float)Math.Sin(angle)) * Range * item.body.Dir);
|
||||
|
||||
ignoredBodies.Clear();
|
||||
foreach (Limb limb in character.AnimController.Limbs)
|
||||
{
|
||||
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue;
|
||||
ignoredBodies.Add(limb.body.FarseerBody);
|
||||
}
|
||||
ignoredBodies.Add(character.AnimController.Collider.FarseerBody);
|
||||
|
||||
IsActive = true;
|
||||
activeTimer = 0.1f;
|
||||
|
||||
debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStart);
|
||||
debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd);
|
||||
|
||||
if (character.Submarine == null)
|
||||
{
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
{
|
||||
Rectangle subBorders = sub.Borders;
|
||||
subBorders.Location += new Point((int)sub.WorldPosition.X, (int)sub.WorldPosition.Y - sub.Borders.Height);
|
||||
if (!MathUtils.CircleIntersectsRectangle(item.WorldPosition, Range * 5.0f, subBorders))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Repair(rayStart - sub.SimPosition, rayEnd - sub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies);
|
||||
}
|
||||
Repair(rayStart, rayEnd, deltaTime, character, degreeOfSuccess, ignoredBodies);
|
||||
}
|
||||
else
|
||||
{
|
||||
Repair(rayStart - character.Submarine.SimPosition, rayEnd - character.Submarine.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies);
|
||||
}
|
||||
|
||||
UseProjSpecific(deltaTime, rayStart);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
partial void UseProjSpecific(float deltaTime, Vector2 raystart);
|
||||
|
||||
private readonly HashSet<Character> hitCharacters = new HashSet<Character>();
|
||||
private readonly List<FireSource> fireSourcesInRange = new List<FireSource>();
|
||||
private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies)
|
||||
{
|
||||
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair;
|
||||
|
||||
//if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them
|
||||
if (statusEffectLists != null && statusEffectLists.ContainsKey(ActionType.OnUse))
|
||||
{
|
||||
if (statusEffectLists[ActionType.OnUse].Any(s => s.SeverLimbsProbability > 0.0f))
|
||||
{
|
||||
float rangeSqr = ConvertUnits.ToSimUnits(Range);
|
||||
rangeSqr *= rangeSqr;
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!c.Enabled || !c.AnimController.BodyInRest) { continue; }
|
||||
//do a broad check first
|
||||
if (Math.Abs(c.WorldPosition.X - item.WorldPosition.X) > 1000.0f) { continue; }
|
||||
if (Math.Abs(c.WorldPosition.Y - item.WorldPosition.Y) > 1000.0f) { continue; }
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
{
|
||||
if (Vector2.DistanceSquared(limb.SimPosition, item.SimPosition) < rangeSqr && Vector2.Dot(rayEnd - rayStart, limb.SimPosition - rayStart) > 0)
|
||||
{
|
||||
c.AnimController.BodyInRest = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float lastPickedFraction = 0.0f;
|
||||
if (RepairMultiple)
|
||||
{
|
||||
var bodies = Submarine.PickBodies(rayStart, rayEnd, ignoredBodies, collisionCategories,
|
||||
ignoreSensors: false,
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; }
|
||||
if (f.Body?.UserData as string == "ruinroom") { return false; }
|
||||
return true;
|
||||
},
|
||||
allowInsideFixture: true);
|
||||
lastPickedFraction = Submarine.LastPickedFraction;
|
||||
Type lastHitType = null;
|
||||
hitCharacters.Clear();
|
||||
foreach (Body body in bodies)
|
||||
{
|
||||
Type bodyType = body.UserData?.GetType();
|
||||
if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType)
|
||||
{
|
||||
//stop the ray if it already hit a door/wall and is now about to hit some other type of entity
|
||||
if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; }
|
||||
}
|
||||
|
||||
Character hitCharacter = null;
|
||||
if (body.UserData is Limb limb)
|
||||
{
|
||||
hitCharacter = limb.character;
|
||||
}
|
||||
else if (body.UserData is Character character)
|
||||
{
|
||||
hitCharacter = character;
|
||||
}
|
||||
//only do damage once to each character even if they ray hit multiple limbs
|
||||
if (hitCharacter != null)
|
||||
{
|
||||
if (hitCharacters.Contains(hitCharacter)) { continue; }
|
||||
hitCharacters.Add(hitCharacter);
|
||||
}
|
||||
|
||||
if (FixBody(user, deltaTime, degreeOfSuccess, body))
|
||||
{
|
||||
lastPickedFraction = Submarine.LastPickedBodyDist(body);
|
||||
if (bodyType != null) { lastHitType = bodyType; }
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FixBody(user, deltaTime, degreeOfSuccess,
|
||||
Submarine.PickBody(rayStart, rayEnd,
|
||||
ignoredBodies, collisionCategories,
|
||||
ignoreSensors: false,
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; }
|
||||
if (f.Body?.UserData as string == "ruinroom") { return false; }
|
||||
return f.Body?.UserData != null;
|
||||
},
|
||||
allowInsideFixture: true));
|
||||
lastPickedFraction = Submarine.LastPickedFraction;
|
||||
}
|
||||
|
||||
if (ExtinguishAmount > 0.0f && item.CurrentHull != null)
|
||||
{
|
||||
fireSourcesInRange.Clear();
|
||||
//step along the ray in 10% intervals, collecting all fire sources in the range
|
||||
for (float x = 0.0f; x <= lastPickedFraction; x += 0.1f)
|
||||
{
|
||||
Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * x);
|
||||
if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; }
|
||||
|
||||
Hull hull = Hull.FindHull(displayPos, item.CurrentHull);
|
||||
if (hull == null) continue;
|
||||
foreach (FireSource fs in hull.FireSources)
|
||||
{
|
||||
if (fs.IsInDamageRange(displayPos, 100.0f) && !fireSourcesInRange.Contains(fs))
|
||||
{
|
||||
fireSourcesInRange.Add(fs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (FireSource fs in fireSourcesInRange)
|
||||
{
|
||||
fs.Extinguish(deltaTime, ExtinguishAmount);
|
||||
#if SERVER
|
||||
GameMain.Server.KarmaManager.OnExtinguishingFire(user, deltaTime);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
if (Rand.Range(0.0f, 1.0f) < FireProbability * deltaTime)
|
||||
{
|
||||
Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * lastPickedFraction * 0.9f);
|
||||
if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; }
|
||||
new FireSource(displayPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool FixBody(Character user, float deltaTime, float degreeOfSuccess, Body targetBody)
|
||||
{
|
||||
if (targetBody?.UserData == null) { return false; }
|
||||
|
||||
pickedPosition = Submarine.LastPickedPosition;
|
||||
|
||||
if (targetBody.UserData is Structure targetStructure)
|
||||
{
|
||||
if (targetStructure.IsPlatform) { return false; }
|
||||
int sectionIndex = targetStructure.FindSectionIndex(ConvertUnits.ToDisplayUnits(pickedPosition));
|
||||
if (sectionIndex < 0) { return false; }
|
||||
|
||||
if (!fixableEntities.Contains("structure") && !fixableEntities.Contains(targetStructure.Prefab.Identifier)) { return true; }
|
||||
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetStructure });
|
||||
FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex);
|
||||
targetStructure.AddDamage(sectionIndex, -StructureFixAmount * degreeOfSuccess, user);
|
||||
|
||||
//if the next section is small enough, apply the effect to it as well
|
||||
//(to make it easier to fix a small "left-over" section)
|
||||
for (int i = -1; i < 2; i += 2)
|
||||
{
|
||||
int nextSectionLength = targetStructure.SectionLength(sectionIndex + i);
|
||||
if ((sectionIndex == 1 && i == -1) ||
|
||||
(sectionIndex == targetStructure.SectionCount - 2 && i == 1) ||
|
||||
(nextSectionLength > 0 && nextSectionLength < Structure.WallSectionSize * 0.3f))
|
||||
{
|
||||
//targetStructure.HighLightSection(sectionIndex + i);
|
||||
targetStructure.AddDamage(sectionIndex + i, -StructureFixAmount * degreeOfSuccess);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (targetBody.UserData is Character targetCharacter)
|
||||
{
|
||||
if (targetCharacter.Removed) { return false; }
|
||||
targetCharacter.LastDamageSource = item;
|
||||
Limb closestLimb = null;
|
||||
float closestDist = float.MaxValue;
|
||||
foreach (Limb limb in targetCharacter.AnimController.Limbs)
|
||||
{
|
||||
float dist = Vector2.DistanceSquared(item.SimPosition, limb.SimPosition);
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestLimb = limb;
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestLimb != null && !MathUtils.NearlyEqual(TargetForce, 0.0f))
|
||||
{
|
||||
Vector2 dir = closestLimb.WorldPosition - item.WorldPosition;
|
||||
dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir);
|
||||
closestLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
|
||||
}
|
||||
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse,
|
||||
closestLimb == null ? new ISerializableEntity[] { targetCharacter } : new ISerializableEntity[] { targetCharacter, closestLimb });
|
||||
FixCharacterProjSpecific(user, deltaTime, targetCharacter);
|
||||
return true;
|
||||
}
|
||||
else if (targetBody.UserData is Limb targetLimb)
|
||||
{
|
||||
if (targetLimb.character == null || targetLimb.character.Removed) { return false; }
|
||||
|
||||
if (!MathUtils.NearlyEqual(TargetForce, 0.0f))
|
||||
{
|
||||
Vector2 dir = targetLimb.WorldPosition - item.WorldPosition;
|
||||
dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir);
|
||||
targetLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
|
||||
}
|
||||
|
||||
targetLimb.character.LastDamageSource = item;
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetLimb.character, targetLimb });
|
||||
FixCharacterProjSpecific(user, deltaTime, targetLimb.character);
|
||||
return true;
|
||||
}
|
||||
else if (targetBody.UserData is Item targetItem)
|
||||
{
|
||||
targetItem.IsHighlighted = true;
|
||||
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects);
|
||||
|
||||
if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f))
|
||||
{
|
||||
Vector2 dir = targetItem.WorldPosition - item.WorldPosition;
|
||||
dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir);
|
||||
targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
|
||||
}
|
||||
|
||||
var levelResource = targetItem.GetComponent<LevelResource>();
|
||||
if (levelResource != null && levelResource.Attached &&
|
||||
levelResource.requiredItems.Any() &&
|
||||
levelResource.HasRequiredItems(user, addMessage: false))
|
||||
{
|
||||
levelResource.DeattachTimer += deltaTime;
|
||||
#if CLIENT
|
||||
Character.Controlled?.UpdateHUDProgressBar(
|
||||
this,
|
||||
targetItem.WorldPosition,
|
||||
levelResource.DeattachTimer / levelResource.DeattachDuration,
|
||||
GUI.Style.Red, GUI.Style.Green);
|
||||
#endif
|
||||
}
|
||||
FixItemProjSpecific(user, deltaTime, targetItem);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
partial void FixStructureProjSpecific(Character user, float deltaTime, Structure targetStructure, int sectionIndex);
|
||||
partial void FixCharacterProjSpecific(Character user, float deltaTime, Character targetCharacter);
|
||||
partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem);
|
||||
|
||||
private float sinTime;
|
||||
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
if (!(objective.OperateTarget is Gap leak)) { return true; }
|
||||
if (leak.Submarine == null) { return true; }
|
||||
Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition;
|
||||
float dist = fromCharacterToLeak.Length();
|
||||
float reach = Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
|
||||
|
||||
//too far away -> consider this done and hope the AI is smart enough to move closer
|
||||
if (dist > reach * 2) { return true; }
|
||||
character.AIController.SteeringManager.Reset();
|
||||
//steer closer if almost in range
|
||||
if (dist > reach)
|
||||
{
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
|
||||
{
|
||||
// Swimming inside the sub
|
||||
if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && indoorSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
Vector2 dir = Vector2.Normalize(fromCharacterToLeak);
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, dir);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Swimming outside the sub
|
||||
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: use the collider size?
|
||||
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
|
||||
Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f)
|
||||
{
|
||||
((HumanoidAnimController)character.AnimController).Crouching = true;
|
||||
}
|
||||
Vector2 standPos = new Vector2(Math.Sign(-fromCharacterToLeak.X), Math.Sign(-fromCharacterToLeak.Y)) / 2;
|
||||
if (leak.IsHorizontal)
|
||||
{
|
||||
standPos.X *= 2;
|
||||
standPos.Y = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
standPos.X = 0;
|
||||
}
|
||||
character.AIController.SteeringManager.SteeringSeek(standPos);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dist < reach / 2)
|
||||
{
|
||||
// Too close -> steer away
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
|
||||
}
|
||||
else if (dist <= reach)
|
||||
{
|
||||
// In range
|
||||
character.CursorPosition = leak.Position;
|
||||
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
var torso = character.AnimController.GetLimb(LimbType.Torso);
|
||||
// Turn facing the target when not moving (handled in the animcontroller if not moving)
|
||||
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
|
||||
Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
|
||||
float newRotation = MathUtils.VectorToAngle(diff);
|
||||
character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
|
||||
|
||||
if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
|
||||
{
|
||||
// Swim past
|
||||
Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
|
||||
moveDir *= character.AnimController.Dir;
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.RequireAimToUse)
|
||||
{
|
||||
bool isOperatingButtons = false;
|
||||
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
|
||||
{
|
||||
var door = indoorSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
|
||||
if (door != null && !door.IsOpen)
|
||||
{
|
||||
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
|
||||
}
|
||||
}
|
||||
if (!isOperatingButtons)
|
||||
{
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
}
|
||||
sinTime += deltaTime * 5;
|
||||
}
|
||||
// Press the trigger only when the tool is approximately facing the target.
|
||||
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
|
||||
var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
|
||||
if (angle < MathHelper.PiOver4)
|
||||
{
|
||||
// Check that we don't hit any friendlies
|
||||
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
|
||||
{
|
||||
if (hit.UserData is Character c)
|
||||
{
|
||||
if (c == character) { return false; }
|
||||
return HumanAIController.IsFriendly(character, c);
|
||||
}
|
||||
return false;
|
||||
}))
|
||||
{
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Use(deltaTime, character);
|
||||
}
|
||||
}
|
||||
|
||||
bool leakFixed = (leak.Open <= 0.0f || leak.Removed) &&
|
||||
(leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1);
|
||||
|
||||
if (leakFixed && leak.FlowTargetHull != null)
|
||||
{
|
||||
if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f))
|
||||
{
|
||||
|
||||
character.Speak(TextManager.GetWithVariable("DialogLeaksFixed", "[roomname]", leak.FlowTargetHull.DisplayName, true), null, 0.0f, "leaksfixed", 10.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogLeakFixed", "[roomname]", leak.FlowTargetHull.DisplayName, true), null, 0.0f, "leakfixed", 10.0f);
|
||||
}
|
||||
}
|
||||
|
||||
return leakFixed;
|
||||
}
|
||||
|
||||
private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, IEnumerable<ISerializableEntity> targets)
|
||||
{
|
||||
if (statusEffectLists == null) { return; }
|
||||
if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffects)) { return; }
|
||||
|
||||
foreach (StatusEffect effect in statusEffects)
|
||||
{
|
||||
effect.SetUser(user);
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
effect.Apply(actionType, deltaTime, item, targets);
|
||||
}
|
||||
else if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Character));
|
||||
}
|
||||
else if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Limb));
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
// Hard-coded progress bars for welding doors stuck.
|
||||
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
|
||||
foreach (ISerializableEntity target in targets)
|
||||
{
|
||||
if (target is Door door)
|
||||
{
|
||||
if (!door.CanBeWelded) continue;
|
||||
for (int i = 0; i < effect.propertyNames.Length; i++)
|
||||
{
|
||||
string propertyName = effect.propertyNames[i];
|
||||
if (propertyName != "stuck") { continue; }
|
||||
if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; }
|
||||
object value = property.GetValue(target);
|
||||
if (door.Stuck > 0)
|
||||
{
|
||||
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White);
|
||||
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class Throwable : Holdable
|
||||
{
|
||||
private float throwForce, throwPos;
|
||||
private bool throwing, throwDone;
|
||||
|
||||
private bool midAir;
|
||||
|
||||
[Serialize(1.0f, false, 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 { return throwForce; }
|
||||
set { throwForce = value; }
|
||||
}
|
||||
|
||||
public Throwable(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
//throwForce = ToolBox.GetAttributeFloat(element, "throwforce", 1.0f);
|
||||
if (aimPos == Vector2.Zero)
|
||||
{
|
||||
aimPos = new Vector2(0.6f, 0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
return characterUsable || character == null; //We do the actual throwing in Aim because Use might be used by chems
|
||||
}
|
||||
|
||||
public override bool SecondaryUse(float deltaTime, Character character = null)
|
||||
{
|
||||
if (!throwDone) return false; //This should only be triggered in update
|
||||
throwDone = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Drop(Character dropper)
|
||||
{
|
||||
base.Drop(dropper);
|
||||
|
||||
throwing = false;
|
||||
throwPos = 0.0f;
|
||||
}
|
||||
|
||||
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.LinearVelocity.LengthSquared() < 0.01f)
|
||||
{
|
||||
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform;
|
||||
midAir = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (picker == null || picker.Removed || !picker.HasSelectedItem(item))
|
||||
{
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (picker.IsKeyDown(InputType.Aim) && picker.IsKeyHit(InputType.Shoot)) { throwing = true; }
|
||||
if (!picker.IsKeyDown(InputType.Aim) && !throwing) { throwPos = 0.0f; }
|
||||
bool aim = picker.IsKeyDown(InputType.Aim) && (picker.SelectedConstruction == null || picker.SelectedConstruction.GetComponent<Ladder>() != null);
|
||||
|
||||
if (picker.IsUnconscious || picker.IsDead || !picker.AllowInput)
|
||||
{
|
||||
throwing = false;
|
||||
aim = false;
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
|
||||
|
||||
if (item.body.Dir != picker.AnimController.Dir) { Flip(); }
|
||||
|
||||
AnimController ac = picker.AnimController;
|
||||
|
||||
item.Submarine = picker.Submarine;
|
||||
|
||||
if (!throwing)
|
||||
{
|
||||
if (aim)
|
||||
{
|
||||
throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2));
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
throwPos = 0;
|
||||
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f);
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos);
|
||||
|
||||
if (throwPos < 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(picker.LogName + " threw " + item.Name, ServerLog.MessageType.ItemInteraction);
|
||||
#endif
|
||||
Character thrower = picker;
|
||||
item.Drop(thrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer);
|
||||
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;
|
||||
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;
|
||||
throwPos = 0;
|
||||
throwDone = true;
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, thrower.ID });
|
||||
}
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
|
||||
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, thrower, user: thrower);
|
||||
}
|
||||
throwing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user