869 lines
34 KiB
C#
869 lines
34 KiB
C#
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using FarseerPhysics.Dynamics;
|
|
#if CLIENT
|
|
using Barotrauma.Lights;
|
|
#endif
|
|
using Barotrauma.Extensions;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
partial class Door : Pickable, IDrawableComponent, IServerSerializable
|
|
{
|
|
private static readonly HashSet<Door> doorList = new HashSet<Door>();
|
|
|
|
public static IReadOnlyCollection<Door> DoorList { get { return doorList; } }
|
|
|
|
private Gap linkedGap;
|
|
private bool isOpen;
|
|
|
|
private float openState, lastOpenState;
|
|
private readonly Sprite doorSprite, weldedSprite, brokenSprite;
|
|
private readonly bool scaleBrokenSprite, fadeBrokenSprite;
|
|
private readonly bool autoOrientGap;
|
|
|
|
private bool isJammed;
|
|
public bool IsJammed
|
|
{
|
|
get { return isJammed; }
|
|
set
|
|
{
|
|
if (isJammed == value) { return; }
|
|
isJammed = value;
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private bool isStuck;
|
|
|
|
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
|
public bool IsStuck
|
|
{
|
|
get { return isStuck; }
|
|
private set
|
|
{
|
|
if (isStuck == value) { return; }
|
|
isStuck = value;
|
|
#if SERVER
|
|
if (item.FullyInitialized)
|
|
{
|
|
item.CreateServerEvent(this);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public bool IgnoreSignals { get; private set; }
|
|
|
|
//how much "less stuck" partially doors get when opened
|
|
const float StuckReductionOnOpen = 30.0f;
|
|
|
|
private float resetPredictionTimer;
|
|
private float toggleCooldownTimer;
|
|
private Character lastUser;
|
|
|
|
private float damageSoundCooldown;
|
|
|
|
private double lastBrokenTime;
|
|
|
|
private Rectangle doorRect;
|
|
|
|
private bool isBroken;
|
|
|
|
public bool CanBeTraversed => !Impassable && (IsBroken || IsOpen);
|
|
|
|
public bool IsBroken
|
|
{
|
|
get { return isBroken; }
|
|
set
|
|
{
|
|
if (isBroken == value) { return; }
|
|
isBroken = value;
|
|
if (isBroken)
|
|
{
|
|
DisableBody();
|
|
}
|
|
else
|
|
{
|
|
EnableBody();
|
|
}
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public PhysicsBody Body { get; private set; }
|
|
|
|
//the fixture that's part of the submarine's collider (= fixture that things outside the sub can collide with if the door is outside hulls)
|
|
public Fixture OutsideSubmarineFixture;
|
|
|
|
private float RepairThreshold
|
|
{
|
|
get { return item.GetComponent<Repairable>() == null ? 0.0f : item.MaxCondition; }
|
|
}
|
|
|
|
public bool CanBeWelded = true;
|
|
|
|
private float stuck;
|
|
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")]
|
|
public float Stuck
|
|
{
|
|
get { return stuck; }
|
|
set
|
|
{
|
|
if (isOpen || isBroken || !CanBeWelded) { return; }
|
|
stuck = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
//don't allow clients to make the door stuck unless the server says so (handled in ClientRead)
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (stuck <= 0.0f) { IsStuck = false; }
|
|
if (stuck >= 99.0f) { IsStuck = true; }
|
|
}
|
|
}
|
|
|
|
[Serialize(3.0f, IsPropertySaveable.Yes, description: "How quickly the door opens."), Editable]
|
|
public float OpeningSpeed { get; private set; }
|
|
|
|
[Serialize(3.0f, IsPropertySaveable.Yes, description: "How quickly the door closes."), Editable]
|
|
public float ClosingSpeed { get; private set; }
|
|
|
|
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The door cannot be opened/closed during this time after it has been opened/closed by another character."), Editable]
|
|
public float ToggleCoolDown { get; private set; }
|
|
|
|
public bool? PredictedState { get; private set; }
|
|
|
|
public Gap LinkedGap
|
|
{
|
|
get
|
|
{
|
|
if (linkedGap == null)
|
|
{
|
|
GetLinkedGap();
|
|
}
|
|
return linkedGap;
|
|
}
|
|
}
|
|
|
|
private void GetLinkedGap()
|
|
{
|
|
linkedGap = item.linkedTo.FirstOrDefault(e => e is Gap) as Gap;
|
|
if (linkedGap == null)
|
|
{
|
|
Rectangle rect = item.Rect;
|
|
linkedGap = new Gap(rect, !IsHorizontal, Item.Submarine)
|
|
{
|
|
Submarine = item.Submarine
|
|
};
|
|
item.linkedTo.Add(linkedGap);
|
|
}
|
|
RefreshLinkedGap();
|
|
}
|
|
|
|
public bool IsHorizontal { get; private set; }
|
|
|
|
public bool IsConvexHullHorizontal => autoOrientGap && linkedGap != null ? !linkedGap.IsHorizontal : IsHorizontal;
|
|
|
|
[Serialize("0.0,0.0,0.0,0.0", IsPropertySaveable.No, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")]
|
|
public Rectangle Window { get; set; }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Is the door currently open.")]
|
|
public bool IsOpen
|
|
{
|
|
get { return isOpen; }
|
|
set
|
|
{
|
|
isOpen = value;
|
|
OpenState = isOpen ? 1.0f : 0.0f;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can be used by status effects to tell the door to open (setting IsOpen directly would make it immediately fully open)
|
|
/// </summary>
|
|
public bool ShouldBeOpen
|
|
{
|
|
get { return isOpen; }
|
|
set
|
|
{
|
|
if (isOpen != value)
|
|
{
|
|
ToggleState(ActionType.OnUse, user: null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsClosed => !IsOpen;
|
|
|
|
public bool IsFullyOpen => IsOpen && OpenState >= 1.0f;
|
|
|
|
public bool IsFullyClosed => IsClosed && OpenState <= 0f;
|
|
|
|
public bool HasWindow => Window != Rectangle.Empty;
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")]
|
|
public bool HasIntegratedButtons { get; private set; }
|
|
|
|
[ConditionallyEditable(ConditionallyEditable.ConditionType.HasIntegratedButtons),
|
|
Serialize(true, IsPropertySaveable.No, description: "If the door has integrated buttons, should clicking on it perform the default action of opening the door? Can be used in conjunction with the \"activate_out\" output to pass a signal to a circuit without toggling the door when someone tries to open/close the door.")]
|
|
public bool ToggleWhenClicked { get; private set; }
|
|
|
|
public float OpenState
|
|
{
|
|
get { return openState; }
|
|
set
|
|
{
|
|
lastOpenState = openState;
|
|
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
|
|
#if CLIENT
|
|
float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
|
|
//refresh convex hulls if the body of the door has moved by 5 pixels,
|
|
//or if it becomes fully closed or fully open
|
|
if (Math.Abs(lastConvexHullState - openState) * size > 5.0f ||
|
|
(openState <= 0.0f && lastConvexHullState > 0.0f) ||
|
|
(openState >= 1.0f && lastConvexHullState < 1.0f))
|
|
{
|
|
UpdateConvexHulls();
|
|
lastConvexHullState = openState;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Characters and items cannot pass through impassable doors. Useful for things such as ducts that should only let water and air through.")]
|
|
public bool Impassable
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "", alwaysUseInstanceValues: true)]
|
|
public bool UseBetweenOutpostModules { get; private set; }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.No, description: "If true, bots won't try to close this door behind them.", alwaysUseInstanceValues: true)]
|
|
public bool BotsShouldKeepOpen { get; private set; }
|
|
|
|
public Door(Item item, ContentXElement element)
|
|
: base(item, element)
|
|
{
|
|
IsHorizontal = element.GetAttributeBool("horizontal", false);
|
|
canBePicked = element.GetAttributeBool("canbepicked", false);
|
|
autoOrientGap = element.GetAttributeBool("autoorientgap", false);
|
|
|
|
allowedSlots.Clear();
|
|
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
string textureDir = GetTextureDirectory(subElement);
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "sprite":
|
|
doorSprite = new Sprite(subElement, path: textureDir);
|
|
break;
|
|
case "weldedsprite":
|
|
weldedSprite = new Sprite(subElement, path: textureDir);
|
|
break;
|
|
case "brokensprite":
|
|
brokenSprite = new Sprite(subElement, path: textureDir);
|
|
scaleBrokenSprite = subElement.GetAttributeBool("scale", false);
|
|
fadeBrokenSprite = subElement.GetAttributeBool("fade", false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
IsActive = true;
|
|
doorList.Add(this);
|
|
}
|
|
|
|
public override void OnItemLoaded()
|
|
{
|
|
//do this here because the scale of the item might not be set to the final value yet in the constructor
|
|
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)),
|
|
radius: 0.0f,
|
|
density: 1.5f,
|
|
BodyType.Static,
|
|
Physics.CollisionWall,
|
|
Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile,
|
|
findNewContacts: false)
|
|
{
|
|
UserData = item,
|
|
Friction = 0.5f
|
|
};
|
|
Body.SetTransformIgnoreContacts(
|
|
ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)),
|
|
0.0f);
|
|
if (isBroken)
|
|
{
|
|
DisableBody();
|
|
}
|
|
}
|
|
|
|
public override void Move(Vector2 amount, bool ignoreContacts = false)
|
|
{
|
|
if (ignoreContacts)
|
|
{
|
|
Body?.SetTransformIgnoreContacts(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
|
|
}
|
|
else
|
|
{
|
|
Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
|
|
}
|
|
#if CLIENT
|
|
UpdateConvexHulls();
|
|
#endif
|
|
}
|
|
|
|
private readonly LocalizedString accessDeniedTxt = TextManager.Get("AccessDenied");
|
|
private readonly LocalizedString cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
|
|
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
|
|
{
|
|
if (IsBroken)
|
|
{
|
|
return false;
|
|
}
|
|
if (isOpen)
|
|
{
|
|
Msg = HasAccess(character) ? "ItemMsgClose" : "ItemMsgForceCloseCrowbar";
|
|
}
|
|
else
|
|
{
|
|
Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar";
|
|
}
|
|
ParseMsg();
|
|
if (addMessage)
|
|
{
|
|
msg ??= (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value;
|
|
}
|
|
return base.HasRequiredItems(character, addMessage, msg);
|
|
}
|
|
|
|
public override bool Pick(Character picker)
|
|
{
|
|
if (item.Condition < RepairThreshold && item.GetComponent<Repairable>().HasRequiredItems(picker, addMessage: false)) { return true; }
|
|
if (RequiredItems.None()) { return false; }
|
|
if (HasAccess(picker) && HasRequiredItems(picker, false)) { return false; }
|
|
return base.Pick(picker);
|
|
}
|
|
|
|
public override bool OnPicked(Character picker)
|
|
{
|
|
if (item.Condition < RepairThreshold && item.GetComponent<Repairable>().HasRequiredItems(picker, addMessage: false)) { return true; }
|
|
if (!HasAccess(picker))
|
|
{
|
|
ToggleState(ActionType.OnPicked, picker);
|
|
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void ToggleState(ActionType actionType, Character user)
|
|
{
|
|
if (toggleCooldownTimer > 0.0f && user != lastUser)
|
|
{
|
|
OnFailedToOpen();
|
|
return;
|
|
}
|
|
if (ToggleWhenClicked)
|
|
{
|
|
//do not activate cooldown at this point if the door doesn't get toggled when clicked
|
|
//(i.e. if it just sends out a signal that might get passed back to the door and try to toggle it)
|
|
toggleCooldownTimer = ToggleCoolDown;
|
|
}
|
|
if (IsStuck || IsJammed)
|
|
{
|
|
#if CLIENT
|
|
if (IsStuck) { HintManager.OnTryOpenStuckDoor(user); }
|
|
#endif
|
|
toggleCooldownTimer = 1.0f;
|
|
OnFailedToOpen();
|
|
return;
|
|
}
|
|
item.SendSignal("1", "activate_out");
|
|
lastUser = user;
|
|
if (ToggleWhenClicked)
|
|
{
|
|
SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked);
|
|
}
|
|
}
|
|
|
|
public override bool Select(Character character)
|
|
{
|
|
if (isBroken) { return true; }
|
|
bool hasRequiredItems = HasRequiredItems(character, false);
|
|
if (HasAccess(character))
|
|
{
|
|
float originalPickingTime = PickingTime;
|
|
PickingTime = 0;
|
|
ToggleState(ActionType.OnUse, character);
|
|
PickingTime = originalPickingTime;
|
|
StopPicking(picker);
|
|
return true;
|
|
}
|
|
#if CLIENT
|
|
else if (hasRequiredItems && character != null && character == Character.Controlled)
|
|
{
|
|
GUI.AddMessage(accessDeniedTxt, GUIStyle.Red);
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the given position inside the vertical bounds of the window, and roughly on the door horizontally? Or the other way around if the door opens horizontally.
|
|
/// </summary>
|
|
/// <param name="position">Position in the same coordinate space as the door.</param>
|
|
/// <param name="maxPerpendicularDistance">Maximum horizontal distance from the door (or vertical if the door opens horizontally)</param>
|
|
public bool IsPositionOnWindow(Vector2 position, float maxPerpendicularDistance = 10.0f)
|
|
{
|
|
if (IsHorizontal)
|
|
{
|
|
return
|
|
position.X >= item.Rect.X + Window.X &&
|
|
position.X <= item.Rect.X + Window.X + Window.Width &&
|
|
position.Y >= item.Rect.Y - maxPerpendicularDistance &&
|
|
position.Y <= item.Rect.Y - item.Rect.Height - maxPerpendicularDistance;
|
|
}
|
|
else
|
|
{
|
|
return
|
|
position.Y >= item.Rect.Y + Window.Y &&
|
|
position.Y <= item.Rect.Y + Window.Y + Window.Height &&
|
|
position.X >= item.Rect.X - maxPerpendicularDistance &&
|
|
position.X <= item.Rect.Right + maxPerpendicularDistance;
|
|
}
|
|
}
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
UpdateProjSpecific(deltaTime);
|
|
toggleCooldownTimer -= deltaTime;
|
|
damageSoundCooldown -= deltaTime;
|
|
|
|
if (isBroken)
|
|
{
|
|
lastBrokenTime = Timing.TotalTime;
|
|
//the door has to be restored to 50% health before collision detection on the body is re-enabled
|
|
|
|
//multiply by MaxRepairConditionMultiplier so the item gets repaired at 50% of the _default max condition_
|
|
//otherwise increasing the max condition is arguably harmful, as the door needs to be repaired further to re-enable the collider
|
|
if (item.ConditionPercentage * Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
|
|
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
|
{
|
|
IsBroken = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool isClosing = false;
|
|
if ((!IsStuck && !IsJammed) || !isOpen)
|
|
{
|
|
if (PredictedState == null)
|
|
{
|
|
OpenState += deltaTime * (isOpen ? OpeningSpeed : -ClosingSpeed);
|
|
isClosing = openState is > 0.0f and < 1.0f && !isOpen;
|
|
}
|
|
else
|
|
{
|
|
OpenState += deltaTime * (PredictedState.Value ? OpeningSpeed : -ClosingSpeed);
|
|
isClosing = openState is > 0.0f and < 1.0f && !PredictedState.Value;
|
|
|
|
resetPredictionTimer -= deltaTime;
|
|
if (resetPredictionTimer <= 0.0f)
|
|
{
|
|
PredictedState = null;
|
|
}
|
|
}
|
|
LinkedGap.Open = isBroken ? 1.0f : openState;
|
|
}
|
|
|
|
if (isClosing)
|
|
{
|
|
//server gives the clients more leeway on moving through closing doors
|
|
//latency can often otherwise make a client get blocked by a closing door server-side even if it seemed like they made it through client-side
|
|
float pushCharactersAwayThreshold = GameMain.NetworkMember is { IsServer: true } ? 0.1f : 0.9f;
|
|
|
|
if (OpenState < pushCharactersAwayThreshold) { PushCharactersAway(); }
|
|
if (CheckSubmarinesInDoorWay())
|
|
{
|
|
PredictedState = null;
|
|
isOpen = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bool wasEnabled = Body.Enabled;
|
|
Body.Enabled = Impassable || openState < 1.0f;
|
|
if (OutsideSubmarineFixture != null)
|
|
{
|
|
OutsideSubmarineFixture.CollidesWith = Body.Enabled ? SubmarineBody.CollidesWith : Category.None;
|
|
}
|
|
if (wasEnabled && !Body.Enabled && IsHorizontal)
|
|
{
|
|
//when opening a hatch, force characters above it to refresh the floor position
|
|
//(otherwise the character won't fall through the hatch until it moves)
|
|
foreach (Character c in Character.CharacterList)
|
|
{
|
|
if (c.WorldPosition.Y < item.WorldPosition.Y) { continue; }
|
|
if (c.WorldPosition.X < item.WorldRect.X || c.WorldPosition.X > item.WorldRect.Right) { continue; }
|
|
c.AnimController?.ForceRefreshFloorY();
|
|
}
|
|
}
|
|
}
|
|
|
|
//don't use the predicted state here, because it might set
|
|
//other items to an incorrect state if the prediction is wrong
|
|
item.SendSignal(isOpen ? "1" : "0", "state_out");
|
|
}
|
|
|
|
partial void UpdateProjSpecific(float deltaTime);
|
|
|
|
|
|
public override void UpdateBroken(float deltaTime, Camera cam)
|
|
{
|
|
base.UpdateBroken(deltaTime, cam);
|
|
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
|
{
|
|
IsBroken = true;
|
|
}
|
|
}
|
|
|
|
private void EnableBody()
|
|
{
|
|
if (!Impassable)
|
|
{
|
|
Body.FarseerBody.SetIsSensor(false);
|
|
var ce = Body.FarseerBody.ContactList;
|
|
while (ce != null && ce.Contact != null)
|
|
{
|
|
ce.Contact.Enabled = false;
|
|
ce = ce.Next;
|
|
}
|
|
PushCharactersAway();
|
|
}
|
|
if (OutsideSubmarineFixture != null && Body.Enabled)
|
|
{
|
|
OutsideSubmarineFixture.CollidesWith = SubmarineBody.CollidesWith;
|
|
}
|
|
#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.SetIsSensor(true);
|
|
var ce = Body.FarseerBody.ContactList;
|
|
while (ce != null && ce.Contact != null)
|
|
{
|
|
ce.Contact.Enabled = false;
|
|
ce = ce.Next;
|
|
}
|
|
}
|
|
|
|
if (OutsideSubmarineFixture != null)
|
|
{
|
|
OutsideSubmarineFixture.CollidesWith = Category.None;
|
|
}
|
|
if (linkedGap != null)
|
|
{
|
|
linkedGap.Open = 1.0f;
|
|
}
|
|
|
|
IsOpen = false;
|
|
#if CLIENT
|
|
if (convexHull != null) { convexHull.Enabled = false; }
|
|
if (convexHull2 != null) { convexHull2.Enabled = false; }
|
|
#endif
|
|
}
|
|
|
|
public void RefreshLinkedGap()
|
|
{
|
|
LinkedGap.Layer = item.Layer;
|
|
LinkedGap.ConnectedDoor = this;
|
|
if (autoOrientGap)
|
|
{
|
|
LinkedGap.AutoOrient();
|
|
}
|
|
LinkedGap.Open = isBroken ? 1.0f : openState;
|
|
LinkedGap.PassAmbientLight = Window != Rectangle.Empty;
|
|
}
|
|
|
|
public override void OnMapLoaded()
|
|
{
|
|
RefreshLinkedGap();
|
|
#if CLIENT
|
|
convexHull = new ConvexHull(doorRect, IsConvexHullHorizontal, item);
|
|
if (Window != Rectangle.Empty)
|
|
{
|
|
convexHull2 = new ConvexHull(doorRect, IsConvexHullHorizontal, item);
|
|
}
|
|
UpdateConvexHulls();
|
|
#endif
|
|
}
|
|
|
|
public override void OnScaleChanged()
|
|
{
|
|
#if CLIENT
|
|
UpdateConvexHulls();
|
|
#endif
|
|
if (linkedGap != null)
|
|
{
|
|
RefreshLinkedGap();
|
|
linkedGap.Rect = item.Rect;
|
|
}
|
|
}
|
|
|
|
protected override void RemoveComponentSpecific()
|
|
{
|
|
base.RemoveComponentSpecific();
|
|
|
|
if (Body != null)
|
|
{
|
|
Body.Remove();
|
|
Body = null;
|
|
}
|
|
|
|
foreach (Gap gap in Gap.GapList)
|
|
{
|
|
if (gap.ConnectedDoor == this)
|
|
{
|
|
gap.ConnectedDoor = null;
|
|
}
|
|
}
|
|
|
|
if (OutsideSubmarineFixture != null)
|
|
{
|
|
OutsideSubmarineFixture.Body.Remove(OutsideSubmarineFixture);
|
|
OutsideSubmarineFixture = 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)
|
|
{
|
|
linkedGap?.Remove();
|
|
}
|
|
doorSprite?.Remove();
|
|
weldedSprite?.Remove();
|
|
|
|
#if CLIENT
|
|
convexHull?.Remove();
|
|
convexHull2?.Remove();
|
|
#endif
|
|
|
|
doorList.Remove(this);
|
|
}
|
|
|
|
private bool CheckSubmarinesInDoorWay()
|
|
{
|
|
if (linkedGap != null && linkedGap.IsRoomToRoom) { return false; }
|
|
|
|
Rectangle doorRect = item.WorldRect;
|
|
if (IsHorizontal)
|
|
{
|
|
doorRect.Width = (int)(item.Rect.Width * (1.0f - openState));
|
|
}
|
|
else
|
|
{
|
|
doorRect.Height = (int)(item.Rect.Height * (1.0f - openState));
|
|
}
|
|
|
|
foreach (Submarine sub in Submarine.Loaded)
|
|
{
|
|
if (sub == item.Submarine || sub.DockedTo.Contains(item.Submarine)) { continue; }
|
|
Rectangle worldBorders = sub.Borders;
|
|
worldBorders.Location += sub.WorldPosition.ToPoint();
|
|
if (!Submarine.RectsOverlap(worldBorders, doorRect)) { continue; }
|
|
foreach (Hull hull in sub.GetHulls(alsoFromConnectedSubs: false))
|
|
{
|
|
if (Submarine.RectsOverlap(hull.WorldRect, doorRect)) { return true; }
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool itemPosErrorShown;
|
|
private readonly HashSet<Character> characterPosErrorShown = new HashSet<Character>();
|
|
private void PushCharactersAway()
|
|
{
|
|
if (!MathUtils.IsValid(item.SimPosition))
|
|
{
|
|
if (!itemPosErrorShown)
|
|
{
|
|
DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ")");
|
|
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:DoorPosInvalid", GameAnalyticsManager.ErrorSeverity.Error,
|
|
"Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ").");
|
|
itemPosErrorShown = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
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 (c.SelectedItem?.GetComponent<Controller>() is { } controller && controller.IsAttachedUser(c)) { continue; }
|
|
if (!MathUtils.IsValid(c.SimPosition))
|
|
{
|
|
if (!characterPosErrorShown.Contains(c))
|
|
{
|
|
if (GameSettings.CurrentConfig.VerboseLogging) { 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", GameAnalyticsManager.ErrorSeverity.Error,
|
|
"Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + c.SimPosition + ")." +
|
|
" Removed: " + c.Removed +
|
|
" Remoteplayer: " + c.IsRemotePlayer);
|
|
characterPosErrorShown.Add(c);
|
|
}
|
|
continue;
|
|
}
|
|
int dir = IsHorizontal ? Math.Sign(c.SimPosition.Y - item.SimPosition.Y) : Math.Sign(c.SimPosition.X - item.SimPosition.X);
|
|
|
|
foreach (Limb limb in c.AnimController.Limbs)
|
|
{
|
|
if (limb.IsSevered) { continue; }
|
|
if (PushBodyOutOfDoorway(c, limb.body, dir, simPos, simSize) && damageSoundCooldown <= 0.0f)
|
|
{
|
|
#if CLIENT
|
|
SoundPlayer.PlayDamageSound("LimbBlunt", 1.0f, limb.body);
|
|
#endif
|
|
damageSoundCooldown = 0.5f;
|
|
}
|
|
}
|
|
PushBodyOutOfDoorway(c, c.AnimController.Collider, dir, simPos, simSize);
|
|
}
|
|
}
|
|
|
|
private bool PushBodyOutOfDoorway(Character c, PhysicsBody body, int dir, Vector2 doorRectSimPos, Vector2 doorRectSimSize)
|
|
{
|
|
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", GameAnalyticsManager.ErrorSeverity.Error,
|
|
"Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + body.SimPosition + ")." +
|
|
" Removed: " + c.Removed +
|
|
" Remoteplayer: " + c.IsRemotePlayer);
|
|
return false;
|
|
}
|
|
|
|
float diff;
|
|
if (IsHorizontal)
|
|
{
|
|
if (body.SimPosition.X < doorRectSimPos.X || body.SimPosition.X > doorRectSimPos.X + doorRectSimSize.X) { return false; }
|
|
diff = body.SimPosition.Y - item.SimPosition.Y;
|
|
}
|
|
else
|
|
{
|
|
if (body.SimPosition.Y > doorRectSimPos.Y || body.SimPosition.Y < doorRectSimPos.Y - doorRectSimSize.Y) { return false; }
|
|
diff = body.SimPosition.X - item.SimPosition.X;
|
|
}
|
|
|
|
//if the limb is at a different side of the door than the character (collider),
|
|
//immediately teleport it to the correct side
|
|
if (Math.Sign(diff) != dir)
|
|
{
|
|
if (IsHorizontal)
|
|
{
|
|
body.SetTransformIgnoreContacts(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
|
|
}
|
|
else
|
|
{
|
|
body.SetTransformIgnoreContacts(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
|
|
}
|
|
}
|
|
|
|
//apply an impulse to push the limb further from the door
|
|
if (IsHorizontal)
|
|
{
|
|
if (Math.Abs(body.SimPosition.Y - item.SimPosition.Y) > doorRectSimSize.Y * 0.5f) { return false; }
|
|
body.ApplyLinearImpulse(new Vector2(isOpen ? 0.0f : 1.0f, dir * 2.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
}
|
|
else
|
|
{
|
|
if (Math.Abs(body.SimPosition.X - item.SimPosition.X) > doorRectSimSize.X * 0.5f) { return false; }
|
|
body.ApplyLinearImpulse(new Vector2(dir * 2.0f, isOpen ? 0.0f : -1.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
}
|
|
|
|
//don't stun if the door was broken a moment ago
|
|
//otherwise enabling the door's collider and pushing the character away will interrupt repairing
|
|
if (lastBrokenTime < Timing.TotalTime - 1.0f)
|
|
{
|
|
c.SetStun(0.2f);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
partial void OnFailedToOpen();
|
|
|
|
public override bool HasAccess(Character character)
|
|
{
|
|
if (!item.IsInteractable(character)) { return false; }
|
|
if (!base.HasAccess(character)) { return false; }
|
|
if (HasIntegratedButtons) { return true; }
|
|
var buttons = Item.GetConnectedComponents<Controller>(recursive: true);
|
|
// If there are no buttons, and we can access the door, treat it accessible. Might be controlled by some mechanism, such as motion sensor.
|
|
return buttons.None() || buttons.Any(b => b.HasAccess(character));
|
|
}
|
|
|
|
public override void ReceiveSignal(Signal signal, Connection connection)
|
|
{
|
|
if (IsStuck || IsJammed || IgnoreSignals) { return; }
|
|
|
|
bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value;
|
|
|
|
if (connection.Name == "toggle")
|
|
{
|
|
if (signal.value == "0") { return; }
|
|
if (toggleCooldownTimer > 0.0f && signal.sender != lastUser) { OnFailedToOpen(); return; }
|
|
if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
|
|
toggleCooldownTimer = ToggleCoolDown;
|
|
lastUser = signal.sender;
|
|
SetState(!wasOpen, false, true, forcedOpen: false);
|
|
}
|
|
else if (connection.Name == "set_state")
|
|
{
|
|
bool signalOpen = signal.value != "0";
|
|
if (IsStuck && signalOpen != wasOpen) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
|
|
SetState(signalOpen, false, true, forcedOpen: false);
|
|
}
|
|
|
|
#if SERVER
|
|
if (signal.sender != null && wasOpen != isOpen)
|
|
{
|
|
GameServer.Log(GameServer.CharacterLogName(signal.sender) + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public void TrySetState(bool open, bool isNetworkMessage, bool sendNetworkMessage = false)
|
|
{
|
|
SetState(open, isNetworkMessage, sendNetworkMessage, forcedOpen: false);
|
|
}
|
|
|
|
partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage, bool forcedOpen);
|
|
}
|
|
}
|