494 lines
18 KiB
C#
494 lines
18 KiB
C#
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
#if CLIENT
|
|
using Barotrauma.Lights;
|
|
#endif
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
partial class Door : Pickable, IDrawableComponent, IServerSerializable
|
|
{
|
|
private Gap linkedGap;
|
|
private bool isOpen;
|
|
|
|
private float openState;
|
|
private Sprite doorSprite, weldedSprite, brokenSprite;
|
|
private bool scaleBrokenSprite, fadeBrokenSprite;
|
|
private bool createdNewGap;
|
|
private bool autoOrientGap;
|
|
|
|
private bool isStuck;
|
|
private float resetPredictionTimer;
|
|
|
|
private Rectangle doorRect;
|
|
|
|
private bool isBroken;
|
|
|
|
public bool IsBroken
|
|
{
|
|
get { return isBroken; }
|
|
set
|
|
{
|
|
if (isBroken == value) return;
|
|
isBroken = value;
|
|
if (isBroken)
|
|
{
|
|
DisableBody();
|
|
}
|
|
else
|
|
{
|
|
EnableBody();
|
|
}
|
|
}
|
|
}
|
|
|
|
public PhysicsBody Body { get; private set; }
|
|
|
|
private float RepairThreshold
|
|
{
|
|
get { return item.GetComponent<Repairable>()?.ShowRepairUIThreshold ?? 0.0f; }
|
|
}
|
|
|
|
private float stuck;
|
|
[Serialize(0.0f, false)]
|
|
public float Stuck
|
|
{
|
|
get { return stuck; }
|
|
set
|
|
{
|
|
if (isOpen || isBroken) return;
|
|
stuck = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
if (stuck <= 0.0f) isStuck = false;
|
|
if (stuck >= 100.0f) isStuck = true;
|
|
}
|
|
}
|
|
|
|
public bool? PredictedState { get; private set; }
|
|
|
|
public Gap LinkedGap
|
|
{
|
|
get
|
|
{
|
|
if (linkedGap != null) return linkedGap;
|
|
|
|
foreach (MapEntity e in item.linkedTo)
|
|
{
|
|
linkedGap = e as Gap;
|
|
if (linkedGap != null)
|
|
{
|
|
linkedGap.PassAmbientLight = Window != Rectangle.Empty;
|
|
return linkedGap;
|
|
}
|
|
}
|
|
Rectangle rect = item.Rect;
|
|
if (IsHorizontal)
|
|
{
|
|
rect.Y += 5;
|
|
rect.Height += 10;
|
|
}
|
|
else
|
|
{
|
|
rect.X -= 5;
|
|
rect.Width += 10;
|
|
}
|
|
|
|
linkedGap = new Gap(rect, !IsHorizontal, Item.Submarine)
|
|
{
|
|
Submarine = item.Submarine,
|
|
PassAmbientLight = Window != Rectangle.Empty,
|
|
Open = openState
|
|
};
|
|
item.linkedTo.Add(linkedGap);
|
|
createdNewGap = true;
|
|
return linkedGap;
|
|
}
|
|
}
|
|
|
|
public bool IsHorizontal { get; private set; }
|
|
|
|
[Serialize("0.0,0.0,0.0,0.0", false)]
|
|
public Rectangle Window { get; set; }
|
|
|
|
[Editable, Serialize(false, true)]
|
|
public bool IsOpen
|
|
{
|
|
get { return isOpen; }
|
|
set
|
|
{
|
|
isOpen = value;
|
|
OpenState = (isOpen) ? 1.0f : 0.0f;
|
|
}
|
|
}
|
|
|
|
public float OpenState
|
|
{
|
|
get { return openState; }
|
|
set
|
|
{
|
|
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
|
|
#if CLIENT
|
|
float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
|
|
if (Math.Abs(lastConvexHullState - openState) * size < 5.0f) { return; }
|
|
UpdateConvexHulls();
|
|
lastConvexHullState = openState;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
[Serialize(false, false)]
|
|
public bool Impassable
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public Door(Item item, XElement element)
|
|
: base(item, element)
|
|
{
|
|
IsHorizontal = element.GetAttributeBool("horizontal", false);
|
|
canBePicked = element.GetAttributeBool("canbepicked", false);
|
|
autoOrientGap = element.GetAttributeBool("autoorientgap", false);
|
|
|
|
foreach (XElement subElement in element.Elements())
|
|
{
|
|
string texturePath = subElement.GetAttributeString("texture", "");
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "sprite":
|
|
doorSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.ConfigFile));
|
|
break;
|
|
case "weldedsprite":
|
|
weldedSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.ConfigFile));
|
|
break;
|
|
case "brokensprite":
|
|
brokenSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.ConfigFile));
|
|
scaleBrokenSprite = subElement.GetAttributeBool("scale", false);
|
|
fadeBrokenSprite = subElement.GetAttributeBool("fade", false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
doorRect = new Rectangle(
|
|
item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale),
|
|
item.Rect.Y - item.Rect.Height/2 + (int)(doorSprite.size.Y / 2.0f * item.Scale),
|
|
(int)(doorSprite.size.X * item.Scale),
|
|
(int)(doorSprite.size.Y * item.Scale));
|
|
|
|
Body = new PhysicsBody(
|
|
ConvertUnits.ToSimUnits(Math.Max(doorRect.Width, 1)),
|
|
ConvertUnits.ToSimUnits(Math.Max(doorRect.Height, 1)),
|
|
0.0f,
|
|
1.5f)
|
|
{
|
|
UserData = item,
|
|
CollisionCategories = Physics.CollisionWall,
|
|
BodyType = BodyType.Static,
|
|
Friction = 0.5f
|
|
};
|
|
Body.SetTransform(
|
|
ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)),
|
|
0.0f);
|
|
|
|
IsActive = true;
|
|
}
|
|
|
|
public override void Move(Vector2 amount)
|
|
{
|
|
base.Move(amount);
|
|
|
|
Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
|
|
|
|
#if CLIENT
|
|
UpdateConvexHulls();
|
|
#endif
|
|
}
|
|
|
|
public override bool HasRequiredItems(Character character, bool addMessage)
|
|
{
|
|
if (item.Condition <= RepairThreshold) return true; //For repairing
|
|
|
|
//this is a bit pointless atm because if canBePicked is false it won't allow you to do Pick() anyway, however it's still good for future-proofing.
|
|
return requiredItems.Any() ? base.HasRequiredItems(character, addMessage) : canBePicked;
|
|
}
|
|
|
|
public override bool Pick(Character picker)
|
|
{
|
|
return item.Condition <= RepairThreshold ? true : base.Pick(picker);
|
|
}
|
|
|
|
public override bool OnPicked(Character picker)
|
|
{
|
|
if (item.Condition <= RepairThreshold) return true; //repairs
|
|
|
|
SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true); //crowbar function
|
|
#if CLIENT
|
|
PlaySound(ActionType.OnPicked, item.WorldPosition, picker);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
public override bool Select(Character character)
|
|
{
|
|
//can only be selected if the item is broken
|
|
return item.Condition <= RepairThreshold;
|
|
}
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
if (isBroken)
|
|
{
|
|
//the door has to be restored to 50% health before collision detection on the body is re-enabled
|
|
if (item.ConditionPercentage > 50.0f)
|
|
{
|
|
IsBroken = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool isClosing = false;
|
|
if (!isStuck)
|
|
{
|
|
if (PredictedState == null)
|
|
{
|
|
OpenState += deltaTime * (isOpen ? 2.0f : -2.0f);
|
|
isClosing = openState > 0.0f && openState < 1.0f && !isOpen;
|
|
}
|
|
else
|
|
{
|
|
OpenState += deltaTime * ((bool)PredictedState ? 2.0f : -2.0f);
|
|
isClosing = openState > 0.0f && openState < 1.0f && !(bool)PredictedState;
|
|
|
|
resetPredictionTimer -= deltaTime;
|
|
if (resetPredictionTimer <= 0.0f)
|
|
{
|
|
PredictedState = null;
|
|
}
|
|
}
|
|
|
|
LinkedGap.Open = openState;
|
|
}
|
|
|
|
if (isClosing)
|
|
{
|
|
PushCharactersAway();
|
|
}
|
|
else
|
|
{
|
|
Body.Enabled = Impassable || openState < 1.0f;
|
|
}
|
|
|
|
//don't use the predicted state here, because it might set
|
|
//other items to an incorrect state if the prediction is wrong
|
|
item.SendSignal(0, (isOpen) ? "1" : "0", "state_out", null);
|
|
}
|
|
|
|
public override void UpdateBroken(float deltaTime, Camera cam)
|
|
{
|
|
IsBroken = true;
|
|
}
|
|
|
|
private void EnableBody()
|
|
{
|
|
if (!Impassable)
|
|
{
|
|
Body.FarseerBody.IsSensor = false;
|
|
}
|
|
#if CLIENT
|
|
UpdateConvexHulls();
|
|
#endif
|
|
isBroken = false;
|
|
}
|
|
|
|
private void DisableBody()
|
|
{
|
|
//change the body to a sensor instead of disabling it completely,
|
|
//because otherwise repairtool raycasts won't hit it
|
|
if (!Impassable)
|
|
{
|
|
Body.FarseerBody.IsSensor = true;
|
|
}
|
|
linkedGap.Open = 1.0f;
|
|
IsOpen = false;
|
|
#if CLIENT
|
|
if (convexHull != null) convexHull.Enabled = false;
|
|
if (convexHull2 != null) convexHull2.Enabled = false;
|
|
#endif
|
|
}
|
|
|
|
public override void OnMapLoaded()
|
|
{
|
|
LinkedGap.ConnectedDoor = this;
|
|
LinkedGap.Open = openState;
|
|
if (createdNewGap && autoOrientGap) linkedGap.AutoOrient();
|
|
|
|
#if CLIENT
|
|
Vector2[] corners = GetConvexHullCorners(Rectangle.Empty);
|
|
|
|
convexHull = new ConvexHull(corners, Color.Black, item);
|
|
if (Window != Rectangle.Empty) convexHull2 = new ConvexHull(corners, Color.Black, item);
|
|
|
|
UpdateConvexHulls();
|
|
#endif
|
|
}
|
|
|
|
protected override void RemoveComponentSpecific()
|
|
{
|
|
base.RemoveComponentSpecific();
|
|
|
|
if (Body != null)
|
|
{
|
|
Body.Remove();
|
|
Body = null;
|
|
}
|
|
|
|
//no need to remove the gap if we're unloading the whole submarine
|
|
//otherwise the gap will be removed twice and cause console warnings
|
|
if (!Submarine.Unloading)
|
|
{
|
|
if (linkedGap != null) linkedGap.Remove();
|
|
}
|
|
|
|
doorSprite.Remove();
|
|
if (weldedSprite != null) weldedSprite.Remove();
|
|
|
|
#if CLIENT
|
|
if (convexHull != null) convexHull.Remove();
|
|
if (convexHull2 != null) convexHull2.Remove();
|
|
#endif
|
|
}
|
|
|
|
private void PushCharactersAway()
|
|
{
|
|
if (!MathUtils.IsValid(item.SimPosition))
|
|
{
|
|
DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ")");
|
|
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:DoorPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
|
"Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ").");
|
|
return;
|
|
}
|
|
|
|
//push characters out of the doorway when the door is closing/opening
|
|
Vector2 simPos = ConvertUnits.ToSimUnits(new Vector2(item.Rect.X, item.Rect.Y));
|
|
|
|
Vector2 currSize = IsHorizontal ?
|
|
new Vector2(item.Rect.Width * (1.0f - openState), doorSprite.size.Y * item.Scale) :
|
|
new Vector2(doorSprite.size.X * item.Scale, item.Rect.Height * (1.0f - openState));
|
|
|
|
Vector2 simSize = ConvertUnits.ToSimUnits(currSize);
|
|
|
|
foreach (Character c in Character.CharacterList)
|
|
{
|
|
if (!c.Enabled) continue;
|
|
if (!MathUtils.IsValid(c.SimPosition))
|
|
{
|
|
DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")");
|
|
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:CharacterPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
|
"Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")." +
|
|
" Removed: " + c.Removed +
|
|
" Remoteplayer: " + c.IsRemotePlayer);
|
|
continue;
|
|
}
|
|
int dir = IsHorizontal ? Math.Sign(c.SimPosition.Y - item.SimPosition.Y) : Math.Sign(c.SimPosition.X - item.SimPosition.X);
|
|
|
|
List<PhysicsBody> bodies = c.AnimController.Limbs.Select(l => l.body).ToList();
|
|
bodies.Add(c.AnimController.Collider);
|
|
|
|
foreach (PhysicsBody body in bodies)
|
|
{
|
|
float diff = 0.0f;
|
|
if (!MathUtils.IsValid(body.SimPosition))
|
|
{
|
|
DebugConsole.ThrowError("Failed to push a limb out of a doorway - position of the body (character \"" + c.Name + "\") is not valid (" + body.SimPosition + ")");
|
|
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:LimbPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
|
"Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + body.SimPosition + ")." +
|
|
" Removed: " + c.Removed +
|
|
" Remoteplayer: " + c.IsRemotePlayer);
|
|
continue;
|
|
}
|
|
|
|
if (IsHorizontal)
|
|
{
|
|
if (body.SimPosition.X < simPos.X || body.SimPosition.X > simPos.X + simSize.X) continue;
|
|
diff = body.SimPosition.Y - item.SimPosition.Y;
|
|
}
|
|
else
|
|
{
|
|
if (body.SimPosition.Y > simPos.Y || body.SimPosition.Y < simPos.Y - simSize.Y) continue;
|
|
diff = body.SimPosition.X - item.SimPosition.X;
|
|
}
|
|
|
|
if (Math.Sign(diff) != dir)
|
|
{
|
|
#if CLIENT
|
|
SoundPlayer.PlayDamageSound("LimbBlunt", 1.0f, body);
|
|
#endif
|
|
|
|
if (IsHorizontal)
|
|
{
|
|
body.SetTransform(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * simSize.Y * 2.0f), body.Rotation);
|
|
body.ApplyLinearImpulse(new Vector2(isOpen ? 0.0f : 1.0f, dir * 2.0f));
|
|
}
|
|
else
|
|
{
|
|
body.SetTransform(new Vector2(item.SimPosition.X + dir * simSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
|
|
body.ApplyLinearImpulse(new Vector2(dir * 0.5f, isOpen ? 0.0f : -1.0f));
|
|
}
|
|
}
|
|
|
|
if (IsHorizontal)
|
|
{
|
|
if (Math.Abs(body.SimPosition.Y - item.SimPosition.Y) > simSize.Y * 0.5f) continue;
|
|
|
|
body.ApplyLinearImpulse(new Vector2(isOpen ? 0.0f : 1.0f, dir * 0.5f));
|
|
}
|
|
else
|
|
{
|
|
if (Math.Abs(body.SimPosition.X - item.SimPosition.X) > simSize.X * 0.5f) continue;
|
|
|
|
body.ApplyLinearImpulse(new Vector2(dir * 0.5f, isOpen ? 0.0f : -1.0f));
|
|
}
|
|
|
|
c.SetStun(0.2f);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
|
{
|
|
if (isStuck) return;
|
|
|
|
bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value;
|
|
|
|
if (connection.Name == "toggle")
|
|
{
|
|
SetState(!wasOpen, false, true);
|
|
}
|
|
else if (connection.Name == "set_state")
|
|
{
|
|
SetState(signal != "0", false, true);
|
|
}
|
|
|
|
#if SERVER
|
|
if (sender != null && wasOpen != isOpen)
|
|
{
|
|
GameServer.Log(sender.LogName + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public void TrySetState(bool open, bool isNetworkMessage, bool sendNetworkMessage = false)
|
|
{
|
|
SetState(open, isNetworkMessage, sendNetworkMessage);
|
|
}
|
|
|
|
partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage);
|
|
}
|
|
}
|