3462 lines
129 KiB
C#
3462 lines
129 KiB
C#
using Barotrauma.Items.Components;
|
|
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics;
|
|
using FarseerPhysics.Dynamics.Contacts;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
using Barotrauma.Extensions;
|
|
using Barotrauma.MapCreatures.Behavior;
|
|
using MoonSharp.Interpreter;
|
|
using System.Collections.Immutable;
|
|
using Barotrauma.Abilities;
|
|
|
|
#if CLIENT
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
#endif
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable
|
|
{
|
|
public static List<Item> ItemList = new List<Item>();
|
|
public new ItemPrefab Prefab => base.Prefab as ItemPrefab;
|
|
|
|
public static bool ShowLinks = true;
|
|
|
|
private readonly HashSet<Identifier> tags;
|
|
|
|
private bool isWire, isLogic;
|
|
|
|
private Hull currentHull;
|
|
public Hull CurrentHull
|
|
{
|
|
get { return currentHull; }
|
|
set
|
|
{
|
|
currentHull = value;
|
|
}
|
|
}
|
|
|
|
public float HullOxygenPercentage
|
|
{
|
|
get { return CurrentHull?.OxygenPercentage ?? 0.0f; }
|
|
}
|
|
|
|
private CampaignMode.InteractionType campaignInteractionType = CampaignMode.InteractionType.None;
|
|
public CampaignMode.InteractionType CampaignInteractionType
|
|
{
|
|
get { return campaignInteractionType; }
|
|
set
|
|
{
|
|
if (campaignInteractionType != value)
|
|
{
|
|
campaignInteractionType = value;
|
|
AssignCampaignInteractionTypeProjSpecific(campaignInteractionType);
|
|
}
|
|
}
|
|
}
|
|
|
|
partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType);
|
|
|
|
public bool Visible = true;
|
|
|
|
#if CLIENT
|
|
public SpriteEffects SpriteEffects = SpriteEffects.None;
|
|
#endif
|
|
|
|
//components that determine the functionality of the item
|
|
private readonly Dictionary<Type, ItemComponent> componentsByType = new Dictionary<Type, ItemComponent>();
|
|
private readonly List<ItemComponent> components;
|
|
/// <summary>
|
|
/// Components that are Active or need to be updated for some other reason (status effects, sounds)
|
|
/// </summary>
|
|
private readonly List<ItemComponent> updateableComponents = new List<ItemComponent>();
|
|
private readonly List<IDrawableComponent> drawableComponents;
|
|
private bool hasComponentsToDraw;
|
|
|
|
public PhysicsBody body;
|
|
|
|
public readonly XElement StaticBodyConfig;
|
|
|
|
public List<Fixture> StaticFixtures = new List<Fixture>();
|
|
|
|
private bool transformDirty = true;
|
|
|
|
private float lastSentCondition;
|
|
private float sendConditionUpdateTimer;
|
|
private bool conditionUpdatePending;
|
|
|
|
private float condition;
|
|
|
|
private bool inWater;
|
|
private readonly bool hasWaterStatusEffects;
|
|
|
|
private Inventory parentInventory;
|
|
private readonly ItemInventory ownInventory;
|
|
|
|
private Rectangle defaultRect;
|
|
/// <summary>
|
|
/// Unscaled rect
|
|
/// </summary>
|
|
public Rectangle DefaultRect
|
|
{
|
|
get { return defaultRect; }
|
|
set { defaultRect = value; }
|
|
}
|
|
|
|
private readonly Dictionary<string, Connection> connections;
|
|
|
|
private readonly List<Repairable> repairables;
|
|
|
|
private readonly Quality qualityComponent;
|
|
|
|
private readonly ConcurrentQueue<float> impactQueue = new ConcurrentQueue<float>();
|
|
|
|
//a dictionary containing lists of the status effects in all the components of the item
|
|
private readonly bool[] hasStatusEffectsOfType;
|
|
private readonly Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
|
|
|
|
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; protected set; }
|
|
|
|
private bool? hasInGameEditableProperties;
|
|
bool HasInGameEditableProperties
|
|
{
|
|
get
|
|
{
|
|
if (hasInGameEditableProperties == null)
|
|
{
|
|
hasInGameEditableProperties = false;
|
|
if (SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().Any()))
|
|
{
|
|
hasInGameEditableProperties = true;
|
|
}
|
|
else
|
|
{
|
|
foreach (ItemComponent component in components)
|
|
{
|
|
if (!component.AllowInGameEditing) { continue; }
|
|
if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().Any())
|
|
|| component.SerializableProperties.Values.Any(p => p.Attributes.OfType<ConditionallyEditable>().Any(a => a.IsEditable(this))))
|
|
{
|
|
hasInGameEditableProperties = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return (bool)hasInGameEditableProperties;
|
|
}
|
|
}
|
|
|
|
public bool EditableWhenEquipped { get; set; } = false;
|
|
|
|
//the inventory in which the item is contained in
|
|
public Inventory ParentInventory
|
|
{
|
|
get
|
|
{
|
|
return parentInventory;
|
|
}
|
|
set
|
|
{
|
|
parentInventory = value;
|
|
if (parentInventory != null) { Container = parentInventory.Owner as Item; }
|
|
#if SERVER
|
|
PreviousParentInventory = value;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private Item container;
|
|
public Item Container
|
|
{
|
|
get { return container; }
|
|
private set
|
|
{
|
|
if (value != container)
|
|
{
|
|
container = value;
|
|
SetActiveSprite();
|
|
}
|
|
}
|
|
}
|
|
|
|
public override string Name
|
|
{
|
|
get { return base.Prefab.Name.Value; }
|
|
}
|
|
|
|
private string description;
|
|
public string Description
|
|
{
|
|
get { return description ?? base.Prefab.Description.Value; }
|
|
set { description = value; }
|
|
}
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
|
public bool NonInteractable
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Use <see cref="IsPlayerInteractable"/> to also check <see cref="NonInteractable"/>
|
|
/// </summary>
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, item is interactable only for characters on non-player teams.", alwaysUseInstanceValues: true)]
|
|
public bool NonPlayerTeamInteractable
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[ConditionallyEditable(ConditionallyEditable.ConditionType.IsSwappableItem), Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
|
public bool AllowSwapping
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.Yes)]
|
|
public bool PurchasedNewSwap
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks both <see cref="NonInteractable"/> and <see cref="NonPlayerTeamInteractable"/>
|
|
/// </summary>
|
|
public bool IsPlayerTeamInteractable
|
|
{
|
|
get
|
|
{
|
|
return !NonInteractable && !NonPlayerTeamInteractable;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns interactibility based on whether the character is on a player team
|
|
/// </summary>
|
|
public bool IsInteractable(Character character)
|
|
{
|
|
#if CLIENT
|
|
if (Screen.Selected is EditorScreen)
|
|
{
|
|
return true;
|
|
}
|
|
#endif
|
|
if (HiddenInGame) { return false; }
|
|
if (character != null && character.IsOnPlayerTeam)
|
|
{
|
|
return IsPlayerTeamInteractable;
|
|
}
|
|
else
|
|
{
|
|
return !NonInteractable;
|
|
}
|
|
}
|
|
|
|
private float rotationRad;
|
|
|
|
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, IsPropertySaveable.Yes)]
|
|
public float Rotation
|
|
{
|
|
get
|
|
{
|
|
return MathHelper.ToDegrees(rotationRad);
|
|
}
|
|
set
|
|
{
|
|
if (!Prefab.AllowRotatingInEditor) { return; }
|
|
rotationRad = MathHelper.ToRadians(value);
|
|
#if CLIENT
|
|
if (Screen.Selected == GameMain.SubEditorScreen)
|
|
{
|
|
SetContainedItemPositions();
|
|
GetComponent<LightComponent>()?.SetLightSourceTransform();
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public float ImpactTolerance
|
|
{
|
|
get { return Prefab.ImpactTolerance; }
|
|
}
|
|
|
|
public float InteractDistance
|
|
{
|
|
get { return Prefab.InteractDistance; }
|
|
}
|
|
|
|
public float InteractPriority
|
|
{
|
|
get { return Prefab.InteractPriority; }
|
|
}
|
|
|
|
public override Vector2 Position
|
|
{
|
|
get
|
|
{
|
|
return (body == null) ? base.Position : body.Position;
|
|
}
|
|
}
|
|
|
|
public override Vector2 SimPosition
|
|
{
|
|
get
|
|
{
|
|
return (body == null) ? ConvertUnits.ToSimUnits(base.Position) : body.SimPosition;
|
|
}
|
|
}
|
|
|
|
public Rectangle InteractionRect
|
|
{
|
|
get
|
|
{
|
|
return WorldRect;
|
|
}
|
|
}
|
|
|
|
private float scale = 1.0f;
|
|
public override float Scale
|
|
{
|
|
get { return scale; }
|
|
set
|
|
{
|
|
if (scale == value) { return; }
|
|
scale = MathHelper.Clamp(value, 0.01f, 10.0f);
|
|
|
|
float relativeScale = scale / base.Prefab.Scale;
|
|
|
|
if (!ResizeHorizontal || !ResizeVertical)
|
|
{
|
|
int newWidth = ResizeHorizontal ? rect.Width : (int)(defaultRect.Width * relativeScale);
|
|
int newHeight = ResizeVertical ? rect.Height : (int)(defaultRect.Height * relativeScale);
|
|
Rect = new Rectangle(rect.X, rect.Y, newWidth, newHeight);
|
|
}
|
|
|
|
if (components != null)
|
|
{
|
|
foreach (ItemComponent component in components)
|
|
{
|
|
component.OnScaleChanged();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public float PositionUpdateInterval
|
|
{
|
|
get;
|
|
set;
|
|
} = float.PositiveInfinity;
|
|
|
|
protected Color spriteColor;
|
|
[Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)]
|
|
public Color SpriteColor
|
|
{
|
|
get { return spriteColor; }
|
|
set { spriteColor = value; }
|
|
}
|
|
|
|
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable]
|
|
public Color InventoryIconColor
|
|
{
|
|
get;
|
|
protected set;
|
|
}
|
|
|
|
[Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")]
|
|
public Color ContainerColor
|
|
{
|
|
get;
|
|
protected set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can be used by status effects or conditionals to check what item this item is contained inside
|
|
/// </summary>
|
|
public Identifier ContainerIdentifier
|
|
{
|
|
get
|
|
{
|
|
return
|
|
Container?.Prefab.Identifier ??
|
|
ParentInventory?.Owner?.ToIdentifier() ??
|
|
Identifier.Empty;
|
|
}
|
|
}
|
|
|
|
|
|
[Serialize("", IsPropertySaveable.Yes)]
|
|
|
|
/// <summary>
|
|
/// Can be used to modify the AITarget's label using status effects
|
|
/// </summary>
|
|
public string SonarLabel
|
|
{
|
|
get { return AiTarget?.SonarLabel?.Value ?? ""; }
|
|
set
|
|
{
|
|
if (AiTarget != null)
|
|
{
|
|
AiTarget.SonarLabel = !string.IsNullOrEmpty(value) && value.Length > 200 ? value.Substring(200) : value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can be used by status effects or conditionals to check if the physics body of the item is active
|
|
/// </summary>
|
|
public bool PhysicsBodyActive
|
|
{
|
|
get
|
|
{
|
|
return body != null && body.Enabled;
|
|
}
|
|
}
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.No)]
|
|
/// <summary>
|
|
/// Can be used by status effects or conditionals to modify the sound range
|
|
/// </summary>
|
|
public new float SoundRange
|
|
{
|
|
get { return aiTarget == null ? 0.0f : aiTarget.SoundRange; }
|
|
set { if (aiTarget != null) { aiTarget.SoundRange = Math.Max(0.0f, value); } }
|
|
}
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.No)]
|
|
/// <summary>
|
|
/// Can be used by status effects or conditionals to modify the sound range
|
|
/// </summary>
|
|
public new float SightRange
|
|
{
|
|
get { return aiTarget == null ? 0.0f : aiTarget.SightRange; }
|
|
set { if (aiTarget != null) { aiTarget.SightRange = Math.Max(0.0f, value); } }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Should the item's Use method be called with the "Use" or with the "Shoot" key?
|
|
/// </summary>
|
|
[Serialize(false, IsPropertySaveable.No)]
|
|
public bool IsShootable { get; set; }
|
|
|
|
/// <summary>
|
|
/// If true, the user has to hold the "aim" key before use is registered. False by default.
|
|
/// </summary>
|
|
[Serialize(false, IsPropertySaveable.No)]
|
|
public bool RequireAimToUse
|
|
{
|
|
get; set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If true, the user has to hold the "aim" key before secondary use is registered. True by default.
|
|
/// </summary>
|
|
[Serialize(true, IsPropertySaveable.No)]
|
|
public bool RequireAimToSecondaryUse
|
|
{
|
|
get; set;
|
|
}
|
|
|
|
public Color Color
|
|
{
|
|
get { return spriteColor; }
|
|
}
|
|
|
|
public bool IsFullCondition => MathUtils.NearlyEqual(Condition, MaxCondition);
|
|
public float MaxCondition => Prefab.Health * healthMultiplier * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition));
|
|
public float ConditionPercentage => MathUtils.Percentage(Condition, MaxCondition);
|
|
|
|
private float offsetOnSelectedMultiplier = 1.0f;
|
|
|
|
[Serialize(1.0f, IsPropertySaveable.No)]
|
|
public float OffsetOnSelectedMultiplier
|
|
{
|
|
get => offsetOnSelectedMultiplier;
|
|
set => offsetOnSelectedMultiplier = value;
|
|
}
|
|
|
|
private float healthMultiplier = 1.0f;
|
|
|
|
[Serialize(1.0f, IsPropertySaveable.Yes, "Multiply the maximum condition by this value")]
|
|
public float HealthMultiplier
|
|
{
|
|
get => healthMultiplier;
|
|
set
|
|
{
|
|
float prevConditionPercentage = ConditionPercentage;
|
|
healthMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity);
|
|
Condition = MaxCondition * prevConditionPercentage / 100.0f;
|
|
}
|
|
}
|
|
|
|
private float maxRepairConditionMultiplier = 1.0f;
|
|
|
|
[Serialize(1.0f, IsPropertySaveable.Yes)]
|
|
public float MaxRepairConditionMultiplier
|
|
{
|
|
get => maxRepairConditionMultiplier;
|
|
set { maxRepairConditionMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); }
|
|
}
|
|
|
|
//the default value should be Prefab.Health, but because we can't use it in the attribute,
|
|
//we'll just use NaN (which does nothing) and set the default value in the constructor/load
|
|
[Serialize(float.NaN, IsPropertySaveable.No), Editable]
|
|
public float Condition
|
|
{
|
|
get { return condition; }
|
|
set
|
|
{
|
|
SetCondition(value, isNetworkEvent: false);
|
|
}
|
|
}
|
|
|
|
private double ConditionLastUpdated { get; set; }
|
|
private float LastConditionChange { get; set; }
|
|
/// <summary>
|
|
/// Return true if the condition of this item increased within the last second.
|
|
/// </summary>
|
|
public bool ConditionIncreasedRecently => (Timing.TotalTime < ConditionLastUpdated + 1.0f) && LastConditionChange > 0.0f;
|
|
|
|
public float Health
|
|
{
|
|
get { return condition; }
|
|
}
|
|
|
|
private bool? indestructible;
|
|
/// <summary>
|
|
/// Per-instance value - if not set, the value of the prefab is used.
|
|
/// </summary>
|
|
public bool Indestructible
|
|
{
|
|
get => indestructible ?? Prefab.Indestructible;
|
|
set => indestructible = value;
|
|
}
|
|
|
|
public bool AllowDeconstruct
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Editable, Serialize(false, isSaveable: IsPropertySaveable.Yes, "When enabled will prevent the item from taking damage from all sources")]
|
|
public bool InvulnerableToDamage { get; set; }
|
|
|
|
public bool StolenDuringRound;
|
|
|
|
private bool spawnedInCurrentOutpost;
|
|
public bool SpawnedInCurrentOutpost
|
|
{
|
|
get { return spawnedInCurrentOutpost; }
|
|
set
|
|
{
|
|
if (!spawnedInCurrentOutpost && value)
|
|
{
|
|
OriginalOutpost = GameMain.GameSession?.StartLocation?.BaseName ?? "";
|
|
}
|
|
spawnedInCurrentOutpost = value;
|
|
}
|
|
}
|
|
|
|
[Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
|
public bool AllowStealing
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
private string originalOutpost;
|
|
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
|
public string OriginalOutpost
|
|
{
|
|
get { return originalOutpost; }
|
|
set
|
|
{
|
|
originalOutpost = value;
|
|
if (!string.IsNullOrEmpty(value) && GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost && GameMain.GameSession?.StartLocation?.BaseName == value)
|
|
{
|
|
spawnedInCurrentOutpost = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
[Editable, Serialize("", IsPropertySaveable.Yes)]
|
|
public string Tags
|
|
{
|
|
get { return string.Join(",", tags); }
|
|
set
|
|
{
|
|
tags.Clear();
|
|
// Always add prefab tags
|
|
base.Prefab.Tags.ForEach(t => tags.Add(t));
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
{
|
|
string[] splitTags = value.Split(',');
|
|
foreach (string tag in splitTags)
|
|
{
|
|
string[] splitTag = tag.Trim().Split(':');
|
|
splitTag[0] = splitTag[0].ToLowerInvariant();
|
|
tags.Add(string.Join(":", splitTag).ToIdentifier());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool FireProof
|
|
{
|
|
get { return Prefab.FireProof; }
|
|
}
|
|
|
|
public bool WaterProof
|
|
{
|
|
get { return Prefab.WaterProof; }
|
|
}
|
|
|
|
public bool UseInHealthInterface
|
|
{
|
|
get { return Prefab.UseInHealthInterface; }
|
|
}
|
|
|
|
public int Quality
|
|
{
|
|
get
|
|
{
|
|
return qualityComponent?.QualityLevel ?? 0;
|
|
}
|
|
set
|
|
{
|
|
if (qualityComponent != null)
|
|
{
|
|
qualityComponent.QualityLevel = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool InWater
|
|
{
|
|
get
|
|
{
|
|
//if the item has an active physics body, inWater is updated in the Update method
|
|
if (body != null && body.Enabled) { return inWater; }
|
|
if (hasWaterStatusEffects) { return inWater; }
|
|
|
|
//if not, we'll just have to check
|
|
return IsInWater();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A list of connections the last signal sent by this item went through
|
|
/// </summary>
|
|
public List<Connection> LastSentSignalRecipients
|
|
{
|
|
get;
|
|
private set;
|
|
} = new List<Connection>(20);
|
|
|
|
public ContentPath ConfigFilePath => Prefab.ContentFile.Path;
|
|
|
|
//which type of inventory slots (head, torso, any, etc) the item can be placed in
|
|
private readonly HashSet<InvSlotType> allowedSlots = new HashSet<InvSlotType>();
|
|
public IEnumerable<InvSlotType> AllowedSlots
|
|
{
|
|
get
|
|
{
|
|
return allowedSlots;
|
|
}
|
|
}
|
|
|
|
public List<Connection> Connections
|
|
{
|
|
get
|
|
{
|
|
ConnectionPanel panel = GetComponent<ConnectionPanel>();
|
|
if (panel == null) return null;
|
|
return panel.Connections;
|
|
}
|
|
}
|
|
|
|
public IEnumerable<Item> ContainedItems
|
|
{
|
|
get
|
|
{
|
|
return ownInventory?.AllItems ?? Enumerable.Empty<Item>();
|
|
}
|
|
}
|
|
|
|
public ItemInventory OwnInventory
|
|
{
|
|
get { return ownInventory; }
|
|
}
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, description:
|
|
"Enable if you want to display the item HUD side by side with another item's HUD, when linked together. " +
|
|
"Disclaimer: It's possible or even likely that the views block each other, if they were not designed to be viewed together!")]
|
|
public bool DisplaySideBySideWhenLinked { get; set; }
|
|
|
|
public List<Repairable> Repairables
|
|
{
|
|
get { return repairables; }
|
|
}
|
|
|
|
public List<ItemComponent> Components
|
|
{
|
|
get { return components; }
|
|
}
|
|
|
|
public override bool Linkable
|
|
{
|
|
get { return Prefab.Linkable; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can be used to move the item from XML (e.g. to correct the positions of items whose sprite origin has been changed)
|
|
/// </summary>
|
|
public float PositionX
|
|
{
|
|
get { return Position.X; }
|
|
private set
|
|
{
|
|
Move(new Vector2(value * Scale, 0.0f));
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// Can be used to move the item from XML (e.g. to correct the positions of items whose sprite origin has been changed)
|
|
/// </summary>
|
|
public float PositionY
|
|
{
|
|
get { return Position.Y; }
|
|
private set
|
|
{
|
|
Move(new Vector2(0.0f, value * Scale));
|
|
}
|
|
}
|
|
|
|
public BallastFloraBranch Infector { get; set; }
|
|
|
|
public ItemPrefab PendingItemSwap { get; set; }
|
|
|
|
public readonly HashSet<ItemPrefab> AvailableSwaps = new HashSet<ItemPrefab>();
|
|
|
|
public override string ToString()
|
|
{
|
|
return Name + " (ID: " + ID + ")";
|
|
}
|
|
|
|
private readonly List<ISerializableEntity> allPropertyObjects = new List<ISerializableEntity>();
|
|
public IReadOnlyList<ISerializableEntity> AllPropertyObjects
|
|
{
|
|
get { return allPropertyObjects; }
|
|
}
|
|
|
|
public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam;
|
|
public bool OrderedToBeIgnored { get; set; }
|
|
|
|
public bool HasBallastFloraInHull
|
|
{
|
|
get
|
|
{
|
|
return CurrentHull?.BallastFlora != null;
|
|
}
|
|
}
|
|
|
|
public bool IsClaimedByBallastFlora
|
|
{
|
|
get
|
|
{
|
|
if (CurrentHull?.BallastFlora == null) { return false; }
|
|
return CurrentHull.BallastFlora.ClaimedTargets.Contains(this);
|
|
}
|
|
}
|
|
|
|
public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true)
|
|
: this(new Rectangle(
|
|
(int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale),
|
|
(int)(position.Y + itemPrefab.Sprite.size.Y / 2 * itemPrefab.Scale),
|
|
(int)(itemPrefab.Sprite.size.X * itemPrefab.Scale),
|
|
(int)(itemPrefab.Sprite.size.Y * itemPrefab.Scale)),
|
|
itemPrefab, submarine, callOnItemLoaded, id: id)
|
|
{
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new item
|
|
/// </summary>
|
|
/// <param name="callOnItemLoaded">Should the OnItemLoaded methods of the ItemComponents be called. Use false if the item needs additional initialization before it can be considered fully loaded (e.g. when loading an item from a sub file or cloning an item).</param>
|
|
public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool callOnItemLoaded = true, ushort id = Entity.NullEntityID)
|
|
: base(itemPrefab, submarine, id)
|
|
{
|
|
spriteColor = base.Prefab.SpriteColor;
|
|
|
|
components = new List<ItemComponent>();
|
|
drawableComponents = new List<IDrawableComponent>(); hasComponentsToDraw = false;
|
|
tags = new HashSet<Identifier>();
|
|
repairables = new List<Repairable>();
|
|
|
|
defaultRect = newRect;
|
|
rect = newRect;
|
|
|
|
condition = MaxCondition;
|
|
lastSentCondition = condition;
|
|
|
|
AllowDeconstruct = itemPrefab.AllowDeconstruct;
|
|
|
|
allPropertyObjects.Add(this);
|
|
|
|
ContentXElement element = itemPrefab.ConfigElement;
|
|
if (element == null) return;
|
|
|
|
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
|
|
|
if (submarine == null || !submarine.Loading) { FindHull(); }
|
|
|
|
SetActiveSprite();
|
|
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "body":
|
|
float density = subElement.GetAttributeFloat("density", 10.0f);
|
|
float minDensity = subElement.GetAttributeFloat("mindensity", density);
|
|
float maxDensity = subElement.GetAttributeFloat("maxdensity", density);
|
|
if (minDensity < maxDensity)
|
|
{
|
|
var rand = new Random(ID);
|
|
density = MathHelper.Lerp(minDensity, maxDensity, (float)rand.NextDouble());
|
|
}
|
|
body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density);
|
|
|
|
string collisionCategory = subElement.GetAttributeString("collisioncategory", null);
|
|
if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) && Condition > 0)
|
|
{
|
|
//force collision category to Character to allow projectiles and weapons to hit
|
|
//(we could also do this by making the projectiles and weapons hit CollisionItem
|
|
//and check if the collision should be ignored in the OnCollision callback, but
|
|
//that'd make the hit detection more expensive because every item would be included)
|
|
body.CollisionCategories = Physics.CollisionCharacter;
|
|
body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
|
|
}
|
|
if (collisionCategory != null)
|
|
{
|
|
if (!Physics.TryParseCollisionCategory(collisionCategory, out Category cat))
|
|
{
|
|
DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategory + ")");
|
|
}
|
|
else
|
|
{
|
|
body.CollisionCategories = cat;
|
|
if (cat.HasFlag(Physics.CollisionCharacter))
|
|
{
|
|
body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
|
|
}
|
|
}
|
|
}
|
|
body.FarseerBody.AngularDamping = subElement.GetAttributeFloat("angulardamping", 0.2f);
|
|
body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f);
|
|
body.UserData = this;
|
|
break;
|
|
case "trigger":
|
|
case "inventoryicon":
|
|
case "sprite":
|
|
case "deconstruct":
|
|
case "brokensprite":
|
|
case "decorativesprite":
|
|
case "upgradepreviewsprite":
|
|
case "price":
|
|
case "levelcommonness":
|
|
case "suitabletreatment":
|
|
case "containedsprite":
|
|
case "fabricate":
|
|
case "fabricable":
|
|
case "fabricableitem":
|
|
case "upgrade":
|
|
case "preferredcontainer":
|
|
case "upgrademodule":
|
|
case "upgradeoverride":
|
|
case "minimapicon":
|
|
case "infectedsprite":
|
|
case "damagedinfectedsprite":
|
|
case "swappableitem":
|
|
break;
|
|
case "staticbody":
|
|
StaticBodyConfig = subElement;
|
|
break;
|
|
case "aitarget":
|
|
aiTarget = new AITarget(this, subElement);
|
|
break;
|
|
default:
|
|
ItemComponent ic = ItemComponent.Load(subElement, this);
|
|
if (ic == null) break;
|
|
|
|
AddComponent(ic);
|
|
|
|
if (ic is IDrawableComponent && ic.Drawable)
|
|
{
|
|
drawableComponents.Add(ic as IDrawableComponent);
|
|
hasComponentsToDraw = true;
|
|
}
|
|
if (ic is Repairable) repairables.Add((Repairable)ic);
|
|
break;
|
|
}
|
|
}
|
|
|
|
hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length];
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
if (ic is Pickable pickable)
|
|
{
|
|
foreach (var allowedSlot in pickable.AllowedSlots)
|
|
{
|
|
allowedSlots.Add(allowedSlot);
|
|
}
|
|
}
|
|
|
|
if (ic.statusEffectLists == null) { continue; }
|
|
|
|
if (statusEffectLists == null)
|
|
{
|
|
statusEffectLists = new Dictionary<ActionType, List<StatusEffect>>();
|
|
}
|
|
|
|
//go through all the status effects of the component
|
|
//and add them to the corresponding statuseffect list
|
|
foreach (List<StatusEffect> componentEffectList in ic.statusEffectLists.Values)
|
|
{
|
|
ActionType actionType = componentEffectList.First().type;
|
|
if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffectList))
|
|
{
|
|
statusEffectList = new List<StatusEffect>();
|
|
statusEffectLists.Add(actionType, statusEffectList);
|
|
hasStatusEffectsOfType[(int)actionType] = true;
|
|
}
|
|
|
|
foreach (StatusEffect effect in componentEffectList)
|
|
{
|
|
statusEffectList.Add(effect);
|
|
}
|
|
}
|
|
}
|
|
|
|
hasWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.InWater] || hasStatusEffectsOfType[(int)ActionType.NotInWater];
|
|
|
|
if (body != null)
|
|
{
|
|
body.Submarine = submarine;
|
|
}
|
|
|
|
//cache connections into a dictionary for faster lookups
|
|
var connectionPanel = GetComponent<ConnectionPanel>();
|
|
if (connectionPanel != null)
|
|
{
|
|
connections = new Dictionary<string, Connection>();
|
|
foreach (Connection c in connectionPanel.Connections)
|
|
{
|
|
if (!connections.ContainsKey(c.Name))
|
|
connections.Add(c.Name, c);
|
|
}
|
|
}
|
|
|
|
if (body != null)
|
|
{
|
|
body.FarseerBody.OnCollision += OnCollision;
|
|
}
|
|
|
|
var itemContainer = GetComponent<ItemContainer>();
|
|
if (itemContainer != null)
|
|
{
|
|
ownInventory = itemContainer.Inventory;
|
|
}
|
|
|
|
qualityComponent = GetComponent<Quality>();
|
|
|
|
InitProjSpecific();
|
|
|
|
if (callOnItemLoaded)
|
|
{
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
ic.OnItemLoaded();
|
|
}
|
|
}
|
|
|
|
InsertToList();
|
|
ItemList.Add(this);
|
|
|
|
DebugConsole.Log("Created " + Name + " (" + ID + ")");
|
|
|
|
if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; }
|
|
if (HasTag("logic")) { isLogic = true; }
|
|
|
|
GameMain.LuaCs.Hook.Call("item.created", this);
|
|
|
|
ApplyStatusEffects(ActionType.OnSpawn, 1.0f);
|
|
Components.ForEach(c => c.ApplyStatusEffects(ActionType.OnSpawn, 1.0f));
|
|
}
|
|
|
|
partial void InitProjSpecific();
|
|
|
|
public bool IsContainerPreferred(ItemContainer container, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRestriction = false)
|
|
=> Prefab.IsContainerPreferred(this, container, out isPreferencesDefined, out isSecondary, requireConditionRestriction);
|
|
|
|
public override MapEntity Clone()
|
|
{
|
|
Item clone = new Item(rect, Prefab, Submarine, callOnItemLoaded: false)
|
|
{
|
|
defaultRect = defaultRect
|
|
};
|
|
foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
|
|
{
|
|
if (!property.Value.Attributes.OfType<Editable>().Any()) continue;
|
|
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
|
|
}
|
|
|
|
if (components.Count != clone.components.Count)
|
|
{
|
|
string errorMsg = "Error while cloning item \"" + Name + "\" - clone does not have the same number of components. ";
|
|
errorMsg += "Original components: " + string.Join(", ", components.Select(c => c.GetType().ToString()));
|
|
errorMsg += ", cloned components: " + string.Join(", ", clone.components.Select(c => c.GetType().ToString()));
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("Item.Clone:" + Name, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
|
}
|
|
|
|
for (int i = 0; i < components.Count && i < clone.components.Count; i++)
|
|
{
|
|
foreach (KeyValuePair<Identifier, SerializableProperty> property in components[i].SerializableProperties)
|
|
{
|
|
if (!property.Value.Attributes.OfType<Editable>().Any()) continue;
|
|
clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i]));
|
|
}
|
|
|
|
//clone requireditem identifiers
|
|
foreach (var kvp in components[i].requiredItems)
|
|
{
|
|
for (int j = 0; j < kvp.Value.Count; j++)
|
|
{
|
|
if (!clone.components[i].requiredItems.ContainsKey(kvp.Key) ||
|
|
clone.components[i].requiredItems[kvp.Key].Count <= j)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
clone.components[i].requiredItems[kvp.Key][j].JoinedIdentifiers =
|
|
kvp.Value[j].JoinedIdentifiers;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (FlippedX) clone.FlipX(false);
|
|
if (FlippedY) clone.FlipY(false);
|
|
|
|
foreach (ItemComponent component in clone.components)
|
|
{
|
|
component.OnItemLoaded();
|
|
}
|
|
|
|
foreach (Item containedItem in ContainedItems)
|
|
{
|
|
var containedClone = containedItem.Clone();
|
|
clone.ownInventory.TryPutItem(containedClone as Item, null);
|
|
}
|
|
|
|
return clone;
|
|
}
|
|
|
|
public void AddComponent(ItemComponent component)
|
|
{
|
|
allPropertyObjects.Add(component);
|
|
components.Add(component);
|
|
|
|
if (component.IsActive || component.UpdateWhenInactive || component.Parent != null || (component.IsActiveConditionals != null && component.IsActiveConditionals.Any()))
|
|
{
|
|
updateableComponents.Add(component);
|
|
}
|
|
|
|
component.OnActiveStateChanged += (bool isActive) =>
|
|
{
|
|
bool hasSounds = false;
|
|
#if CLIENT
|
|
hasSounds = component.HasSounds;
|
|
#endif
|
|
//component doesn't need to be updated if it isn't active, doesn't have a parent that could activate it,
|
|
//nor status effects, sounds or conditionals that would need to run
|
|
if (!isActive && !component.UpdateWhenInactive &&
|
|
!hasSounds &&
|
|
component.Parent == null &&
|
|
(component.IsActiveConditionals == null || !component.IsActiveConditionals.Any()) &&
|
|
(component.statusEffectLists == null || !component.statusEffectLists.Any()))
|
|
{
|
|
if (updateableComponents.Contains(component)) { updateableComponents.Remove(component); }
|
|
}
|
|
else
|
|
{
|
|
if (!updateableComponents.Contains(component))
|
|
{
|
|
updateableComponents.Add(component);
|
|
this.isActive = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
Type type = component.GetType();
|
|
if (!componentsByType.ContainsKey(type))
|
|
{
|
|
componentsByType.Add(type, component);
|
|
Type baseType = type.BaseType;
|
|
while (baseType != null && baseType != typeof(ItemComponent))
|
|
{
|
|
if (!componentsByType.ContainsKey(baseType))
|
|
{
|
|
componentsByType.Add(baseType, component);
|
|
}
|
|
baseType = baseType.BaseType;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void EnableDrawableComponent(IDrawableComponent drawable)
|
|
{
|
|
if (!drawableComponents.Contains(drawable))
|
|
{
|
|
drawableComponents.Add(drawable);
|
|
hasComponentsToDraw = true;
|
|
#if CLIENT
|
|
cachedVisibleExtents = null;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public void DisableDrawableComponent(IDrawableComponent drawable)
|
|
{
|
|
if (drawableComponents.Contains(drawable))
|
|
{
|
|
drawableComponents.Remove(drawable);
|
|
hasComponentsToDraw = drawableComponents.Count > 0;
|
|
#if CLIENT
|
|
cachedVisibleExtents = null;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public int GetComponentIndex(ItemComponent component)
|
|
{
|
|
return components.IndexOf(component);
|
|
}
|
|
|
|
public T GetComponent<T>() where T : ItemComponent
|
|
{
|
|
if (componentsByType.TryGetValue(typeof(T), out ItemComponent component))
|
|
{
|
|
return (T)component;
|
|
}
|
|
if (typeof(T) == typeof(ItemComponent))
|
|
{
|
|
return (T)components.FirstOrDefault();
|
|
}
|
|
return default;
|
|
}
|
|
|
|
public IEnumerable<T> GetComponents<T>()
|
|
{
|
|
if (typeof(T) == typeof(ItemComponent))
|
|
{
|
|
return components.Cast<T>();
|
|
}
|
|
if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty<T>(); }
|
|
return components.Where(c => c is T).Cast<T>();
|
|
}
|
|
|
|
public float GetQualityModifier(Quality.StatType statType)
|
|
{
|
|
return GetComponent<Quality>()?.GetValue(statType) ?? 0.0f;
|
|
}
|
|
|
|
public void RemoveContained(Item contained)
|
|
{
|
|
ownInventory?.RemoveItem(contained);
|
|
|
|
contained.Container = null;
|
|
}
|
|
|
|
public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true, bool setPrevTransform = true)
|
|
{
|
|
if (!MathUtils.IsValid(simPosition))
|
|
{
|
|
string errorMsg =
|
|
"Attempted to move the item " + Name +
|
|
" to an invalid position (" + simPosition + ")\n" + Environment.StackTrace.CleanupStackTrace();
|
|
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce(
|
|
"Item.SetPosition:InvalidPosition" + ID,
|
|
GameAnalyticsManager.ErrorSeverity.Error,
|
|
errorMsg);
|
|
return;
|
|
}
|
|
|
|
if (body != null)
|
|
{
|
|
#if DEBUG
|
|
try
|
|
{
|
|
#endif
|
|
if (body.PhysEnabled)
|
|
{
|
|
body.SetTransform(simPosition, rotation, setPrevTransform);
|
|
}
|
|
else
|
|
{
|
|
body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform);
|
|
}
|
|
#if DEBUG
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
DebugConsole.ThrowError("Failed to set item transform", e);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
Vector2 displayPos = ConvertUnits.ToDisplayUnits(simPosition);
|
|
|
|
rect.X = (int)(displayPos.X - rect.Width / 2.0f);
|
|
rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);
|
|
|
|
if (findNewHull) { FindHull(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is dropping the item allowed when trying to swap it with the other item
|
|
/// </summary>
|
|
public bool AllowDroppingOnSwapWith(Item otherItem)
|
|
{
|
|
if (!Prefab.AllowDroppingOnSwap || otherItem == null) { return false; }
|
|
if (Prefab.AllowDroppingOnSwapWith.Any())
|
|
{
|
|
foreach (Identifier tagOrIdentifier in Prefab.AllowDroppingOnSwapWith)
|
|
{
|
|
if (otherItem.Prefab.Identifier == tagOrIdentifier) { return true; }
|
|
if (otherItem.HasTag(tagOrIdentifier)) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public void SetActiveSprite()
|
|
{
|
|
SetActiveSpriteProjSpecific();
|
|
}
|
|
|
|
partial void SetActiveSpriteProjSpecific();
|
|
|
|
public override void Move(Vector2 amount)
|
|
{
|
|
Move(amount, ignoreContacts: false);
|
|
}
|
|
|
|
public void Move(Vector2 amount, bool ignoreContacts)
|
|
{
|
|
if (!MathUtils.IsValid(amount))
|
|
{
|
|
DebugConsole.ThrowError($"Attempted to move an item by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}");
|
|
return;
|
|
}
|
|
|
|
base.Move(amount);
|
|
|
|
if (ItemList != null && body != null)
|
|
{
|
|
if (ignoreContacts)
|
|
{
|
|
body.SetTransformIgnoreContacts(body.SimPosition + ConvertUnits.ToSimUnits(amount), body.Rotation);
|
|
}
|
|
else
|
|
{
|
|
body.SetTransform(body.SimPosition + ConvertUnits.ToSimUnits(amount), body.Rotation);
|
|
}
|
|
}
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
ic.Move(amount);
|
|
}
|
|
|
|
if (body != null && (Submarine == null || !Submarine.Loading)) { FindHull(); }
|
|
}
|
|
|
|
public Rectangle TransformTrigger(Rectangle trigger, bool world = false)
|
|
{
|
|
Rectangle baseRect = world ? WorldRect : Rect;
|
|
|
|
Rectangle transformedRect =
|
|
new Rectangle(
|
|
(int)(baseRect.X + trigger.X * Scale),
|
|
(int)(baseRect.Y + trigger.Y * Scale),
|
|
(trigger.Width == 0) ? Rect.Width : (int)(trigger.Width * Scale),
|
|
(trigger.Height == 0) ? Rect.Height : (int)(trigger.Height * Scale));
|
|
|
|
if (FlippedX)
|
|
{
|
|
transformedRect.X = baseRect.X + (baseRect.Right - transformedRect.Right);
|
|
}
|
|
if (FlippedY)
|
|
{
|
|
transformedRect.Y = baseRect.Y + ((baseRect.Y - baseRect.Height) - (transformedRect.Y - transformedRect.Height));
|
|
}
|
|
|
|
return transformedRect;
|
|
}
|
|
|
|
/// <summary>
|
|
/// goes through every item and re-checks which hull they are in
|
|
/// </summary>
|
|
public static void UpdateHulls()
|
|
{
|
|
foreach (Item item in ItemList)
|
|
{
|
|
item.FindHull();
|
|
}
|
|
}
|
|
|
|
public Hull FindHull()
|
|
{
|
|
if (parentInventory != null && parentInventory.Owner != null)
|
|
{
|
|
if (parentInventory.Owner is Character character)
|
|
{
|
|
CurrentHull = character.AnimController.CurrentHull;
|
|
}
|
|
else if (parentInventory.Owner is Item item)
|
|
{
|
|
CurrentHull = item.CurrentHull;
|
|
}
|
|
|
|
Submarine = parentInventory.Owner.Submarine;
|
|
if (body != null) { body.Submarine = Submarine; }
|
|
|
|
return CurrentHull;
|
|
}
|
|
|
|
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull);
|
|
if (body != null && body.Enabled && (body.BodyType == BodyType.Dynamic || Submarine == null))
|
|
{
|
|
Submarine = CurrentHull?.Submarine;
|
|
body.Submarine = Submarine;
|
|
}
|
|
|
|
return CurrentHull;
|
|
}
|
|
|
|
public Item GetRootContainer()
|
|
{
|
|
if (Container == null) { return null; }
|
|
Item rootContainer = Container;
|
|
while (rootContainer.Container != null)
|
|
{
|
|
rootContainer = rootContainer.Container;
|
|
}
|
|
return rootContainer;
|
|
}
|
|
|
|
public bool HasAccess(Character character)
|
|
{
|
|
if (character.IsBot && IgnoreByAI(character)) { return false; }
|
|
if (!IsInteractable(character)) { return false; }
|
|
var itemContainer = GetComponent<ItemContainer>();
|
|
if (itemContainer != null && !itemContainer.HasAccess(character)) { return false; }
|
|
if (Container != null && !Container.HasAccess(character)) { return false; }
|
|
return true;
|
|
}
|
|
|
|
public bool IsOwnedBy(Entity entity) => FindParentInventory(i => i.Owner == entity) != null;
|
|
|
|
public Entity GetRootInventoryOwner()
|
|
{
|
|
if (ParentInventory == null) { return this; }
|
|
if (ParentInventory.Owner is Character) { return ParentInventory.Owner; }
|
|
var rootContainer = GetRootContainer();
|
|
if (rootContainer?.ParentInventory?.Owner is Character) { return rootContainer.ParentInventory.Owner; }
|
|
return rootContainer ?? this;
|
|
}
|
|
|
|
public Inventory FindParentInventory(Func<Inventory, bool> predicate)
|
|
{
|
|
if (parentInventory != null)
|
|
{
|
|
if (predicate(parentInventory))
|
|
{
|
|
return parentInventory;
|
|
}
|
|
if (parentInventory.Owner is Item owner)
|
|
{
|
|
return owner.FindParentInventory(predicate);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public void SetContainedItemPositions()
|
|
{
|
|
foreach (ItemComponent component in components)
|
|
{
|
|
(component as ItemContainer)?.SetContainedItemPositions();
|
|
}
|
|
}
|
|
|
|
public void AddTag(string tag)
|
|
{
|
|
AddTag(tag.ToIdentifier());
|
|
}
|
|
|
|
public void AddTag(Identifier tag)
|
|
{
|
|
if (tags.Contains(tag)) { return; }
|
|
tags.Add(tag);
|
|
}
|
|
|
|
|
|
public bool HasTag(string tag)
|
|
{
|
|
return HasTag(tag.ToIdentifier());
|
|
}
|
|
|
|
public bool HasTag(Identifier tag)
|
|
{
|
|
if (tag == null) { return true; }
|
|
return tags.Contains(tag) || base.Prefab.Tags.Contains(tag);
|
|
}
|
|
|
|
public void ReplaceTag(string tag, string newTag)
|
|
{
|
|
ReplaceTag(tag.ToIdentifier(), newTag.ToIdentifier());
|
|
}
|
|
|
|
public void ReplaceTag(Identifier tag, Identifier newTag)
|
|
{
|
|
if (!tags.Contains(tag)) { return; }
|
|
tags.Remove(tag);
|
|
tags.Add(newTag);
|
|
}
|
|
|
|
public IEnumerable<Identifier> GetTags()
|
|
{
|
|
return tags;
|
|
}
|
|
|
|
public bool HasTag(IEnumerable<Identifier> allowedTags)
|
|
{
|
|
if (allowedTags == null) return true;
|
|
foreach (Identifier tag in allowedTags)
|
|
{
|
|
if (tags.Contains(tag)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool ConditionalMatches(PropertyConditional conditional)
|
|
{
|
|
if (string.IsNullOrEmpty(conditional.TargetItemComponentName))
|
|
{
|
|
if (!conditional.Matches(this)) { return false; }
|
|
}
|
|
else
|
|
{
|
|
foreach (ItemComponent component in components)
|
|
{
|
|
if (component.Name != conditional.TargetItemComponentName) { continue; }
|
|
if (!conditional.Matches(component)) { return false; }
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, Vector2? worldPosition = null)
|
|
{
|
|
if (!hasStatusEffectsOfType[(int)type]) { return; }
|
|
|
|
foreach (StatusEffect effect in statusEffectLists[type])
|
|
{
|
|
ApplyStatusEffect(effect, type, deltaTime, character, limb, useTarget, isNetworkEvent, false, worldPosition);
|
|
}
|
|
}
|
|
|
|
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
|
|
|
public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, bool checkCondition = true, Vector2? worldPosition = null)
|
|
{
|
|
if (!isNetworkEvent && checkCondition)
|
|
{
|
|
if (condition == 0.0f && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { return; }
|
|
}
|
|
if (effect.type != type) { return; }
|
|
|
|
bool hasTargets = effect.TargetIdentifiers == null;
|
|
|
|
targets.Clear();
|
|
|
|
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
|
|
{
|
|
foreach (Item containedItem in ContainedItems)
|
|
{
|
|
if (effect.TargetIdentifiers != null &&
|
|
!effect.TargetIdentifiers.Contains(((MapEntity)containedItem).Prefab.Identifier) &&
|
|
!effect.TargetIdentifiers.Any(id => containedItem.HasTag(id)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (effect.TargetSlot > -1)
|
|
{
|
|
if (OwnInventory.FindIndex(containedItem) != effect.TargetSlot) { continue; }
|
|
}
|
|
|
|
hasTargets = true;
|
|
targets.Add(containedItem);
|
|
}
|
|
}
|
|
|
|
if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters) || effect.HasTargetType(StatusEffect.TargetType.NearbyItems))
|
|
{
|
|
targets.AddRange(effect.GetNearbyTargets(WorldPosition, targets));
|
|
if (targets.Count > 0)
|
|
{
|
|
hasTargets = true;
|
|
}
|
|
}
|
|
|
|
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget) && useTarget is ISerializableEntity serializableTarget)
|
|
{
|
|
hasTargets = true;
|
|
targets.Add(serializableTarget);
|
|
}
|
|
|
|
if (!hasTargets) { return; }
|
|
|
|
if (effect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null)
|
|
{
|
|
targets.Add(CurrentHull);
|
|
}
|
|
|
|
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
|
{
|
|
foreach (var pobject in AllPropertyObjects)
|
|
{
|
|
targets.Add(pobject);
|
|
}
|
|
}
|
|
|
|
if (character != null)
|
|
{
|
|
if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
|
{
|
|
if (type == ActionType.OnContained && ParentInventory is CharacterInventory characterInventory)
|
|
{
|
|
targets.Add(characterInventory.Owner as ISerializableEntity);
|
|
}
|
|
else
|
|
{
|
|
targets.Add(character);
|
|
}
|
|
}
|
|
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
|
{
|
|
targets.AddRange(character.AnimController.Limbs.ToList());
|
|
}
|
|
}
|
|
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
|
{
|
|
targets.Add(limb);
|
|
}
|
|
|
|
if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.Add(Container); }
|
|
|
|
effect.Apply(type, deltaTime, this, targets, worldPosition);
|
|
}
|
|
|
|
|
|
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
|
|
{
|
|
if (Indestructible || InvulnerableToDamage) { return new AttackResult(); }
|
|
|
|
float damageAmount = attack.GetItemDamage(deltaTime);
|
|
Condition -= damageAmount;
|
|
|
|
if (damageAmount >= Prefab.OnDamagedThreshold)
|
|
{
|
|
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
|
|
}
|
|
|
|
return new AttackResult(damageAmount, null);
|
|
}
|
|
|
|
private void SetCondition(float value, bool isNetworkEvent)
|
|
{
|
|
if (!isNetworkEvent)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
}
|
|
if (!MathUtils.IsValid(value)) { return; }
|
|
if (Indestructible) { return; }
|
|
if (InvulnerableToDamage && value <= condition) { return; }
|
|
|
|
float prev = condition;
|
|
bool wasInFullCondition = IsFullCondition;
|
|
|
|
condition = MathHelper.Clamp(value, 0.0f, MaxCondition);
|
|
if (condition == 0.0f && prev > 0.0f)
|
|
{
|
|
//Flag connections to be updated as device is broken
|
|
flagChangedConnections(connections);
|
|
#if CLIENT
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
ic.PlaySound(ActionType.OnBroken);
|
|
}
|
|
if (Screen.Selected == GameMain.SubEditorScreen) { return; }
|
|
#endif
|
|
ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
|
|
}
|
|
else if (condition > 0.0f && prev <= 0.0f)
|
|
{
|
|
//Flag connections to be updated as device is now working again
|
|
flagChangedConnections(connections);
|
|
}
|
|
|
|
SetActiveSprite();
|
|
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
if (Math.Abs(lastSentCondition - condition) > 1.0f)
|
|
{
|
|
conditionUpdatePending = true;
|
|
isActive = true;
|
|
}
|
|
else if (wasInFullCondition != IsFullCondition)
|
|
{
|
|
conditionUpdatePending = true;
|
|
isActive = true;
|
|
}
|
|
else if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= MaxCondition))
|
|
{
|
|
sendConditionUpdateTimer = 0.0f;
|
|
conditionUpdatePending = true;
|
|
isActive = true;
|
|
}
|
|
}
|
|
|
|
LastConditionChange = condition - prev;
|
|
ConditionLastUpdated = Timing.TotalTime;
|
|
|
|
static void flagChangedConnections(Dictionary<string, Connection> connections)
|
|
{
|
|
if (connections == null) { return; }
|
|
foreach (Connection c in connections.Values)
|
|
{
|
|
if (c.IsPower)
|
|
{
|
|
Powered.ChangedConnections.Add(c);
|
|
foreach (Connection conn in c.Recipients)
|
|
{
|
|
Powered.ChangedConnections.Add(conn);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsInWater()
|
|
{
|
|
if (CurrentHull == null) { return true; }
|
|
|
|
float surfaceY = CurrentHull.Surface;
|
|
return CurrentHull.WaterVolume > 0.0f && Position.Y < surfaceY;
|
|
}
|
|
|
|
public void SendPendingNetworkUpdates()
|
|
{
|
|
if (!(GameMain.NetworkMember is { IsServer: true })) { return; }
|
|
if (!conditionUpdatePending) { return; }
|
|
|
|
CreateStatusEvent();
|
|
lastSentCondition = condition;
|
|
sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
|
|
conditionUpdatePending = false;
|
|
}
|
|
|
|
public void CreateStatusEvent()
|
|
{
|
|
GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData());
|
|
}
|
|
|
|
private bool isActive = true;
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
while (impactQueue.TryDequeue(out float impact))
|
|
{
|
|
HandleCollision(impact);
|
|
}
|
|
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (!Submarine?.Loading ?? true))
|
|
{
|
|
sendConditionUpdateTimer -= deltaTime;
|
|
if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f)
|
|
{
|
|
SendPendingNetworkUpdates();
|
|
}
|
|
}
|
|
|
|
if (aiTarget != null)
|
|
{
|
|
aiTarget.Update(deltaTime);
|
|
}
|
|
|
|
if (!isActive) { return; }
|
|
|
|
ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
|
|
ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
|
|
|
|
for (int i = 0; i < updateableComponents.Count; i++)
|
|
{
|
|
ItemComponent ic = updateableComponents[i];
|
|
|
|
if (ic.IsActiveConditionals != null)
|
|
{
|
|
bool shouldBeActive = true;
|
|
foreach (var conditional in ic.IsActiveConditionals)
|
|
{
|
|
if (!ConditionalMatches(conditional))
|
|
{
|
|
shouldBeActive = false;
|
|
break;
|
|
}
|
|
}
|
|
ic.IsActive = shouldBeActive;
|
|
}
|
|
#if CLIENT
|
|
if (ic.HasSounds)
|
|
{
|
|
ic.PlaySound(ActionType.Always);
|
|
ic.UpdateSounds();
|
|
if (!ic.WasUsed) { ic.StopSounds(ActionType.OnUse); }
|
|
if (!ic.WasSecondaryUsed) { ic.StopSounds(ActionType.OnSecondaryUse); }
|
|
}
|
|
#endif
|
|
ic.WasUsed = false;
|
|
ic.WasSecondaryUsed = false;
|
|
|
|
if (ic.IsActive || ic.UpdateWhenInactive)
|
|
{
|
|
if (condition <= 0.0f)
|
|
{
|
|
ic.UpdateBroken(deltaTime, cam);
|
|
}
|
|
else
|
|
{
|
|
ic.Update(deltaTime, cam);
|
|
#if CLIENT
|
|
if (ic.IsActive)
|
|
{
|
|
if (ic.IsActiveTimer > 0.02f)
|
|
{
|
|
ic.PlaySound(ActionType.OnActive);
|
|
}
|
|
ic.IsActiveTimer += deltaTime;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Removed) { return; }
|
|
|
|
bool needsWaterCheck = hasWaterStatusEffects;
|
|
if (body != null && body.Enabled)
|
|
{
|
|
System.Diagnostics.Debug.Assert(body.FarseerBody.FixtureList != null);
|
|
|
|
if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f || transformDirty)
|
|
{
|
|
UpdateTransform();
|
|
if (CurrentHull == null && Level.Loaded != null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth))
|
|
{
|
|
Spawner?.AddItemToRemoveQueue(this);
|
|
return;
|
|
}
|
|
}
|
|
needsWaterCheck = true;
|
|
UpdateNetPosition(deltaTime);
|
|
if (inWater)
|
|
{
|
|
ApplyWaterForces();
|
|
CurrentHull?.ApplyFlowForces(deltaTime, this);
|
|
}
|
|
}
|
|
|
|
if (needsWaterCheck)
|
|
{
|
|
inWater = IsInWater();
|
|
bool waterProof = WaterProof;
|
|
if (inWater)
|
|
{
|
|
Item container = this.Container;
|
|
while (!waterProof && container != null)
|
|
{
|
|
waterProof = container.WaterProof;
|
|
container = container.Container;
|
|
}
|
|
}
|
|
if (hasWaterStatusEffects && condition > 0.0f)
|
|
{
|
|
ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (updateableComponents.Count == 0 && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled))
|
|
{
|
|
#if CLIENT
|
|
positionBuffer.Clear();
|
|
#endif
|
|
isActive = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public void UpdateTransform()
|
|
{
|
|
if (body == null) { return; }
|
|
Submarine prevSub = Submarine;
|
|
|
|
var projectile = GetComponent<Projectile>();
|
|
if (projectile?.StickTarget != null)
|
|
{
|
|
if (projectile?.StickTarget.UserData is Limb limb && limb.character != null)
|
|
{
|
|
Submarine = body.Submarine = limb.character.Submarine;
|
|
currentHull = limb.character.CurrentHull;
|
|
}
|
|
else if (projectile.StickTarget.UserData is Structure structure)
|
|
{
|
|
Submarine = body.Submarine = structure.Submarine;
|
|
currentHull = Hull.FindHull(WorldPosition, CurrentHull);
|
|
}
|
|
else if (projectile.StickTarget.UserData is Item targetItem)
|
|
{
|
|
Submarine = body.Submarine = targetItem.Submarine;
|
|
currentHull = targetItem.CurrentHull;
|
|
}
|
|
else if (projectile.StickTarget.UserData is Submarine)
|
|
{
|
|
//attached to a sub from the outside -> don't move inside the sub
|
|
Submarine = body.Submarine = null;
|
|
currentHull = null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FindHull();
|
|
}
|
|
|
|
if (Submarine == null && prevSub != null)
|
|
{
|
|
body.SetTransform(body.SimPosition + prevSub.SimPosition, body.Rotation);
|
|
}
|
|
else if (Submarine != null && prevSub == null)
|
|
{
|
|
body.SetTransform(body.SimPosition - Submarine.SimPosition, body.Rotation);
|
|
}
|
|
else if (Submarine != null && prevSub != null && Submarine != prevSub)
|
|
{
|
|
body.SetTransform(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation);
|
|
}
|
|
|
|
if (Submarine != prevSub)
|
|
{
|
|
foreach (Item containedItem in ContainedItems)
|
|
{
|
|
if (containedItem == null) { continue; }
|
|
containedItem.Submarine = Submarine;
|
|
}
|
|
}
|
|
|
|
Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition);
|
|
rect.X = (int)(displayPos.X - rect.Width / 2.0f);
|
|
rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);
|
|
|
|
if (Math.Abs(body.LinearVelocity.X) > NetConfig.MaxPhysicsBodyVelocity ||
|
|
Math.Abs(body.LinearVelocity.Y) > NetConfig.MaxPhysicsBodyVelocity)
|
|
{
|
|
body.LinearVelocity = new Vector2(
|
|
MathHelper.Clamp(body.LinearVelocity.X, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity),
|
|
MathHelper.Clamp(body.LinearVelocity.Y, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity));
|
|
}
|
|
|
|
transformDirty = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies buoyancy, drag and angular drag caused by water
|
|
/// </summary>
|
|
private void ApplyWaterForces()
|
|
{
|
|
if (body.Mass <= 0.0f || body.Density <= 0.0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float forceFactor = 1.0f;
|
|
if (CurrentHull != null)
|
|
{
|
|
float floor = CurrentHull.Rect.Y - CurrentHull.Rect.Height;
|
|
float waterLevel = floor + CurrentHull.WaterVolume / CurrentHull.Rect.Width;
|
|
|
|
//forceFactor is 1.0f if the item is completely submerged,
|
|
//and goes to 0.0f as the item goes through the surface
|
|
forceFactor = Math.Min((waterLevel - Position.Y) / rect.Height, 1.0f);
|
|
if (forceFactor <= 0.0f) return;
|
|
}
|
|
|
|
float volume = body.Mass / body.Density;
|
|
|
|
var uplift = -GameMain.World.Gravity * forceFactor * volume;
|
|
|
|
Vector2 drag = body.LinearVelocity * volume;
|
|
|
|
body.ApplyForce((uplift - drag) * 10.0f);
|
|
|
|
//apply simple angular drag
|
|
body.ApplyTorque(body.AngularVelocity * volume * -0.05f);
|
|
}
|
|
|
|
|
|
private bool OnCollision(Fixture f1, Fixture f2, Contact contact)
|
|
{
|
|
if (transformDirty) { return false; }
|
|
|
|
var projectile = GetComponent<Projectile>();
|
|
if (projectile != null)
|
|
{
|
|
//ignore character colliders (a projectile only hits limbs)
|
|
if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) { return false; }
|
|
if (projectile.IgnoredBodies != null && projectile.IgnoredBodies.Contains(f2.Body)) { return false; }
|
|
if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; }
|
|
}
|
|
|
|
contact.GetWorldManifold(out Vector2 normal, out _);
|
|
if (contact.FixtureA.Body == f1.Body) { normal = -normal; }
|
|
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
|
|
|
|
impactQueue.Enqueue(impact);
|
|
|
|
return true;
|
|
}
|
|
|
|
private void HandleCollision(float impact)
|
|
{
|
|
OnCollisionProjSpecific(impact);
|
|
if (GameMain.NetworkMember is { IsClient: true }) { return; }
|
|
|
|
if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance)
|
|
{
|
|
ApplyStatusEffects(ActionType.OnImpact, 1.0f);
|
|
#if SERVER
|
|
GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact));
|
|
#endif
|
|
}
|
|
|
|
foreach (Item contained in ContainedItems)
|
|
{
|
|
if (contained.body != null) { contained.HandleCollision(impact); }
|
|
}
|
|
}
|
|
|
|
partial void OnCollisionProjSpecific(float impact);
|
|
|
|
public override void FlipX(bool relativeToSub)
|
|
{
|
|
//call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
|
|
base.FlipX(relativeToSub);
|
|
|
|
if (!Prefab.CanFlipX)
|
|
{
|
|
flippedX = false;
|
|
return;
|
|
}
|
|
|
|
if (Prefab.AllowRotatingInEditor)
|
|
{
|
|
rotationRad = MathUtils.WrapAngleTwoPi(-rotationRad);
|
|
}
|
|
#if CLIENT
|
|
if (Prefab.CanSpriteFlipX)
|
|
{
|
|
SpriteEffects ^= SpriteEffects.FlipHorizontally;
|
|
}
|
|
#endif
|
|
|
|
foreach (ItemComponent component in components)
|
|
{
|
|
component.FlipX(relativeToSub);
|
|
}
|
|
SetContainedItemPositions();
|
|
}
|
|
|
|
public override void FlipY(bool relativeToSub)
|
|
{
|
|
//call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
|
|
base.FlipY(relativeToSub);
|
|
|
|
if (!Prefab.CanFlipY)
|
|
{
|
|
flippedY = false;
|
|
return;
|
|
}
|
|
|
|
#if CLIENT
|
|
if (Prefab.CanSpriteFlipY)
|
|
{
|
|
SpriteEffects ^= SpriteEffects.FlipVertically;
|
|
}
|
|
#endif
|
|
|
|
foreach (ItemComponent component in components)
|
|
{
|
|
component.FlipY(relativeToSub);
|
|
}
|
|
SetContainedItemPositions();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Note: This function generates garbage and might be a bit too heavy to be used once per frame.
|
|
/// </summary>
|
|
public List<T> GetConnectedComponents<T>(bool recursive = false, bool allowTraversingBackwards = true) where T : ItemComponent
|
|
{
|
|
List<T> connectedComponents = new List<T>();
|
|
|
|
if (recursive)
|
|
{
|
|
HashSet<Connection> alreadySearched = new HashSet<Connection>();
|
|
GetConnectedComponentsRecursive(alreadySearched, connectedComponents, allowTraversingBackwards: allowTraversingBackwards);
|
|
return connectedComponents;
|
|
}
|
|
|
|
ConnectionPanel connectionPanel = GetComponent<ConnectionPanel>();
|
|
if (connectionPanel == null) { return connectedComponents; }
|
|
|
|
foreach (Connection c in connectionPanel.Connections)
|
|
{
|
|
var recipients = c.Recipients;
|
|
foreach (Connection recipient in recipients)
|
|
{
|
|
var component = recipient.Item.GetComponent<T>();
|
|
if (component != null && !connectedComponents.Contains(component))
|
|
{
|
|
connectedComponents.Add(component);
|
|
}
|
|
}
|
|
}
|
|
|
|
return connectedComponents;
|
|
}
|
|
|
|
private void GetConnectedComponentsRecursive<T>(HashSet<Connection> alreadySearched, List<T> connectedComponents, bool ignoreInactiveRelays = false, bool allowTraversingBackwards = true) where T : ItemComponent
|
|
{
|
|
ConnectionPanel connectionPanel = GetComponent<ConnectionPanel>();
|
|
if (connectionPanel == null) { return; }
|
|
|
|
foreach (Connection c in connectionPanel.Connections)
|
|
{
|
|
if (alreadySearched.Contains(c)) { continue; }
|
|
alreadySearched.Add(c);
|
|
GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Note: This function generates garbage and might be a bit too heavy to be used once per frame.
|
|
/// </summary>
|
|
public List<T> GetConnectedComponentsRecursive<T>(Connection c, bool ignoreInactiveRelays = false, bool allowTraversingBackwards = true) where T : ItemComponent
|
|
{
|
|
List<T> connectedComponents = new List<T>();
|
|
HashSet<Connection> alreadySearched = new HashSet<Connection>();
|
|
GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
|
|
|
|
return connectedComponents;
|
|
}
|
|
|
|
public static readonly ImmutableArray<(Identifier Input, Identifier Output)> connectionPairs = new (Identifier, Identifier)[]
|
|
{
|
|
("power_in".ToIdentifier(), "power_out".ToIdentifier()),
|
|
("signal_in1".ToIdentifier(), "signal_out1".ToIdentifier()),
|
|
("signal_in2".ToIdentifier(), "signal_out2".ToIdentifier()),
|
|
("signal_in3".ToIdentifier(), "signal_out3".ToIdentifier()),
|
|
("signal_in4".ToIdentifier(), "signal_out4".ToIdentifier()),
|
|
("signal_in".ToIdentifier(), "signal_out".ToIdentifier()),
|
|
("signal_in1".ToIdentifier(), "signal_out".ToIdentifier()),
|
|
("signal_in2".ToIdentifier(), "signal_out".ToIdentifier())
|
|
}.ToImmutableArray();
|
|
|
|
private void GetConnectedComponentsRecursive<T>(Connection c, HashSet<Connection> alreadySearched, List<T> connectedComponents, bool ignoreInactiveRelays, bool allowTraversingBackwards = true) where T : ItemComponent
|
|
{
|
|
alreadySearched.Add(c);
|
|
|
|
var recipients = c.Recipients;
|
|
foreach (Connection recipient in recipients)
|
|
{
|
|
if (alreadySearched.Contains(recipient)) { continue; }
|
|
var component = recipient.Item.GetComponent<T>();
|
|
if (component != null && !connectedComponents.Contains(component))
|
|
{
|
|
connectedComponents.Add(component);
|
|
}
|
|
|
|
//connected to a wifi component -> see which other wifi components it can communicate with
|
|
var wifiComponent = recipient.Item.GetComponent<WifiComponent>();
|
|
if (wifiComponent != null && wifiComponent.CanTransmit())
|
|
{
|
|
foreach (var wifiReceiver in wifiComponent.GetTransmittersInRange())
|
|
{
|
|
var receiverConnections = wifiReceiver.Item.Connections;
|
|
if (receiverConnections == null) { continue; }
|
|
foreach (Connection wifiOutput in receiverConnections)
|
|
{
|
|
if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; }
|
|
GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
|
|
}
|
|
}
|
|
}
|
|
|
|
recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
|
|
}
|
|
|
|
if (ignoreInactiveRelays)
|
|
{
|
|
var relay = GetComponent<RelayComponent>();
|
|
if (relay != null && !relay.IsOn) { return; }
|
|
}
|
|
|
|
foreach ((Identifier input, Identifier output) in connectionPairs)
|
|
{
|
|
void searchFromAToB(Identifier connectionEndA, Identifier connectionEndB)
|
|
{
|
|
if (connectionEndA == c.Name)
|
|
{
|
|
var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == connectionEndB);
|
|
if (pairedConnection != null)
|
|
{
|
|
if (alreadySearched.Contains(pairedConnection)) { return; }
|
|
GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
|
|
}
|
|
}
|
|
}
|
|
searchFromAToB(input, output);
|
|
if (allowTraversingBackwards) { searchFromAToB(output, input); }
|
|
}
|
|
}
|
|
|
|
public Controller FindController(ImmutableArray<Identifier>? tags = null)
|
|
{
|
|
//try finding the controller with the simpler non-recursive method first
|
|
var controllers = GetConnectedComponents<Controller>();
|
|
bool needsTag = tags != null && tags.Value.Length > 0;
|
|
if (controllers.None() || (needsTag && controllers.None(c => c.Item.HasTag(tags))))
|
|
{
|
|
controllers = GetConnectedComponents<Controller>(recursive: true);
|
|
}
|
|
if (needsTag)
|
|
{
|
|
controllers.RemoveAll(c => !c.Item.HasTag(tags));
|
|
}
|
|
return controllers.Count < 2 ?
|
|
controllers.FirstOrDefault() :
|
|
controllers.FirstOrDefault(c => c.GetFocusTarget() == this) ?? controllers.FirstOrDefault();
|
|
}
|
|
|
|
public bool TryFindController(out Controller controller, ImmutableArray<Identifier>? tags = null)
|
|
{
|
|
controller = FindController(tags: tags);
|
|
return controller != null;
|
|
}
|
|
|
|
public void SendSignal(string signal, string connectionName)
|
|
{
|
|
SendSignal(new Signal(signal), connectionName);
|
|
}
|
|
|
|
public void SendSignal(Signal signal, string connectionName)
|
|
{
|
|
if (connections == null) { return; }
|
|
if (!connections.TryGetValue(connectionName, out Connection connection)) { return; }
|
|
|
|
signal.source ??= this;
|
|
SendSignal(signal, connection);
|
|
}
|
|
|
|
private readonly HashSet<(Signal Signal, Connection Connection)> delayedSignals = new HashSet<(Signal Signal, Connection Connection)>();
|
|
|
|
public void SendSignal(Signal signal, Connection connection)
|
|
{
|
|
LastSentSignalRecipients.Clear();
|
|
if (connections == null || connection == null) { return; }
|
|
|
|
signal.stepsTaken++;
|
|
|
|
//if the signal has been passed through this item multiple times already, interrupt it to prevent infinite loops
|
|
if (signal.stepsTaken > 5 && signal.source != null)
|
|
{
|
|
int duplicateRecipients = 0;
|
|
foreach (var recipient in signal.source.LastSentSignalRecipients)
|
|
{
|
|
if (recipient == connection)
|
|
{
|
|
duplicateRecipients++;
|
|
if (duplicateRecipients > 2) { return; }
|
|
}
|
|
}
|
|
}
|
|
|
|
//use a coroutine to prevent infinite loops by creating a one
|
|
//frame delay if the "signal chain" gets too long
|
|
if (signal.stepsTaken > 10)
|
|
{
|
|
//if there's an equal signal waiting to be sent
|
|
//to the same connection, don't add a new one
|
|
signal.stepsTaken = 0;
|
|
bool duplicateFound = false;
|
|
foreach (var s in delayedSignals)
|
|
{
|
|
if (s.Connection == connection && s.Signal.source == signal.source && s.Signal.value == signal.value && s.Signal.sender == signal.sender)
|
|
{
|
|
duplicateFound = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!duplicateFound)
|
|
{
|
|
delayedSignals.Add((signal, connection));
|
|
CoroutineManager.StartCoroutine(DelaySignal(signal, connection));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (connection.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value))
|
|
{
|
|
foreach (StatusEffect effect in connection.Effects)
|
|
{
|
|
if (condition <= 0.0f && effect.type != ActionType.OnBroken) { continue; }
|
|
ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
|
|
}
|
|
}
|
|
signal.source ??= this;
|
|
connection.SendSignal(signal);
|
|
}
|
|
}
|
|
|
|
private IEnumerable<CoroutineStatus> DelaySignal(Signal signal, Connection connection)
|
|
{
|
|
do
|
|
{
|
|
//wait at least one frame
|
|
yield return CoroutineStatus.Running;
|
|
} while (CoroutineManager.DeltaTime <= 0.0f);
|
|
|
|
delayedSignals.Remove((signal, connection));
|
|
connection.SendSignal(signal);
|
|
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
public bool IsInsideTrigger(Vector2 worldPosition)
|
|
{
|
|
return IsInsideTrigger(worldPosition, out _);
|
|
}
|
|
|
|
public bool IsInsideTrigger(Vector2 worldPosition, out Rectangle transformedTrigger)
|
|
{
|
|
foreach (Rectangle trigger in Prefab.Triggers)
|
|
{
|
|
transformedTrigger = TransformTrigger(trigger, true);
|
|
if (Submarine.RectContains(transformedTrigger, worldPosition)) { return true; }
|
|
}
|
|
|
|
transformedTrigger = Rectangle.Empty;
|
|
return false;
|
|
}
|
|
|
|
public bool CanClientAccess(Client c)
|
|
{
|
|
return c != null && c.Character != null && c.Character.CanInteractWith(this);
|
|
}
|
|
|
|
public bool TryInteract(Character user, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceUseKey = false)
|
|
{
|
|
if (CampaignMode.BlocksInteraction(CampaignInteractionType))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool picked = false, selected = false;
|
|
#if CLIENT
|
|
bool hasRequiredSkills = true;
|
|
Skill requiredSkill = null;
|
|
float skillMultiplier = 1;
|
|
#endif
|
|
if (!IsInteractable(user)) { return false; }
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
bool pickHit = false, selectHit = false;
|
|
if (user.IsKeyDown(InputType.Aim))
|
|
{
|
|
pickHit = false;
|
|
selectHit = false;
|
|
}
|
|
else
|
|
{
|
|
if (forceSelectKey)
|
|
{
|
|
if (ic.PickKey == InputType.Select)
|
|
{
|
|
pickHit = true;
|
|
}
|
|
if (ic.SelectKey == InputType.Select)
|
|
{
|
|
selectHit = true;
|
|
}
|
|
}
|
|
else if (forceUseKey)
|
|
{
|
|
if (ic.PickKey == InputType.Use)
|
|
{
|
|
pickHit = true;
|
|
}
|
|
if (ic.SelectKey == InputType.Use)
|
|
{
|
|
selectHit = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pickHit = user.IsKeyHit(ic.PickKey);
|
|
selectHit = user.IsKeyHit(ic.SelectKey);
|
|
|
|
#if CLIENT
|
|
//if the cursor is on a UI component, disable interaction with the left mouse button
|
|
//to prevent accidentally selecting items when clicking UI elements
|
|
if (user == Character.Controlled && GUI.MouseOn != null)
|
|
{
|
|
if (GameSettings.CurrentConfig.KeyMap.Bindings[ic.PickKey].MouseButton == 0)
|
|
{
|
|
pickHit = false;
|
|
}
|
|
|
|
if (GameSettings.CurrentConfig.KeyMap.Bindings[ic.SelectKey].MouseButton == 0)
|
|
{
|
|
selectHit = false;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
#if CLIENT
|
|
//use the non-mouse interaction key (E on both default and legacy keybinds) in wiring mode
|
|
//LMB is used to manipulate wires, so using E to select connection panels is much easier
|
|
if (Screen.Selected == GameMain.SubEditorScreen && GameMain.SubEditorScreen.WiringMode)
|
|
{
|
|
pickHit = selectHit = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use].MouseButton == MouseButton.None ?
|
|
user.IsKeyHit(InputType.Use) :
|
|
user.IsKeyHit(InputType.Select);
|
|
}
|
|
#endif
|
|
if (!pickHit && !selectHit) { continue; }
|
|
|
|
bool showUiMsg = false;
|
|
#if CLIENT
|
|
if (!ic.HasRequiredSkills(user, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); }
|
|
showUiMsg = user == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen;
|
|
#endif
|
|
if (!ignoreRequiredItems && !ic.HasRequiredItems(user, showUiMsg)) { continue; }
|
|
if ((ic.CanBePicked && pickHit && ic.Pick(user)) ||
|
|
(ic.CanBeSelected && selectHit && ic.Select(user)))
|
|
{
|
|
picked = true;
|
|
ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, user);
|
|
#if CLIENT
|
|
if (user == Character.Controlled) { GUI.ForceMouseOn(null); }
|
|
if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; }
|
|
#endif
|
|
if (ic.CanBeSelected && !(ic is Door)) { selected = true; }
|
|
}
|
|
}
|
|
|
|
if (!picked) { return false; }
|
|
|
|
if (user != null)
|
|
{
|
|
if (user.SelectedConstruction == this)
|
|
{
|
|
if (user.IsKeyHit(InputType.Select) || forceSelectKey)
|
|
{
|
|
user.SelectedConstruction = null;
|
|
}
|
|
}
|
|
else if (selected)
|
|
{
|
|
user.SelectedConstruction = this;
|
|
}
|
|
}
|
|
|
|
#if CLIENT
|
|
if (!hasRequiredSkills && Character.Controlled == user && Screen.Selected != GameMain.SubEditorScreen)
|
|
{
|
|
if (requiredSkill != null)
|
|
{
|
|
GUI.AddMessage(TextManager.GetWithVariables("InsufficientSkills",
|
|
("[requiredskill]", TextManager.Get("SkillName." + requiredSkill.Identifier), FormatCapitals.Yes),
|
|
("[requiredlevel]", ((int)(requiredSkill.Level * skillMultiplier)).ToString(), FormatCapitals.No)), GUIStyle.Red);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (Container != null)
|
|
{
|
|
Container.RemoveContained(this);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public float GetContainedItemConditionPercentage()
|
|
{
|
|
if (ownInventory == null) { return -1; }
|
|
|
|
float condition = 0f;
|
|
float maxCondition = 0f;
|
|
foreach (Item item in ContainedItems)
|
|
{
|
|
condition += item.condition;
|
|
maxCondition += item.MaxCondition;
|
|
}
|
|
if (maxCondition > 0.0f)
|
|
{
|
|
return condition / maxCondition;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public void Use(float deltaTime, Character character = null, Limb targetLimb = null)
|
|
{
|
|
if (RequireAimToUse && (character == null || !character.IsKeyDown(InputType.Aim)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (condition == 0.0f) { return; }
|
|
|
|
var should = GameMain.LuaCs.Hook.Call<bool?>("item.use", new object[] { this, character, targetLimb });
|
|
|
|
if (should != null && should.Value)
|
|
return;
|
|
|
|
bool remove = false;
|
|
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
bool isControlled = false;
|
|
#if CLIENT
|
|
isControlled = character == Character.Controlled;
|
|
#endif
|
|
if (!ic.HasRequiredContainedItems(character, isControlled)) { continue; }
|
|
if (ic.Use(deltaTime, character))
|
|
{
|
|
ic.WasUsed = true;
|
|
|
|
#if CLIENT
|
|
ic.PlaySound(ActionType.OnUse, character);
|
|
#endif
|
|
|
|
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb);
|
|
|
|
if (ic.DeleteOnUse) { remove = true; }
|
|
}
|
|
}
|
|
|
|
if (remove)
|
|
{
|
|
Spawner.AddItemToRemoveQueue(this);
|
|
}
|
|
}
|
|
|
|
public void SecondaryUse(float deltaTime, Character character = null)
|
|
{
|
|
if (condition == 0.0f) { return; }
|
|
|
|
var should = GameMain.LuaCs.Hook.Call<bool?>("item.secondaryUse", this, character);
|
|
|
|
if (should != null && should.Value)
|
|
return;
|
|
|
|
bool remove = false;
|
|
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
bool isControlled = false;
|
|
#if CLIENT
|
|
isControlled = character == Character.Controlled;
|
|
#endif
|
|
if (!ic.HasRequiredContainedItems(character, isControlled)) { continue; }
|
|
if (ic.SecondaryUse(deltaTime, character))
|
|
{
|
|
ic.WasSecondaryUsed = true;
|
|
|
|
#if CLIENT
|
|
ic.PlaySound(ActionType.OnSecondaryUse, character);
|
|
#endif
|
|
|
|
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character);
|
|
|
|
if (ic.DeleteOnUse) { remove = true; }
|
|
}
|
|
}
|
|
|
|
if (remove)
|
|
{
|
|
Spawner.AddItemToRemoveQueue(this);
|
|
}
|
|
}
|
|
|
|
public void ApplyTreatment(Character user, Character character, Limb targetLimb)
|
|
{
|
|
//can't apply treatment to dead characters
|
|
if (character.IsDead) { return; }
|
|
if (!UseInHealthInterface) { return; }
|
|
|
|
#if CLIENT
|
|
if (GameMain.Client != null)
|
|
{
|
|
GameMain.Client.CreateEntityEvent(this, new TreatmentEventData(character, targetLimb));
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
float applyOnSelfFraction = user?.GetStatValue(StatTypes.ApplyTreatmentsOnSelfFraction) ?? 0.0f;
|
|
|
|
bool remove = false;
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
if (!ic.HasRequiredContainedItems(user, addMessage: user == Character.Controlled)) { continue; }
|
|
|
|
bool success = Rand.Range(0.0f, 0.5f) < ic.DegreeOfSuccess(user);
|
|
ActionType actionType = success ? ActionType.OnUse : ActionType.OnFailure;
|
|
|
|
#if CLIENT
|
|
ic.PlaySound(actionType, user);
|
|
#endif
|
|
ic.WasUsed = true;
|
|
ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user, applyOnUserFraction: applyOnSelfFraction);
|
|
|
|
if (applyOnSelfFraction > 0.0f)
|
|
{
|
|
//hacky af
|
|
ic.statusEffectLists.TryGetValue(actionType, out var effectList);
|
|
if (effectList != null)
|
|
{
|
|
effectList.ForEach(e => e.AfflictionMultiplier = applyOnSelfFraction);
|
|
ic.ApplyStatusEffects(actionType, 1.0f, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), user: user);
|
|
effectList.ForEach(e => e.AfflictionMultiplier = 1.0f);
|
|
}
|
|
}
|
|
|
|
if (GameMain.NetworkMember is { IsServer: true })
|
|
{
|
|
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(
|
|
actionType, ic, character, targetLimb));
|
|
}
|
|
|
|
if (ic.DeleteOnUse) { remove = true; }
|
|
}
|
|
|
|
if (user != null)
|
|
{
|
|
var abilityItem = new AbilityApplyTreatment(user, character, this);
|
|
user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem);
|
|
|
|
}
|
|
|
|
if (remove) { Spawner?.AddItemToRemoveQueue(this); }
|
|
}
|
|
|
|
public bool Combine(Item item, Character user)
|
|
{
|
|
if (item == this) { return false; }
|
|
bool isCombined = false;
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
if (ic.Combine(item, user)) { isCombined = true; }
|
|
}
|
|
#if CLIENT
|
|
if (isCombined) { GameMain.Client?.CreateEntityEvent(this, new CombineEventData(item)); }
|
|
#endif
|
|
return isCombined;
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="dropper">Character who dropped the item</param>
|
|
/// <param name="createNetworkEvent">Should clients be notified of the item being dropped</param>
|
|
/// <param name="setTransform">Should the transform of the physics body be updated. Only disable this if you're moving the item somewhere else / calling SetTransform manually immediately after dropping!</param>
|
|
public void Drop(Character dropper, bool createNetworkEvent = true, bool setTransform = true)
|
|
{
|
|
if (createNetworkEvent)
|
|
{
|
|
if (parentInventory != null && !parentInventory.Owner.Removed && !Removed &&
|
|
GameMain.NetworkMember != null && (GameMain.NetworkMember.IsServer || Character.Controlled == dropper))
|
|
{
|
|
parentInventory.CreateNetworkEvent();
|
|
//send frequent updates after the item has been dropped
|
|
PositionUpdateInterval = 0.0f;
|
|
}
|
|
}
|
|
|
|
if (body != null)
|
|
{
|
|
isActive = true;
|
|
body.Enabled = true;
|
|
body.PhysEnabled = true;
|
|
body.ResetDynamics();
|
|
if (dropper != null)
|
|
{
|
|
if (body.Removed)
|
|
{
|
|
DebugConsole.ThrowError(
|
|
"Failed to drop the item \"" + Name + "\" (body has been removed"
|
|
+ (Removed ? ", item has been removed)" : ")"));
|
|
}
|
|
else if (setTransform)
|
|
{
|
|
body.SetTransform(dropper.SimPosition, 0.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (ItemComponent ic in components) { ic.Drop(dropper); }
|
|
|
|
if (Container != null)
|
|
{
|
|
if (setTransform)
|
|
{
|
|
SetTransform(Container.SimPosition, 0.0f);
|
|
}
|
|
Container.RemoveContained(this);
|
|
Container = null;
|
|
}
|
|
|
|
if (parentInventory != null)
|
|
{
|
|
parentInventory.RemoveItem(this);
|
|
parentInventory = null;
|
|
}
|
|
|
|
SetContainedItemPositions();
|
|
}
|
|
|
|
public void Equip(Character character)
|
|
{
|
|
if (Removed)
|
|
{
|
|
DebugConsole.ThrowError($"Tried to equip a removed item ({Name}).\n{Environment.StackTrace.CleanupStackTrace()}");
|
|
return;
|
|
}
|
|
|
|
foreach (ItemComponent ic in components) { ic.Equip(character); }
|
|
}
|
|
|
|
public void Unequip(Character character)
|
|
{
|
|
foreach (ItemComponent ic in components) { ic.Unequip(character); }
|
|
}
|
|
|
|
public List<Pair<object, SerializableProperty>> GetProperties<T>()
|
|
{
|
|
List<Pair<object, SerializableProperty>> allProperties = new List<Pair<object, SerializableProperty>>();
|
|
|
|
List<SerializableProperty> itemProperties = SerializableProperty.GetProperties<T>(this);
|
|
foreach (var itemProperty in itemProperties)
|
|
{
|
|
allProperties.Add(new Pair<object, SerializableProperty>(this, itemProperty));
|
|
}
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
List<SerializableProperty> componentProperties = SerializableProperty.GetProperties<T>(ic);
|
|
foreach (var componentProperty in componentProperties)
|
|
{
|
|
allProperties.Add(new Pair<object, SerializableProperty>(ic, componentProperty));
|
|
}
|
|
}
|
|
return allProperties;
|
|
}
|
|
|
|
private void WritePropertyChange(IWriteMessage msg, ChangePropertyEventData extraData, bool inGameEditableOnly)
|
|
{
|
|
//ignoreConditions: true = include all ConditionallyEditable properties at this point,
|
|
//to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients
|
|
var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
|
|
SerializableProperty property = extraData.SerializableProperty;
|
|
if (property != null)
|
|
{
|
|
var propertyOwner = allProperties.Find(p => p.Second == property);
|
|
if (allProperties.Count > 1)
|
|
{
|
|
msg.Write((byte)allProperties.FindIndex(p => p.Second == property));
|
|
}
|
|
|
|
object value = property.GetValue(propertyOwner.First);
|
|
if (value is string stringVal)
|
|
{
|
|
msg.Write(stringVal);
|
|
}
|
|
else if (value is Identifier idValue)
|
|
{
|
|
msg.Write(idValue);
|
|
}
|
|
else if (value is float floatVal)
|
|
{
|
|
msg.Write(floatVal);
|
|
}
|
|
else if (value is int intVal)
|
|
{
|
|
msg.Write(intVal);
|
|
}
|
|
else if (value is bool boolVal)
|
|
{
|
|
msg.Write(boolVal);
|
|
}
|
|
else if (value is Color color)
|
|
{
|
|
msg.Write(color.R);
|
|
msg.Write(color.G);
|
|
msg.Write(color.B);
|
|
msg.Write(color.A);
|
|
}
|
|
else if (value is Vector2 vector2)
|
|
{
|
|
msg.Write(vector2.X);
|
|
msg.Write(vector2.Y);
|
|
}
|
|
else if (value is Vector3 vector3)
|
|
{
|
|
msg.Write(vector3.X);
|
|
msg.Write(vector3.Y);
|
|
msg.Write(vector3.Z);
|
|
}
|
|
else if (value is Vector4 vector4)
|
|
{
|
|
msg.Write(vector4.X);
|
|
msg.Write(vector4.Y);
|
|
msg.Write(vector4.Z);
|
|
msg.Write(vector4.W);
|
|
}
|
|
else if (value is Point point)
|
|
{
|
|
msg.Write(point.X);
|
|
msg.Write(point.Y);
|
|
}
|
|
else if (value is Rectangle rect)
|
|
{
|
|
msg.Write(rect.X);
|
|
msg.Write(rect.Y);
|
|
msg.Write(rect.Width);
|
|
msg.Write(rect.Height);
|
|
}
|
|
else if (value is Enum)
|
|
{
|
|
msg.Write((int)value);
|
|
}
|
|
else if (value is string[] a)
|
|
{
|
|
msg.Write(a.Length);
|
|
for (int i = 0; i < a.Length; i++)
|
|
{
|
|
msg.Write(a[i] ?? "");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new NotImplementedException("Serializing item properties of the type \"" + value.GetType() + "\" not supported");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException("Failed to write propery value - property \"" + (property == null ? "null" : property.Name) + "\" is not serializable.");
|
|
}
|
|
}
|
|
|
|
private List<Pair<object, SerializableProperty>> GetInGameEditableProperties(bool ignoreConditions = false)
|
|
{
|
|
if (ignoreConditions)
|
|
{
|
|
return GetProperties<ConditionallyEditable>().Union(GetProperties<InGameEditable>()).ToList();
|
|
}
|
|
else
|
|
{
|
|
return GetProperties<ConditionallyEditable>()
|
|
.Where(ce => ce.Second.GetAttribute<ConditionallyEditable>().IsEditable(this))
|
|
.Union(GetProperties<InGameEditable>()).ToList();
|
|
}
|
|
}
|
|
|
|
private void ReadPropertyChange(IReadMessage msg, bool inGameEditableOnly, Client sender = null)
|
|
{
|
|
//ignoreConditions: true = include all ConditionallyEditable properties at this point,
|
|
//to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients
|
|
var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
|
|
if (allProperties.Count == 0) { return; }
|
|
|
|
int propertyIndex = 0;
|
|
if (allProperties.Count > 1)
|
|
{
|
|
propertyIndex = msg.ReadByte();
|
|
}
|
|
|
|
bool allowEditing = true;
|
|
object parentObject = allProperties[propertyIndex].First;
|
|
SerializableProperty property = allProperties[propertyIndex].Second;
|
|
if (inGameEditableOnly && parentObject is ItemComponent ic)
|
|
{
|
|
if (!ic.AllowInGameEditing) { allowEditing = false; }
|
|
}
|
|
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
if (!CanClientAccess(sender) || !(property.GetAttribute<ConditionallyEditable>()?.IsEditable(this) ?? true))
|
|
{
|
|
allowEditing = false;
|
|
}
|
|
}
|
|
|
|
var result = GameMain.LuaCs.Hook.Call<bool?>("item.readPropertyChange", this, property, parentObject, allowEditing);
|
|
if (result != null && result.Value)
|
|
return;
|
|
|
|
Type type = property.PropertyType;
|
|
string logValue = "";
|
|
if (type == typeof(string))
|
|
{
|
|
string val = msg.ReadString();
|
|
logValue = val;
|
|
if (allowEditing)
|
|
{
|
|
property.TrySetValue(parentObject, val);
|
|
}
|
|
}
|
|
else if (type == typeof(Identifier))
|
|
{
|
|
Identifier val = msg.ReadIdentifier();
|
|
logValue = val.Value;
|
|
if (allowEditing) { property.TrySetValue(parentObject, val); }
|
|
}
|
|
else if (type == typeof(float))
|
|
{
|
|
float val = msg.ReadSingle();
|
|
logValue = val.ToString("G", CultureInfo.InvariantCulture);
|
|
if (allowEditing) { property.TrySetValue(parentObject, val); }
|
|
}
|
|
else if (type == typeof(int))
|
|
{
|
|
int val = msg.ReadInt32();
|
|
logValue = val.ToString();
|
|
if (allowEditing) { property.TrySetValue(parentObject, val); }
|
|
}
|
|
else if (type == typeof(bool))
|
|
{
|
|
bool val = msg.ReadBoolean();
|
|
logValue = val.ToString();
|
|
if (allowEditing) { property.TrySetValue(parentObject, val); }
|
|
}
|
|
else if (type == typeof(Color))
|
|
{
|
|
Color val = new Color(msg.ReadByte(), msg.ReadByte(), msg.ReadByte(), msg.ReadByte());
|
|
logValue = XMLExtensions.ColorToString(val);
|
|
if (allowEditing) { property.TrySetValue(parentObject, val); }
|
|
}
|
|
else if (type == typeof(Vector2))
|
|
{
|
|
Vector2 val = new Vector2(msg.ReadSingle(), msg.ReadSingle());
|
|
logValue = XMLExtensions.Vector2ToString(val);
|
|
if (allowEditing) { property.TrySetValue(parentObject, val); }
|
|
}
|
|
else if (type == typeof(Vector3))
|
|
{
|
|
Vector3 val = new Vector3(msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle());
|
|
logValue = XMLExtensions.Vector3ToString(val);
|
|
if (allowEditing) { property.TrySetValue(parentObject, val); }
|
|
}
|
|
else if (type == typeof(Vector4))
|
|
{
|
|
Vector4 val = new Vector4(msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle(), msg.ReadSingle());
|
|
logValue = XMLExtensions.Vector4ToString(val);
|
|
if (allowEditing) { property.TrySetValue(parentObject, val); }
|
|
}
|
|
else if (type == typeof(Point))
|
|
{
|
|
Point val = new Point(msg.ReadInt32(), msg.ReadInt32());
|
|
logValue = XMLExtensions.PointToString(val);
|
|
if (allowEditing) { property.TrySetValue(parentObject, val); }
|
|
}
|
|
else if (type == typeof(Rectangle))
|
|
{
|
|
Rectangle val = new Rectangle(msg.ReadInt32(), msg.ReadInt32(), msg.ReadInt32(), msg.ReadInt32());
|
|
logValue = XMLExtensions.RectToString(val);
|
|
if (allowEditing) { property.TrySetValue(parentObject, val); }
|
|
}
|
|
else if (type == typeof(string[]))
|
|
{
|
|
int arrayLength = msg.ReadInt32();
|
|
string[] val = new string[arrayLength];
|
|
for (int i = 0; i < arrayLength; i++)
|
|
{
|
|
val[i] = msg.ReadString();
|
|
}
|
|
if (allowEditing)
|
|
{
|
|
property.TrySetValue(parentObject, val);
|
|
}
|
|
}
|
|
else if (typeof(Enum).IsAssignableFrom(type))
|
|
{
|
|
int intVal = msg.ReadInt32();
|
|
try
|
|
{
|
|
if (allowEditing)
|
|
{
|
|
property.TrySetValue(parentObject, Enum.ToObject(type, intVal));
|
|
logValue = property.GetValue(parentObject).ToString();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
#if DEBUG
|
|
DebugConsole.ThrowError("Failed to convert the int value \"" + intVal + "\" to " + type, e);
|
|
#endif
|
|
GameAnalyticsManager.AddErrorEventOnce(
|
|
"Item.ReadPropertyChange:" + Name + ":" + type,
|
|
GameAnalyticsManager.ErrorSeverity.Warning,
|
|
"Failed to convert the int value \"" + intVal + "\" to " + type + " (item " + Name + ")");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
#if SERVER
|
|
if (allowEditing)
|
|
{
|
|
//the property change isn't logged until the value stays unchanged for 1 second to prevent log spam when a player adjusts a value
|
|
if (logPropertyChangeCoroutine != null)
|
|
{
|
|
CoroutineManager.StopCoroutines(logPropertyChangeCoroutine);
|
|
}
|
|
logPropertyChangeCoroutine = CoroutineManager.Invoke(() =>
|
|
{
|
|
if(sender.Character != null)
|
|
GameServer.Log($"{sender.Character.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction);
|
|
}, delay: 1.0f);
|
|
}
|
|
#endif
|
|
|
|
if (GameMain.NetworkMember is { IsServer: true })
|
|
{
|
|
GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property));
|
|
}
|
|
}
|
|
|
|
partial void UpdateNetPosition(float deltaTime);
|
|
|
|
public static Item Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
|
|
{
|
|
return Load(element, submarine, createNetworkEvent: false, idRemap: idRemap);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Instantiate a new item and load its data from the XML element.
|
|
/// </summary>
|
|
/// <param name="element">The element containing the data of the item</param>
|
|
/// <param name="submarine">The submarine to spawn the item in (can be null)</param>
|
|
/// <param name="createNetworkEvent">Should an EntitySpawner event be created to notify clients about the item being created.</param>
|
|
/// <returns></returns>
|
|
public static Item Load(ContentXElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap)
|
|
{
|
|
string name = element.GetAttribute("name").Value;
|
|
Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
|
|
|
|
if (string.IsNullOrWhiteSpace(name) && identifier.IsEmpty)
|
|
{
|
|
string errorMessage = "Failed to load an item (both name and identifier were null):\n"+element.ToString();
|
|
DebugConsole.ThrowError(errorMessage);
|
|
GameAnalyticsManager.AddErrorEventOnce("Item.Load:NameAndIdentifierNull", GameAnalyticsManager.ErrorSeverity.Error, errorMessage);
|
|
return null;
|
|
}
|
|
|
|
Identifier pendingSwap = element.GetAttributeIdentifier("pendingswap", Identifier.Empty);
|
|
ItemPrefab appliedSwap = null;
|
|
ItemPrefab oldPrefab = null;
|
|
if (!pendingSwap.IsEmpty && Level.Loaded?.Type != LevelData.LevelType.Outpost)
|
|
{
|
|
oldPrefab = ItemPrefab.Find(name, identifier);
|
|
appliedSwap = ItemPrefab.Find(string.Empty, pendingSwap);
|
|
identifier = pendingSwap;
|
|
pendingSwap = Identifier.Empty;
|
|
}
|
|
|
|
ItemPrefab prefab = ItemPrefab.Find(name, identifier);
|
|
if (prefab == null) { return null; }
|
|
|
|
Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty);
|
|
Vector2 centerPos = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2);
|
|
if (appliedSwap != null)
|
|
{
|
|
rect.Width = (int)(prefab.Sprite.size.X * prefab.Scale);
|
|
rect.Height = (int)(prefab.Sprite.size.Y * prefab.Scale);
|
|
}
|
|
else if (rect.Width == 0 && rect.Height == 0)
|
|
{
|
|
rect.Width = (int)(prefab.Size.X * prefab.Scale);
|
|
rect.Height = (int)(prefab.Size.Y * prefab.Scale);
|
|
}
|
|
|
|
Item item = new Item(rect, prefab, submarine, callOnItemLoaded: false, id: idRemap.GetOffsetId(element))
|
|
{
|
|
Submarine = submarine,
|
|
linkedToID = new List<ushort>(),
|
|
PendingItemSwap = pendingSwap.IsEmpty ? null : MapEntityPrefab.Find(pendingSwap.Value) as ItemPrefab
|
|
};
|
|
|
|
#if SERVER
|
|
if (createNetworkEvent)
|
|
{
|
|
Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
|
|
}
|
|
#endif
|
|
|
|
foreach (XAttribute attribute in (appliedSwap?.ConfigElement ?? element).Attributes())
|
|
{
|
|
if (!item.SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; }
|
|
bool shouldBeLoaded = false;
|
|
foreach (var propertyAttribute in property.Attributes.OfType<Serialize>())
|
|
{
|
|
if (propertyAttribute.IsSaveable == IsPropertySaveable.Yes)
|
|
{
|
|
shouldBeLoaded = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (shouldBeLoaded)
|
|
{
|
|
object prevValue = property.GetValue(item);
|
|
property.TrySetValue(item, attribute.Value);
|
|
//create network events for properties that differ from the prefab values
|
|
//(e.g. if a character has an item with modified colors in their inventory)
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && property.Attributes.OfType<Editable>().Any() &&
|
|
(submarine == null || !submarine.Loading))
|
|
{
|
|
if (property.Name == "Tags" ||
|
|
property.Name == "Condition" ||
|
|
property.Name == "Description")
|
|
{
|
|
//these can be ignored, they're always written in the spawn data
|
|
}
|
|
else
|
|
{
|
|
if (!(property.GetValue(item)?.Equals(prevValue) ?? true))
|
|
{
|
|
GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
item.ParseLinks(element, idRemap);
|
|
|
|
bool thisIsOverride = element.GetAttributeBool("isoverride", false);
|
|
|
|
//if we're overriding a non-overridden item in a sub/assembly xml or vice versa,
|
|
//use the values from the prefab instead of loading them from the sub/assembly xml
|
|
bool usePrefabValues = thisIsOverride != ItemPrefab.Prefabs.IsOverride(prefab) || appliedSwap != null;
|
|
List<ItemComponent> unloadedComponents = new List<ItemComponent>(item.components);
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "upgrade":
|
|
{
|
|
var upgradeIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
|
|
UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier);
|
|
int level = subElement.GetAttributeInt("level", 1);
|
|
if (upgradePrefab != null)
|
|
{
|
|
item.AddUpgrade(new Upgrade(item, upgradePrefab, level, appliedSwap != null ? null : subElement));
|
|
}
|
|
else
|
|
{
|
|
DebugConsole.AddWarning($"An upgrade with identifier \"{upgradeIdentifier}\" on {item.Name} was not found. " +
|
|
"It's effect will not be applied and won't be saved after the round ends.");
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString());
|
|
if (component == null) { continue; }
|
|
component.Load(subElement, usePrefabValues, idRemap);
|
|
unloadedComponents.Remove(component);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (usePrefabValues && appliedSwap == null)
|
|
{
|
|
//use prefab scale when overriding a non-overridden item or vice versa
|
|
item.Scale = prefab.ConfigElement.GetAttributeFloat(item.scale, "scale", "Scale");
|
|
}
|
|
|
|
item.Upgrades.ForEach(upgrade => upgrade.ApplyUpgrade());
|
|
|
|
var availableSwapIds = element.GetAttributeIdentifierArray("availableswaps", Array.Empty<Identifier>());
|
|
foreach (Identifier swapId in availableSwapIds)
|
|
{
|
|
ItemPrefab swapPrefab = ItemPrefab.Find(string.Empty, swapId);
|
|
if (swapPrefab != null)
|
|
{
|
|
item.AvailableSwaps.Add(swapPrefab);
|
|
}
|
|
}
|
|
|
|
float prevRotation = item.Rotation;
|
|
if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); }
|
|
if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); }
|
|
item.Rotation = prevRotation;
|
|
|
|
if (appliedSwap != null)
|
|
{
|
|
item.SpriteDepth = element.GetAttributeFloat("spritedepth", item.SpriteDepth);
|
|
item.SpriteColor = element.GetAttributeColor("spritecolor", item.SpriteColor);
|
|
item.Rotation = element.GetAttributeFloat("rotation", item.Rotation);
|
|
item.PurchasedNewSwap = element.GetAttributeBool("purchasednewswap", false);
|
|
|
|
float scaleRelativeToPrefab = element.GetAttributeFloat(item.scale, "scale", "Scale") / oldPrefab.Scale;
|
|
item.Scale *= scaleRelativeToPrefab;
|
|
|
|
if (oldPrefab.SwappableItem != null && prefab.SwappableItem != null)
|
|
{
|
|
Vector2 oldRelativeOrigin = (oldPrefab.SwappableItem.SwapOrigin - oldPrefab.Size / 2) * element.GetAttributeFloat(item.scale, "scale", "Scale");
|
|
oldRelativeOrigin.Y = -oldRelativeOrigin.Y;
|
|
oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, -item.rotationRad);
|
|
Vector2 oldOrigin = centerPos + oldRelativeOrigin;
|
|
|
|
Vector2 relativeOrigin = (prefab.SwappableItem.SwapOrigin - prefab.Size / 2) * item.Scale;
|
|
relativeOrigin.Y = -relativeOrigin.Y;
|
|
relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -item.rotationRad);
|
|
Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin;
|
|
|
|
item.rect.Location -= (origin - oldOrigin).ToPoint();
|
|
}
|
|
|
|
if (item.PurchasedNewSwap && !string.IsNullOrEmpty(appliedSwap.SwappableItem?.SpawnWithId))
|
|
{
|
|
var container = item.GetComponent<ItemContainer>();
|
|
if (container != null)
|
|
{
|
|
container.SpawnWithId = appliedSwap.SwappableItem.SpawnWithId;
|
|
}
|
|
/*string[] splitIdentifier = appliedSwap.SwappableItem.SpawnWithId.Split(',');
|
|
foreach (string id in splitIdentifier)
|
|
{
|
|
ItemPrefab itemToSpawn = ItemPrefab.Find(name: null, identifier: id.Trim());
|
|
if (itemToSpawn == null)
|
|
{
|
|
DebugConsole.ThrowError($"Failed to spawn an item inside the purchased {item.Name} (could not find an item with the identifier \"{id}\").");
|
|
}
|
|
else
|
|
{
|
|
var spawnedItem = new Item(itemToSpawn, Vector2.Zero, null);
|
|
item.OwnInventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots, createNetworkEvent: false);
|
|
Spawner?.AddToSpawnQueue(itemToSpawn, item.OwnInventory, spawnIfInventoryFull: false);
|
|
}
|
|
}*/
|
|
}
|
|
item.PurchasedNewSwap = false;
|
|
}
|
|
|
|
float condition = element.GetAttributeFloat("condition", item.MaxCondition);
|
|
item.condition = MathHelper.Clamp(condition, 0, item.MaxCondition);
|
|
item.lastSentCondition = item.condition;
|
|
|
|
item.SetActiveSprite();
|
|
|
|
if (submarine?.Info.GameVersion != null)
|
|
{
|
|
SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.Info.GameVersion);
|
|
}
|
|
|
|
foreach (ItemComponent component in item.components)
|
|
{
|
|
component.OnItemLoaded();
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
public override XElement Save(XElement parentElement)
|
|
{
|
|
XElement element = new XElement("Item");
|
|
|
|
element.Add(
|
|
new XAttribute("name", Prefab.OriginalName),
|
|
new XAttribute("identifier", Prefab.Identifier),
|
|
new XAttribute("ID", ID));
|
|
|
|
if (PendingItemSwap != null)
|
|
{
|
|
element.Add(new XAttribute("pendingswap", PendingItemSwap.Identifier));
|
|
}
|
|
|
|
if (Rotation != 0f) { element.Add(new XAttribute("rotation", Rotation)); }
|
|
|
|
if (ItemPrefab.Prefabs.IsOverride(Prefab)) { element.Add(new XAttribute("isoverride", "true")); }
|
|
if (FlippedX) { element.Add(new XAttribute("flippedx", true)); }
|
|
if (FlippedY) { element.Add(new XAttribute("flippedy", true)); }
|
|
|
|
if (AvailableSwaps.Any())
|
|
{
|
|
element.Add(new XAttribute("availableswaps", string.Join(',', AvailableSwaps.Select(s => s.Identifier))));
|
|
}
|
|
|
|
if (condition < MaxCondition)
|
|
{
|
|
element.Add(new XAttribute("condition", condition.ToString("G", CultureInfo.InvariantCulture)));
|
|
}
|
|
|
|
if (!MathUtils.NearlyEqual(healthMultiplier, 1.0f))
|
|
{
|
|
element.Add(new XAttribute("healthmultiplier", HealthMultiplier.ToString("G", CultureInfo.InvariantCulture)));
|
|
}
|
|
|
|
Item rootContainer = GetRootContainer() ?? this;
|
|
System.Diagnostics.Debug.Assert(Submarine != null || rootContainer.ParentInventory?.Owner is Character);
|
|
|
|
Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition;
|
|
|
|
int width = ResizeHorizontal ? rect.Width : defaultRect.Width;
|
|
int height = ResizeVertical ? rect.Height : defaultRect.Height;
|
|
element.Add(new XAttribute("rect",
|
|
(int)(rect.X - subPosition.X) + "," +
|
|
(int)(rect.Y - subPosition.Y) + "," +
|
|
width + "," + height));
|
|
|
|
if (linkedTo != null && linkedTo.Count > 0)
|
|
{
|
|
bool isOutpost = Submarine != null && Submarine.Info.IsOutpost;
|
|
var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved && (l.Removed == Removed) && (l.Submarine == null || l.Submarine.Info.IsOutpost == isOutpost));
|
|
element.Add(new XAttribute("linked", string.Join(",", saveableLinked.Select(l => l.ID.ToString()))));
|
|
}
|
|
|
|
SerializableProperty.SerializeProperties(this, element);
|
|
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
ic.Save(element);
|
|
}
|
|
|
|
foreach (var upgrade in Upgrades)
|
|
{
|
|
upgrade.Save(element);
|
|
}
|
|
|
|
parentElement.Add(element);
|
|
|
|
return element;
|
|
}
|
|
|
|
public virtual void Reset()
|
|
{
|
|
var holdable = GetComponent<Holdable>();
|
|
bool wasAttached = holdable?.Attached ?? false;
|
|
|
|
SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement);
|
|
Sprite.ReloadXML();
|
|
SpriteDepth = Sprite.Depth;
|
|
condition = MaxCondition;
|
|
components.ForEach(c => c.Reset());
|
|
if (wasAttached)
|
|
{
|
|
holdable.AttachToWall();
|
|
}
|
|
}
|
|
|
|
public override void OnMapLoaded()
|
|
{
|
|
FindHull();
|
|
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
ic.OnMapLoaded();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove the item so that it doesn't appear to exist in the game world (stop sounds, remove bodies etc)
|
|
/// but don't reset anything that's required for cloning the item
|
|
/// </summary>
|
|
public override void ShallowRemove()
|
|
{
|
|
base.ShallowRemove();
|
|
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
ic.ShallowRemove();
|
|
}
|
|
ItemList.Remove(this);
|
|
|
|
if (body != null)
|
|
{
|
|
body.Remove();
|
|
body = null;
|
|
}
|
|
|
|
GameMain.LuaCs.Hook.Call("item.removed", this);
|
|
}
|
|
|
|
public override void Remove()
|
|
{
|
|
if (Removed)
|
|
{
|
|
DebugConsole.ThrowError("Attempting to remove an already removed item (" + Name + ")\n" + Environment.StackTrace.CleanupStackTrace());
|
|
return;
|
|
}
|
|
DebugConsole.Log("Removing item " + Name + " (ID: " + ID + ")");
|
|
|
|
base.Remove();
|
|
|
|
foreach (Character character in Character.CharacterList)
|
|
{
|
|
if (character.SelectedConstruction == this) { character.SelectedConstruction = null; }
|
|
}
|
|
|
|
Door door = GetComponent<Door>();
|
|
Ladder ladder = GetComponent<Ladder>();
|
|
if (door != null || ladder != null)
|
|
{
|
|
foreach (WayPoint wp in WayPoint.WayPointList)
|
|
{
|
|
if (door != null && wp.ConnectedDoor == door) { wp.ConnectedGap = null; }
|
|
if (ladder != null && wp.Ladders == ladder) { wp.Ladders = null; }
|
|
}
|
|
}
|
|
|
|
connections?.Clear();
|
|
|
|
if (parentInventory != null)
|
|
{
|
|
if (parentInventory is CharacterInventory characterInventory)
|
|
{
|
|
characterInventory.RemoveItem(this, tryEquipFromSameStack: true);
|
|
}
|
|
else
|
|
{
|
|
parentInventory.RemoveItem(this);
|
|
}
|
|
parentInventory = null;
|
|
}
|
|
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
ic.Remove();
|
|
#if CLIENT
|
|
ic.GuiFrame = null;
|
|
#endif
|
|
}
|
|
ItemList.Remove(this);
|
|
|
|
if (body != null)
|
|
{
|
|
body.Remove();
|
|
body = null;
|
|
}
|
|
|
|
CurrentHull = null;
|
|
|
|
if (StaticFixtures != null)
|
|
{
|
|
foreach (Fixture fixture in StaticFixtures)
|
|
{
|
|
//if the world is null, the body has already been removed
|
|
//happens if the sub the fixture is attached to is removed before the item
|
|
if (fixture.Body?.World == null) { continue; }
|
|
fixture.Body.Remove(fixture);
|
|
}
|
|
StaticFixtures.Clear();
|
|
}
|
|
|
|
foreach (Item it in ItemList)
|
|
{
|
|
if (it.linkedTo.Contains(this))
|
|
{
|
|
it.linkedTo.Remove(this);
|
|
}
|
|
}
|
|
|
|
RemoveProjSpecific();
|
|
|
|
GameMain.LuaCs.Hook.Call("item.removed", this);
|
|
}
|
|
|
|
partial void RemoveProjSpecific();
|
|
|
|
public static void RemoveByPrefab(ItemPrefab prefab)
|
|
{
|
|
if (ItemList == null) { return; }
|
|
List<Item> list = new List<Item>(ItemList);
|
|
foreach (Item item in list)
|
|
{
|
|
if (((MapEntity)item).Prefab == prefab)
|
|
{
|
|
item.Remove();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem
|
|
{
|
|
public Character Character { get; set; }
|
|
public Character User { get; set; }
|
|
public Item Item { get; set; }
|
|
|
|
public AbilityApplyTreatment(Character user, Character target, Item item)
|
|
{
|
|
Character = target;
|
|
User = user;
|
|
Item = item;
|
|
}
|
|
}
|
|
}
|