1108 lines
41 KiB
C#
1108 lines
41 KiB
C#
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
class LimbPos : ISerializableEntity
|
|
{
|
|
[Editable]
|
|
public LimbType LimbType { get; set; }
|
|
|
|
[Editable]
|
|
public Vector2 Position { get; set; }
|
|
|
|
public bool AllowUsingLimb;
|
|
|
|
public string Name => LimbType.ToString();
|
|
|
|
public Dictionary<Identifier, SerializableProperty> SerializableProperties => null;
|
|
|
|
public LimbPos(LimbType limbType, Vector2 position, bool allowUsingLimb)
|
|
{
|
|
LimbType = limbType;
|
|
Position = position;
|
|
AllowUsingLimb = allowUsingLimb;
|
|
}
|
|
}
|
|
|
|
partial class Controller : ItemComponent, IServerSerializable
|
|
{
|
|
//where the limbs of the user should be positioned when using the controller
|
|
private readonly List<LimbPos> limbPositions = new List<LimbPos>();
|
|
|
|
private Direction dir;
|
|
public Direction Direction => dir;
|
|
|
|
//the position where the user walks to when using the controller
|
|
//(relative to the position of the item)
|
|
private Vector2 userPos;
|
|
|
|
private Camera cam;
|
|
|
|
private Character user;
|
|
public Character User
|
|
{
|
|
get { return user; }
|
|
private set
|
|
{
|
|
if (user == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
user = value;
|
|
|
|
if (user != null)
|
|
{
|
|
teleportTransition = 0f;
|
|
teleportStartPosition = user.WorldPosition;
|
|
}
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
|
|
#if CLIENT
|
|
UpdateMsg();
|
|
|
|
if (HideAllItemComponentHUDs && Character.Controlled == user)
|
|
{
|
|
// Prevents any UIs in this item from briefly showing up when you select this controller, since
|
|
// activeHUDs would take a single frame to be updated to not contain any other item component HUD
|
|
Item.ClearActiveHUDs();
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private Item focusTarget;
|
|
private float targetRotation;
|
|
|
|
public Vector2 UserPos
|
|
{
|
|
get { return userPos; }
|
|
set { userPos = value; }
|
|
}
|
|
|
|
public IEnumerable<LimbPos> LimbPositions { get { return limbPositions; } }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out a signal when interacted with.", alwaysUseInstanceValues: true)]
|
|
public bool IsToggle
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
private string output;
|
|
[ConditionallyEditable(ConditionallyEditable.ConditionType.HasConnectionPanel, onlyInEditors: false),
|
|
Serialize("1", IsPropertySaveable.Yes, description: "The signal sent when the controller is being activated or is toggled on. If empty, no signal is sent.", alwaysUseInstanceValues: true)]
|
|
public string Output
|
|
{
|
|
get { return output; }
|
|
set
|
|
{
|
|
if (value == null || value == output) { return; }
|
|
output = value;
|
|
//reactivate if signal isn't empty (we may not have been previously sending a signal, but might now)
|
|
if (!value.IsNullOrEmpty()) { IsActive = true; }
|
|
}
|
|
}
|
|
|
|
private string falseOutput;
|
|
[ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: false),
|
|
Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the controller is toggled off. If empty, no signal is sent. Only valid if IsToggle is true.", alwaysUseInstanceValues: true)]
|
|
public string FalseOutput
|
|
{
|
|
get { return falseOutput; }
|
|
set
|
|
{
|
|
if (value == null || value == falseOutput) { return; }
|
|
falseOutput = value;
|
|
//reactivate if signal isn't empty (we may not have been previously sending a signal, but might now)
|
|
if (!value.IsNullOrEmpty()) { IsActive = true; }
|
|
}
|
|
}
|
|
|
|
private bool state;
|
|
[ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: true),
|
|
Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)]
|
|
public bool State
|
|
{
|
|
get { return state; }
|
|
set
|
|
{
|
|
if (state != value)
|
|
{
|
|
state = value;
|
|
string newOutput = state ? output : falseOutput;
|
|
IsActive = !string.IsNullOrEmpty(newOutput);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Serialize(true, IsPropertySaveable.No, description: "Should the HUD (inventory, health bar, etc) be hidden when this item is selected.")]
|
|
public bool HideHUD
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Should the HUDs of all item components in this item be hidden when a character is using this controller.")]
|
|
public bool HideAllItemComponentHUDs
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public enum UseEnvironment
|
|
{
|
|
Air, Water, Both
|
|
};
|
|
|
|
[Serialize(UseEnvironment.Both, IsPropertySaveable.No, description: "Can the item be selected in air, underwater or both.")]
|
|
public UseEnvironment UsableIn { get; set; }
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Should the character using the item be drawn behind the item.")]
|
|
public bool DrawUserBehind
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when another character has already selected it?")]
|
|
public bool AllowSelectingWhenSelectedByOther
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when a bot has already selected it?")]
|
|
public bool AllowSelectingWhenSelectedByBot
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Can a character put another character into this controller by dragging them and selecting this controller?")]
|
|
public bool AllowPuttingInOtherCharacters
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(true, IsPropertySaveable.No, description: "Can a character select this controller by themselves?")]
|
|
public bool CanBeSelectedByCharacters
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "If a character selects this controller, but another character already has it selected, should it be kicked out?")]
|
|
public bool SelectingKicksCharacterOut
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize("", IsPropertySaveable.No, description: "Message displayed when there's a character inside this controller.")]
|
|
public string KickOutCharacterMsg
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize("", IsPropertySaveable.No, description: "Message displayed when you are putting a character into the controller.")]
|
|
public string PutOtherCharacterMsg
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
|
|
[Serialize("", IsPropertySaveable.No, description: "Spawns this item in the first available item container slot when a character selects this controller, if the item container is full, the character will not be able to select the controller.")]
|
|
public Identifier SpawnItemOnSelected
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public bool ControlCharacterPose
|
|
{
|
|
get { return limbPositions.Count > 0; }
|
|
}
|
|
|
|
public bool UserInCorrectPosition
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public bool AllowAiming
|
|
{
|
|
get;
|
|
private set;
|
|
} = true;
|
|
|
|
[Serialize(false, IsPropertySaveable.No)]
|
|
public bool NonInteractableWhenFlippedX
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No)]
|
|
public bool NonInteractableWhenFlippedY
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Does the Controller require power to function (= to send signals and move the camera focus to a connected item)?")]
|
|
public bool RequirePower
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "If true, other items can be used simultaneously.")]
|
|
public bool IsSecondaryItem
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "If enabled, the user sticks to the position of this item even if the item moves.")]
|
|
public bool ForceUserToStayAttached
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to determine how fast the character is teleported
|
|
/// to the item when they first select the controller.
|
|
/// Only relevant for <see cref="ForceUserToStayAttached" />
|
|
/// </summary>
|
|
private const float TeleportTransitionSpeed = 8f;
|
|
private float teleportTransition = 0f;
|
|
private Vector2 teleportStartPosition;
|
|
|
|
private readonly ItemPrefab spawnItemOnSelectedPrefab;
|
|
private readonly ItemContainer containerToSpawnOnSelectedItem;
|
|
|
|
/// <summary>
|
|
/// Item spawned by <see cref="spawnItemOnSelectedPrefab"/>
|
|
/// </summary>
|
|
private Item spawnedItemOnSelected = null;
|
|
|
|
public Controller(Item item, ContentXElement element)
|
|
: base(item, element)
|
|
{
|
|
userPos = element.GetAttributeVector2("UserPos", Vector2.Zero);
|
|
Enum.TryParse(element.GetAttributeString("direction", "None"), out dir);
|
|
LoadLimbPositions(element);
|
|
IsActive = true;
|
|
|
|
containerToSpawnOnSelectedItem = item.GetComponent<ItemContainer>();
|
|
|
|
if (!SpawnItemOnSelected.IsEmpty && !ItemPrefab.Prefabs.TryGet(SpawnItemOnSelected, out spawnItemOnSelectedPrefab))
|
|
{
|
|
DebugConsole.ThrowError($"Failed to find item prefab \"{SpawnItemOnSelected}\"");
|
|
}
|
|
|
|
if (containerToSpawnOnSelectedItem == null && !SpawnItemOnSelected.IsEmpty)
|
|
{
|
|
DebugConsole.ThrowError($"Error - Controller has a {nameof(SpawnItemOnSelected)} but no ItemContainer defined");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hack for allowing characters to interact with a loader to get inside a boarding pod.
|
|
/// Doing that simply by autointeracting with the contained pod is difficult, because interacting with the loader selects it
|
|
/// _after_ the Select method of the pod is called by the autointeract logic, and the character only goes inside the pod if it's the selected item.
|
|
/// </summary>
|
|
private bool forceSelectNextFrame;
|
|
|
|
private float userCanInteractCheckTimer;
|
|
|
|
private const float UserCanInteractCheckInterval = 1.0f;
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
this.cam = cam;
|
|
if (!ForceUserToStayAttached) { UserInCorrectPosition = false; }
|
|
|
|
string signal = IsToggle && State ? output : falseOutput;
|
|
if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal) && !IsOutOfPower())
|
|
{
|
|
item.SendSignal(signal, "signal_out");
|
|
item.SendSignal(signal, "trigger_out");
|
|
}
|
|
|
|
if (forceSelectNextFrame && User != null)
|
|
{
|
|
User.SelectedItem = item;
|
|
}
|
|
forceSelectNextFrame = false;
|
|
|
|
userCanInteractCheckTimer -= deltaTime;
|
|
|
|
if (User == null
|
|
|| User.Removed
|
|
|| (((User.Stun <= 0f && !User.IsKnockedDownOrRagdolled && !User.LockHands) || !ForceUserToStayAttached) && (!User.IsAnySelectedItem(item) || !CheckUserCanInteract()))
|
|
|| (item.ParentInventory != null && !IsAttachedUser(User))
|
|
|| (UsableIn == UseEnvironment.Water && !User.AnimController.InWater)
|
|
|| (UsableIn == UseEnvironment.Air && User.AnimController.InWater)
|
|
|| !CheckSpawnItem()
|
|
)
|
|
{
|
|
if (User != null)
|
|
{
|
|
CancelUsing(User);
|
|
User = null;
|
|
}
|
|
if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; }
|
|
return;
|
|
}
|
|
|
|
if (ForceUserToStayAttached)
|
|
{
|
|
teleportTransition = MathF.Min(teleportTransition + deltaTime * TeleportTransitionSpeed, 1f);
|
|
|
|
if (teleportTransition >= 1f)
|
|
{
|
|
// Transition is complete, if someone was holding this character, force them to deselect
|
|
// so they aren't holding the character that is now attached to the controller
|
|
if (User.SelectedBy != null)
|
|
{
|
|
User.SelectedBy.SelectedCharacter = null;
|
|
}
|
|
}
|
|
|
|
if (User == Character.Controlled
|
|
|| teleportTransition < 1f
|
|
|| Vector2.DistanceSquared(item.WorldPosition, User.WorldPosition) > 0.1f)
|
|
{
|
|
var targetPosition = Vector2.Lerp(teleportStartPosition, item.WorldPosition, teleportTransition);
|
|
User.TeleportTo(targetPosition);
|
|
User.AnimController.Collider.ResetDynamics();
|
|
foreach (var limb in User.AnimController.Limbs)
|
|
{
|
|
if (limb.Removed || limb.IsSevered) { continue; }
|
|
limb.body?.ResetDynamics();
|
|
}
|
|
}
|
|
}
|
|
|
|
User.AnimController.StartUsingItem();
|
|
|
|
if (userPos != Vector2.Zero)
|
|
{
|
|
Vector2 diff = (item.WorldPosition + userPos) - User.WorldPosition;
|
|
|
|
if (User.AnimController.InWater)
|
|
{
|
|
if (diff.LengthSquared() > 30.0f * 30.0f)
|
|
{
|
|
User.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One);
|
|
User.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
|
|
}
|
|
else
|
|
{
|
|
User.AnimController.TargetMovement = Vector2.Zero;
|
|
UserInCorrectPosition = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Secondary items (like ladders or chairs) will control the character position over primary items
|
|
// Only control the character position if the character doesn't have another secondary item already controlling it
|
|
if (!User.HasSelectedAnotherSecondaryItem(Item))
|
|
{
|
|
diff.Y = 0.0f;
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && User != Character.Controlled)
|
|
{
|
|
if (Math.Abs(diff.X) > 20.0f)
|
|
{
|
|
//wait for the character to walk to the correct position
|
|
return;
|
|
}
|
|
else if (Math.Abs(diff.X) > 0.1f)
|
|
{
|
|
//aim to keep the collider at the correct position once close enough
|
|
User.AnimController.Collider.LinearVelocity = new Vector2(
|
|
diff.X * 0.1f,
|
|
User.AnimController.Collider.LinearVelocity.Y);
|
|
}
|
|
}
|
|
else if (Math.Abs(diff.X) > 10.0f)
|
|
{
|
|
User.AnimController.TargetMovement = Vector2.Normalize(diff);
|
|
User.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
|
|
return;
|
|
}
|
|
User.AnimController.TargetMovement = Vector2.Zero;
|
|
}
|
|
UserInCorrectPosition = true;
|
|
}
|
|
}
|
|
|
|
ApplyStatusEffects(ActionType.OnActive, deltaTime, User);
|
|
|
|
if (limbPositions.Count == 0) { return; }
|
|
|
|
User.AnimController.StartUsingItem();
|
|
|
|
if (User.SelectedItem != null)
|
|
{
|
|
User.AnimController.ResetPullJoints(l => l.IsLowerBody);
|
|
}
|
|
else
|
|
{
|
|
User.AnimController.ResetPullJoints();
|
|
}
|
|
|
|
if (dir != 0) { User.AnimController.TargetDir = dir; }
|
|
|
|
foreach (LimbPos lb in limbPositions)
|
|
{
|
|
Limb limb = User.AnimController.GetLimb(lb.LimbType);
|
|
if (limb == null || !limb.body.Enabled) { continue; }
|
|
// Don't move lower body limbs if there's another selected secondary item that should control them
|
|
if (limb.IsLowerBody && User.HasSelectedAnotherSecondaryItem(Item)) { continue; }
|
|
// Don't move hands if there's a selected primary item that should control them
|
|
if (limb.IsArm && Item == User.SelectedSecondaryItem && User.SelectedItem != null) { continue; }
|
|
if (lb.AllowUsingLimb)
|
|
{
|
|
switch (lb.LimbType)
|
|
{
|
|
case LimbType.RightHand:
|
|
case LimbType.RightForearm:
|
|
case LimbType.RightArm:
|
|
if (User.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; }
|
|
break;
|
|
case LimbType.LeftHand:
|
|
case LimbType.LeftForearm:
|
|
case LimbType.LeftArm:
|
|
if (User.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; }
|
|
break;
|
|
}
|
|
}
|
|
limb.Disabled = true;
|
|
Vector2 worldPosition = new Vector2(item.WorldRect.X, item.WorldRect.Y) + lb.Position * item.Scale;
|
|
Vector2 diff = worldPosition - limb.WorldPosition;
|
|
limb.PullJointEnabled = true;
|
|
limb.PullJointWorldAnchorB = limb.SimPosition + ConvertUnits.ToSimUnits(diff);
|
|
}
|
|
}
|
|
|
|
private bool IsSpawnContainerFull()
|
|
{
|
|
if (spawnItemOnSelectedPrefab == null || containerToSpawnOnSelectedItem == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (containerToSpawnOnSelectedItem.Inventory.IsFull())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool CheckSpawnItem()
|
|
{
|
|
if (spawnItemOnSelectedPrefab == null || containerToSpawnOnSelectedItem == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(x => x.Prefab == spawnItemOnSelectedPrefab))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (spawnedItemOnSelected != null && !spawnedItemOnSelected.Removed)
|
|
{
|
|
if (spawnedItemOnSelected.ParentInventory != containerToSpawnOnSelectedItem.Inventory)
|
|
{
|
|
// Item was moved or dropped, force the user in this controller out
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (IsSpawnContainerFull())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
|
{
|
|
Entity.Spawner.AddItemToSpawnQueue(spawnItemOnSelectedPrefab, containerToSpawnOnSelectedItem.Inventory, onSpawned: spawnedItem =>
|
|
{
|
|
spawnedItemOnSelected = spawnedItem;
|
|
|
|
var linkedCharacterComponent = spawnedItem.GetComponent<LinkedControllerCharacterComponent>();
|
|
if (linkedCharacterComponent != null)
|
|
{
|
|
linkedCharacterComponent.UpdateLinkedCharacter(User);
|
|
}
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool CheckUserCanInteract()
|
|
{
|
|
//optimization: CanInteractWith is relatively heavy (can involve visibility checks for example), let's not do it every frame
|
|
if (User != null)
|
|
{
|
|
if (userCanInteractCheckTimer <= 0.0f)
|
|
{
|
|
userCanInteractCheckTimer = UserCanInteractCheckInterval;
|
|
return User.CanInteractWith(item);
|
|
}
|
|
}
|
|
//we only do the actual check every UserCanInteractCheckInterval seconds
|
|
//can mean the component can stay selected for <1s after the user no longer has access to it
|
|
return true;
|
|
}
|
|
|
|
private double lastUsed;
|
|
|
|
public override bool Use(float deltaTime, Character activator = null)
|
|
{
|
|
if (activator != User)
|
|
{
|
|
return false;
|
|
}
|
|
if (User == null || User.Removed || !User.IsAnySelectedItem(item) || !User.CanInteractWith(item))
|
|
{
|
|
User = null;
|
|
return false;
|
|
}
|
|
|
|
if (IsOutOfPower()) { return false; }
|
|
|
|
ApplyStatusEffects(ActionType.OnUse, 1.0f, activator);
|
|
if (IsToggle && (activator == null || lastUsed < Timing.TotalTime - 0.1))
|
|
{
|
|
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
|
{
|
|
State = !State;
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
}
|
|
}
|
|
else if (!string.IsNullOrEmpty(output))
|
|
{
|
|
item.SendSignal(new Signal(output, sender: User), "trigger_out");
|
|
}
|
|
|
|
lastUsed = Timing.TotalTime;
|
|
return true;
|
|
}
|
|
|
|
public override bool SecondaryUse(float deltaTime, Character character = null)
|
|
{
|
|
if (User != character)
|
|
{
|
|
return false;
|
|
}
|
|
if (User == null || character.Removed || !User.IsAnySelectedItem(item) || !character.CanInteractWith(item))
|
|
{
|
|
User = null;
|
|
return false;
|
|
}
|
|
if (character == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (IsOutOfPower()) { return false; }
|
|
|
|
focusTarget = GetFocusTarget();
|
|
|
|
if (focusTarget == null)
|
|
{
|
|
Vector2 centerPos = new Vector2(item.WorldRect.Center.X, item.WorldRect.Center.Y);
|
|
Vector2 offset = character.CursorWorldPosition - centerPos;
|
|
offset.Y = -offset.Y;
|
|
targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset));
|
|
return false;
|
|
}
|
|
|
|
character.ViewTarget = focusTarget;
|
|
|
|
#if CLIENT
|
|
if (character == Character.Controlled && cam != null)
|
|
{
|
|
Lights.LightManager.ViewTarget = focusTarget;
|
|
cam.TargetPos = focusTarget.WorldPosition;
|
|
cam.OffsetAmount = MathHelper.Lerp(cam.OffsetAmount, (focusTarget as Item).Prefab.OffsetOnSelected * focusTarget.OffsetOnSelectedMultiplier, deltaTime * 10.0f);
|
|
HideHUDs(true);
|
|
}
|
|
#endif
|
|
|
|
if (!character.IsRemotePlayer || character.ViewTarget == focusTarget)
|
|
{
|
|
Vector2 centerPos = new Vector2(focusTarget.WorldRect.Center.X, focusTarget.WorldRect.Center.Y);
|
|
if (focusTarget.GetComponent<Turret>() is { } turret)
|
|
{
|
|
centerPos = new Vector2(focusTarget.WorldRect.X + turret.TransformedBarrelPos.X, focusTarget.WorldRect.Y - turret.TransformedBarrelPos.Y);
|
|
}
|
|
Vector2 offset = character.CursorWorldPosition - centerPos;
|
|
offset.Y = -offset.Y;
|
|
targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public bool IsOutOfPower()
|
|
{
|
|
if (!RequirePower) { return false; }
|
|
var powered = item.GetComponent<Powered>();
|
|
return powered == null || powered.Voltage < powered.MinVoltage;
|
|
}
|
|
|
|
public Item GetFocusTarget()
|
|
{
|
|
var positionOut = item.Connections?.Find(c => c.Name == "position_out");
|
|
if (positionOut == null) { return null; }
|
|
|
|
if (IsOutOfPower()) { return null; }
|
|
|
|
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: User), positionOut);
|
|
|
|
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
|
|
{
|
|
if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; }
|
|
if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected)
|
|
{
|
|
return item.LastSentSignalRecipients[i].Item;
|
|
}
|
|
}
|
|
|
|
foreach (var recipientPanel in item.GetConnectedComponentsRecursive<ConnectionPanel>(positionOut, allowTraversingBackwards: false))
|
|
{
|
|
if (recipientPanel.Item.Condition <= 0.0f) { continue; }
|
|
if (recipientPanel.Item.Prefab.FocusOnSelected)
|
|
{
|
|
return recipientPanel.Item;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public override bool Pick(Character picker)
|
|
{
|
|
if (IsOutOfPower()) { return false; }
|
|
#if CLIENT
|
|
if (Screen.Selected == GameMain.SubEditorScreen) { return false; }
|
|
#endif
|
|
if (IsToggle)
|
|
{
|
|
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
|
{
|
|
State = !State;
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
}
|
|
}
|
|
else if (!string.IsNullOrEmpty(output))
|
|
{
|
|
item.SendSignal(new Signal(output, sender: picker), "signal_out");
|
|
}
|
|
#if CLIENT
|
|
PlaySound(ActionType.OnUse, picker);
|
|
#endif
|
|
ApplyStatusEffects(ActionType.OnUse, 1f, picker);
|
|
return true;
|
|
}
|
|
|
|
private void CancelUsing(Character character)
|
|
{
|
|
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
|
{
|
|
if (spawnedItemOnSelected != null)
|
|
{
|
|
Entity.Spawner.AddEntityToRemoveQueue(spawnedItemOnSelected);
|
|
spawnedItemOnSelected = null;
|
|
}
|
|
}
|
|
|
|
if (character == null || character.Removed) { return; }
|
|
|
|
// Wake character's colliders so they don't just float in the air when taken out of the controller
|
|
character.AnimController.BodyInRest = false;
|
|
|
|
foreach (LimbPos lb in limbPositions)
|
|
{
|
|
Limb limb = character.AnimController.GetLimb(lb.LimbType);
|
|
if (limb == null) { continue; }
|
|
|
|
limb.Disabled = false;
|
|
limb.PullJointEnabled = false;
|
|
}
|
|
|
|
//disable flipping for 0.5 seconds, because flipping the character when it's in a weird pose (e.g. lying in bed) can mess up the ragdoll
|
|
if (character.AnimController is HumanoidAnimController humanoidAnim)
|
|
{
|
|
humanoidAnim.LockFlipping(0.5f);
|
|
}
|
|
|
|
if (character.SelectedItem == item) { character.SelectedItem = null; }
|
|
if (character.SelectedSecondaryItem == item) { character.SelectedSecondaryItem = null; }
|
|
|
|
character.AnimController.StopUsingItem();
|
|
if (character == Character.Controlled)
|
|
{
|
|
HideHUDs(false);
|
|
}
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
}
|
|
|
|
public override bool Select(Character activator)
|
|
{
|
|
if (activator == null || activator.Removed) { return false; }
|
|
if (Item.Condition <= 0.0f && !UpdateWhenInactive) { return false; }
|
|
|
|
if (UsableIn == UseEnvironment.Water && !activator.AnimController.InWater ||
|
|
UsableIn == UseEnvironment.Air && activator.AnimController.InWater)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Someone already using the item
|
|
if (User != null && !User.Removed)
|
|
{
|
|
// Let the server handle the logic here
|
|
if (GameMain.NetworkMember is { IsClient: true })
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Prevent user from kicking character out if they are holding another character
|
|
if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (User == activator || SelectingKicksCharacterOut)
|
|
{
|
|
#if SERVER
|
|
if (User != activator)
|
|
{
|
|
GameServer.Log($"{GameServer.CharacterLogName(activator)} removed {GameServer.CharacterLogName(User)} from {item.Name}",
|
|
ServerLog.MessageType.Attack);
|
|
}
|
|
#endif
|
|
|
|
IsActive = false;
|
|
CancelUsing(User);
|
|
User = null;
|
|
return false;
|
|
}
|
|
else if (User.IsBot && !activator.IsBot)
|
|
{
|
|
if (AllowSelectingWhenSelectedByBot)
|
|
{
|
|
CancelUsing(User);
|
|
User = activator;
|
|
IsActive = true;
|
|
return true;
|
|
}
|
|
}
|
|
return AllowSelectingWhenSelectedByOther;
|
|
}
|
|
else if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter))
|
|
{
|
|
// Stun pets longer so they don't immediately get out of the controller
|
|
if (activator.SelectedCharacter.IsPet)
|
|
{
|
|
activator.SelectedCharacter.SetStun(MathF.Max(activator.SelectedCharacter.Stun, 4f), isNetworkMessage: true);
|
|
}
|
|
else
|
|
{
|
|
// Small amount of stun since non-ragdolled characters behave weirdly when syncing the periodic teleportation in multiplayer
|
|
activator.SelectedCharacter.SetStun(MathF.Max(activator.SelectedCharacter.Stun, 1f), isNetworkMessage: true);
|
|
}
|
|
|
|
#if SERVER
|
|
GameServer.Log($"{GameServer.CharacterLogName(activator)} forced {GameServer.CharacterLogName(activator.SelectedCharacter)} into {item.Name}",
|
|
ServerLog.MessageType.Attack);
|
|
#endif
|
|
|
|
User = activator.SelectedCharacter;
|
|
User.SelectedItem = this.Item;
|
|
IsActive = true;
|
|
if (ForceUserToStayAttached && item.Container != null)
|
|
{
|
|
forceSelectNextFrame = true;
|
|
}
|
|
return false;
|
|
}
|
|
else if (CanBeSelectedByCharacters)
|
|
{
|
|
activator.DeselectCharacter();
|
|
|
|
User = activator;
|
|
IsActive = true;
|
|
if (ForceUserToStayAttached && item.Container != null)
|
|
{
|
|
forceSelectNextFrame = true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//allow the selection logic above to run when out of power, but disallow sending signals
|
|
if (IsOutOfPower()) { return false; }
|
|
|
|
if (!string.IsNullOrEmpty(output))
|
|
{
|
|
item.SendSignal(new Signal(output, sender: User), "signal_out");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// "Attached user" sticks to this item. Can be used for things such as clown crates and boarding pods.
|
|
/// </summary>
|
|
public bool IsAttachedUser(Character character)
|
|
{
|
|
return character != null && character == User && ForceUserToStayAttached;
|
|
}
|
|
|
|
public override void FlipX(bool relativeToSub)
|
|
{
|
|
if (dir != Direction.None)
|
|
{
|
|
dir = dir == Direction.Left ? Direction.Right : Direction.Left;
|
|
}
|
|
userPos.X = -UserPos.X;
|
|
FlipLimbPositions();
|
|
}
|
|
|
|
public override void FlipY(bool relativeToSub)
|
|
{
|
|
userPos.Y = -UserPos.Y;
|
|
|
|
for (int i = 0; i < limbPositions.Count; i++)
|
|
{
|
|
float diff = (item.Rect.Y + limbPositions[i].Position.Y) - item.Rect.Center.Y;
|
|
|
|
Vector2 flippedPos =
|
|
new Vector2(
|
|
limbPositions[i].Position.X,
|
|
item.Rect.Center.Y - diff - item.Rect.Y);
|
|
limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb);
|
|
}
|
|
}
|
|
|
|
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
|
|
{
|
|
#if CLIENT
|
|
UpdateMsg();
|
|
#endif
|
|
|
|
bool canPutCharacter = AllowPuttingInOtherCharacters && CanPutSelectedCharacter(character.SelectedCharacter, addMessage);
|
|
bool canKickCharacter = SelectingKicksCharacterOut && User != null && !User.Removed;
|
|
bool canUseController = CanBeSelectedByCharacters;
|
|
|
|
// Prevent kicking a character out when the user is holding another character to put into the controller.
|
|
// This avoids accidentally taking out a character (e.g. from a deconstructor).
|
|
if (canPutCharacter && canKickCharacter)
|
|
{
|
|
#if CLIENT
|
|
if (addMessage)
|
|
{
|
|
GUI.AddMessage(TextManager.Get("ItemMsgAlreadyHasCharacterFail"), Color.Red, playSound: false);
|
|
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
|
|
}
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!canKickCharacter && !canPutCharacter && !canUseController)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (IsSpawnContainerFull())
|
|
{
|
|
#if CLIENT
|
|
if (addMessage)
|
|
{
|
|
GUI.AddMessage(TextManager.Get("ItemMsgNotEnoughSpaceCharacterFail"), Color.Red, playSound: false);
|
|
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
|
|
}
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
|
|
return base.HasRequiredItems(character, addMessage, msg);
|
|
}
|
|
|
|
public override bool HasAccess(Character character)
|
|
{
|
|
if (!item.IsInteractable(character)) { return false; }
|
|
return base.HasAccess(character);
|
|
}
|
|
|
|
private bool CanPutSelectedCharacter(Character character, bool showMessage = false)
|
|
{
|
|
if (character == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!character.IsContainable)
|
|
{
|
|
#if CLIENT
|
|
if (showMessage)
|
|
{
|
|
GUI.AddMessage(TextManager.Get("ItemMsgPutCharacterFail"), Color.Red);
|
|
}
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
|
|
if (character.IsKnockedDownOrRagdolled) { return true; }
|
|
if (character.LockHands) { return true; }
|
|
if (character.IsPet)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
partial void HideHUDs(bool value);
|
|
|
|
public override XElement Save(XElement parentElement)
|
|
{
|
|
return SaveLimbPositions(base.Save(parentElement));
|
|
}
|
|
|
|
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
|
{
|
|
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
|
|
if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode)
|
|
{
|
|
LoadLimbPositions(componentElement);
|
|
}
|
|
}
|
|
|
|
private XElement SaveLimbPositions(XElement element)
|
|
{
|
|
if (Screen.Selected == GameMain.SubEditorScreen)
|
|
{
|
|
if (item.FlippedX)
|
|
{
|
|
FlipLimbPositions();
|
|
}
|
|
// Don't save flipped positions.
|
|
foreach (var limbPos in limbPositions)
|
|
{
|
|
element.Add(new XElement("limbposition",
|
|
new XAttribute("limb", limbPos.LimbType),
|
|
new XAttribute("position", XMLExtensions.Vector2ToString(limbPos.Position)),
|
|
new XAttribute("allowusinglimb", limbPos.AllowUsingLimb)));
|
|
}
|
|
if (item.FlippedX)
|
|
{
|
|
FlipLimbPositions();
|
|
}
|
|
}
|
|
return element;
|
|
}
|
|
|
|
private void LoadLimbPositions(ContentXElement element)
|
|
{
|
|
limbPositions.Clear();
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
if (subElement.Name != "limbposition") { continue; }
|
|
string limbStr = subElement.GetAttributeString("limb", "");
|
|
if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType))
|
|
{
|
|
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type.",
|
|
contentPackage: element.ContentPackage);
|
|
}
|
|
else
|
|
{
|
|
LimbPos limbPos = new LimbPos(limbType,
|
|
subElement.GetAttributeVector2("position", Vector2.Zero),
|
|
subElement.GetAttributeBool("allowusinglimb", false));
|
|
limbPositions.Add(limbPos);
|
|
if (!limbPos.AllowUsingLimb)
|
|
{
|
|
if (limbType == LimbType.RightHand || limbType == LimbType.RightForearm || limbType == LimbType.RightArm ||
|
|
limbType == LimbType.LeftHand || limbType == LimbType.LeftForearm || limbType == LimbType.LeftArm)
|
|
{
|
|
AllowAiming = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void FlipLimbPositions()
|
|
{
|
|
for (int i = 0; i < limbPositions.Count; i++)
|
|
{
|
|
float diff = (item.Rect.X + limbPositions[i].Position.X * item.Scale) - item.Rect.Center.X;
|
|
|
|
Vector2 flippedPos =
|
|
new Vector2(
|
|
(item.Rect.Center.X - diff - item.Rect.X) / item.Scale,
|
|
limbPositions[i].Position.Y);
|
|
limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb);
|
|
}
|
|
}
|
|
|
|
public override void OnItemLoaded()
|
|
{
|
|
if (item.FlippedX && NonInteractableWhenFlippedX)
|
|
{
|
|
item.NonInteractable = true;
|
|
}
|
|
else if (item.FlippedY && NonInteractableWhenFlippedY)
|
|
{
|
|
item.NonInteractable = true;
|
|
}
|
|
}
|
|
|
|
public override void Reset()
|
|
{
|
|
base.Reset();
|
|
LoadLimbPositions(originalElement);
|
|
if (item.FlippedX)
|
|
{
|
|
FlipLimbPositions();
|
|
}
|
|
}
|
|
}
|
|
}
|