2573 lines
96 KiB
C#
2573 lines
96 KiB
C#
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics.Joints;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.IO;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
using Barotrauma.Items.Components;
|
|
using FarseerPhysics.Dynamics;
|
|
using Barotrauma.Extensions;
|
|
using System.Text;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable
|
|
{
|
|
public static List<Character> CharacterList = new List<Character>();
|
|
|
|
partial void UpdateLimbLightSource(Limb limb);
|
|
|
|
private bool enabled = true;
|
|
public bool Enabled
|
|
{
|
|
get
|
|
{
|
|
return enabled && !Removed;
|
|
}
|
|
set
|
|
{
|
|
if (value == enabled) return;
|
|
|
|
if (Removed)
|
|
{
|
|
enabled = false;
|
|
return;
|
|
}
|
|
|
|
enabled = value;
|
|
|
|
foreach (Limb limb in AnimController.Limbs)
|
|
{
|
|
if (limb.body != null)
|
|
{
|
|
limb.body.Enabled = enabled;
|
|
}
|
|
UpdateLimbLightSource(limb);
|
|
}
|
|
AnimController.Collider.Enabled = value;
|
|
}
|
|
}
|
|
|
|
public Hull PreviousHull = null;
|
|
public Hull CurrentHull = null;
|
|
|
|
public bool IsRemotePlayer;
|
|
public readonly Dictionary<string, SerializableProperty> Properties;
|
|
public Dictionary<string, SerializableProperty> SerializableProperties
|
|
{
|
|
get { return Properties; }
|
|
}
|
|
|
|
public Key[] Keys
|
|
{
|
|
get { return keys; }
|
|
}
|
|
|
|
protected Key[] keys;
|
|
private Item[] selectedItems;
|
|
|
|
public enum TeamType
|
|
{
|
|
None,
|
|
Team1,
|
|
Team2,
|
|
FriendlyNPC
|
|
}
|
|
|
|
private TeamType teamID;
|
|
public TeamType TeamID
|
|
{
|
|
get { return teamID; }
|
|
set
|
|
{
|
|
teamID = value;
|
|
if (info != null) info.TeamID = value;
|
|
}
|
|
}
|
|
|
|
public AnimController AnimController;
|
|
|
|
private Vector2 cursorPosition;
|
|
|
|
protected bool needsAir;
|
|
protected float oxygenAvailable;
|
|
|
|
//seed used to generate this character
|
|
private readonly string seed;
|
|
protected Item focusedItem;
|
|
private Character focusedCharacter, selectedCharacter, selectedBy;
|
|
public Character LastAttacker;
|
|
public Entity LastDamageSource;
|
|
|
|
public readonly bool IsHumanoid;
|
|
|
|
//the name of the species (e.q. human)
|
|
public readonly string SpeciesName;
|
|
|
|
private float attackCoolDown;
|
|
|
|
private Order currentOrder;
|
|
public Order CurrentOrder
|
|
{
|
|
get { return currentOrder; }
|
|
}
|
|
|
|
private string currentOrderOption;
|
|
|
|
private List<StatusEffect> statusEffects = new List<StatusEffect>();
|
|
private List<float> speedMultipliers = new List<float>();
|
|
|
|
public Entity ViewTarget
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public Vector2 AimRefPosition
|
|
{
|
|
get
|
|
{
|
|
if (ViewTarget == null) return AnimController.AimSourcePos;
|
|
if (ViewTarget is Item targetItem)
|
|
{
|
|
Turret turret = targetItem.GetComponent<Turret>();
|
|
if (turret != null)
|
|
{
|
|
return new Vector2(targetItem.Rect.X + turret.TransformedBarrelPos.X, targetItem.Rect.Y - turret.TransformedBarrelPos.Y);
|
|
}
|
|
}
|
|
return ViewTarget.Position;
|
|
}
|
|
}
|
|
|
|
private CharacterInfo info;
|
|
public CharacterInfo Info
|
|
{
|
|
get
|
|
{
|
|
return info;
|
|
}
|
|
set
|
|
{
|
|
if (info != null && info != value) info.Remove();
|
|
|
|
info = value;
|
|
if (info != null) info.Character = this;
|
|
}
|
|
}
|
|
|
|
public string Name
|
|
{
|
|
get
|
|
{
|
|
return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name : SpeciesName;
|
|
}
|
|
}
|
|
|
|
private string displayName;
|
|
public string DisplayName
|
|
{
|
|
get
|
|
{
|
|
return displayName != null && displayName.Length > 0 ? displayName : Name;
|
|
}
|
|
}
|
|
|
|
//Only used by server logs to determine "true identity" of the player for cases when they're disguised
|
|
public string LogName
|
|
{
|
|
get
|
|
{
|
|
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowDisguises) return Name;
|
|
return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name + (info.DisplayName != info.Name ? " (as " + info.DisplayName + ")" : "") : SpeciesName;
|
|
}
|
|
}
|
|
|
|
private float hideFaceTimer;
|
|
public bool HideFace
|
|
{
|
|
get
|
|
{
|
|
return hideFaceTimer > 0.0f;
|
|
}
|
|
set
|
|
{
|
|
hideFaceTimer = MathHelper.Clamp(hideFaceTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f);
|
|
}
|
|
}
|
|
|
|
public string ConfigPath
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public float Mass
|
|
{
|
|
get { return AnimController.Mass; }
|
|
}
|
|
|
|
public CharacterInventory Inventory { get; private set; }
|
|
|
|
private Color speechBubbleColor;
|
|
private float speechBubbleTimer;
|
|
|
|
public bool ResetInteract;
|
|
|
|
//text displayed when the character is highlighted if custom interact is set
|
|
public string customInteractHUDText;
|
|
private Action<Character, Character> onCustomInteract;
|
|
|
|
private float lockHandsTimer;
|
|
public bool LockHands
|
|
{
|
|
get
|
|
{
|
|
return lockHandsTimer > 0.0f;
|
|
}
|
|
set
|
|
{
|
|
lockHandsTimer = MathHelper.Clamp(lockHandsTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f);
|
|
}
|
|
}
|
|
|
|
public bool AllowInput
|
|
{
|
|
get { return !IsUnconscious && Stun <= 0.0f && !IsDead; }
|
|
}
|
|
|
|
public bool CanInteract
|
|
{
|
|
get { return AllowInput && IsHumanoid && !LockHands && !Removed; }
|
|
}
|
|
|
|
public Vector2 CursorPosition
|
|
{
|
|
get { return cursorPosition; }
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value)) return;
|
|
cursorPosition = value;
|
|
}
|
|
}
|
|
|
|
public Vector2 SmoothedCursorPosition
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public Vector2 CursorWorldPosition
|
|
{
|
|
get { return Submarine == null ? cursorPosition : cursorPosition + Submarine.Position; }
|
|
}
|
|
|
|
public Character FocusedCharacter
|
|
{
|
|
get { return focusedCharacter; }
|
|
}
|
|
|
|
public Character SelectedCharacter
|
|
{
|
|
get { return selectedCharacter; }
|
|
set
|
|
{
|
|
if (value == selectedCharacter) return;
|
|
if (selectedCharacter != null)
|
|
selectedCharacter.selectedBy = null;
|
|
selectedCharacter = value;
|
|
if (selectedCharacter != null)
|
|
selectedCharacter.selectedBy = this;
|
|
}
|
|
}
|
|
|
|
public Character SelectedBy
|
|
{
|
|
get { return selectedBy; }
|
|
set
|
|
{
|
|
if (selectedBy != null)
|
|
selectedBy.selectedCharacter = null;
|
|
selectedBy = value;
|
|
if (selectedBy != null)
|
|
selectedBy.selectedCharacter = this;
|
|
}
|
|
}
|
|
|
|
private float lowPassMultiplier;
|
|
public float LowPassMultiplier
|
|
{
|
|
get { return lowPassMultiplier; }
|
|
set { lowPassMultiplier = MathHelper.Clamp(value, 0.0f, 1.0f); }
|
|
}
|
|
|
|
private float obstructVisionAmount;
|
|
public bool ObstructVision
|
|
{
|
|
get
|
|
{
|
|
return obstructVisionAmount > 0.5f;
|
|
}
|
|
set
|
|
{
|
|
obstructVisionAmount = 1.0f;
|
|
}
|
|
}
|
|
|
|
private float Noise { get; set; }
|
|
|
|
private float pressureProtection;
|
|
public float PressureProtection
|
|
{
|
|
get { return pressureProtection; }
|
|
set
|
|
{
|
|
pressureProtection = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
private float ragdollingLockTimer;
|
|
public bool IsRagdolled;
|
|
public bool IsForceRagdolled;
|
|
public bool dontFollowCursor;
|
|
|
|
public bool IsUnconscious
|
|
{
|
|
get { return CharacterHealth.IsUnconscious; }
|
|
}
|
|
|
|
public bool NeedsAir
|
|
{
|
|
get { return needsAir; }
|
|
set { needsAir = value; }
|
|
}
|
|
|
|
public float Oxygen
|
|
{
|
|
get { return CharacterHealth.OxygenAmount; }
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value)) return;
|
|
CharacterHealth.OxygenAmount = MathHelper.Clamp(value, -100.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
public float OxygenAvailable
|
|
{
|
|
get { return oxygenAvailable; }
|
|
set { oxygenAvailable = MathHelper.Clamp(value, 0.0f, 100.0f); }
|
|
}
|
|
|
|
public float Stun
|
|
{
|
|
get { return IsRagdolled ? 1.0f : CharacterHealth.StunTimer; }
|
|
set
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
|
|
|
|
SetStun(value, true);
|
|
}
|
|
}
|
|
|
|
public CharacterHealth CharacterHealth { get; private set; }
|
|
|
|
public float Vitality
|
|
{
|
|
get { return CharacterHealth.Vitality; }
|
|
}
|
|
|
|
public float Health
|
|
{
|
|
get { return CharacterHealth.Vitality; }
|
|
}
|
|
|
|
public float MaxVitality
|
|
{
|
|
get { return CharacterHealth.MaxVitality; }
|
|
}
|
|
|
|
public float Bloodloss
|
|
{
|
|
get { return CharacterHealth.BloodlossAmount; }
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value)) return;
|
|
CharacterHealth.BloodlossAmount = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
public float Bleeding
|
|
{
|
|
get { return CharacterHealth.GetAfflictionStrength("bleeding", true); }
|
|
}
|
|
|
|
public float HuskInfectionState
|
|
{
|
|
get
|
|
{
|
|
var huskAffliction = CharacterHealth.GetAffliction("huskinfection", false) as AfflictionHusk;
|
|
return huskAffliction == null ? 0.0f : huskAffliction.Strength;
|
|
}
|
|
set
|
|
{
|
|
var huskAffliction = CharacterHealth.GetAffliction("huskinfection", false) as AfflictionHusk;
|
|
if (huskAffliction == null)
|
|
{
|
|
CharacterHealth.ApplyAffliction(null, AfflictionPrefab.Husk.Instantiate(value));
|
|
}
|
|
else
|
|
{
|
|
huskAffliction.Strength = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool canSpeak;
|
|
|
|
private bool speechImpedimentSet;
|
|
|
|
//value between 0-100 (50 = speech range is reduced by 50%)
|
|
private float speechImpediment;
|
|
public float SpeechImpediment
|
|
{
|
|
get
|
|
{
|
|
if (!canSpeak || IsUnconscious || Stun > 0.0f || IsDead) return 100.0f;
|
|
return speechImpediment;
|
|
}
|
|
set
|
|
{
|
|
if (value < speechImpediment) return;
|
|
speechImpedimentSet = true;
|
|
speechImpediment = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
public float PressureTimer
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public float DisableImpactDamageTimer
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public Item[] SelectedItems
|
|
{
|
|
get { return selectedItems; }
|
|
}
|
|
|
|
public Item SelectedConstruction { get; set; }
|
|
|
|
public Item FocusedItem
|
|
{
|
|
get { return focusedItem; }
|
|
}
|
|
|
|
public Item PickingItem
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public virtual AIController AIController
|
|
{
|
|
get { return null; }
|
|
}
|
|
|
|
public bool IsDead { get; private set; }
|
|
|
|
public CauseOfDeath CauseOfDeath
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
//can other characters select (= grab) this character
|
|
public bool CanBeSelected
|
|
{
|
|
get
|
|
{
|
|
return !Removed;
|
|
}
|
|
}
|
|
|
|
private bool canBeDragged = true;
|
|
public bool CanBeDragged
|
|
{
|
|
get
|
|
{
|
|
if (!canBeDragged) { return false; }
|
|
if (Removed || !AnimController.Draggable) { return false; }
|
|
return IsDead || Stun > 0.0f || LockHands || IsUnconscious;
|
|
}
|
|
set { canBeDragged = value; }
|
|
}
|
|
|
|
//can other characters access the inventory of this character
|
|
private bool canInventoryBeAccessed = true;
|
|
public bool CanInventoryBeAccessed
|
|
{
|
|
get
|
|
{
|
|
if (!canInventoryBeAccessed || Removed || Inventory == null) { return false; }
|
|
if (!Inventory.AccessibleWhenAlive)
|
|
{
|
|
return IsDead;
|
|
}
|
|
else
|
|
{
|
|
return (IsDead || Stun > 0.0f || LockHands || IsUnconscious);
|
|
}
|
|
}
|
|
set { canInventoryBeAccessed = value; }
|
|
}
|
|
|
|
public override Vector2 SimPosition
|
|
{
|
|
get
|
|
{
|
|
if (AnimController?.Collider == null)
|
|
{
|
|
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed+".";
|
|
if (AnimController == null)
|
|
{
|
|
errorMsg += " AnimController == null";
|
|
}
|
|
else if (AnimController.Collider == null)
|
|
{
|
|
errorMsg += " AnimController.Collider == null";
|
|
}
|
|
|
|
DebugConsole.NewMessage(errorMsg, Color.Red);
|
|
GameAnalyticsManager.AddErrorEventOnce(
|
|
"Character.SimPosition:AccessRemoved",
|
|
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
|
errorMsg + "\n" + Environment.StackTrace);
|
|
|
|
return Vector2.Zero;
|
|
}
|
|
|
|
return AnimController.Collider.SimPosition;
|
|
}
|
|
}
|
|
|
|
public override Vector2 Position
|
|
{
|
|
get { return ConvertUnits.ToDisplayUnits(SimPosition); }
|
|
}
|
|
|
|
public override Vector2 DrawPosition
|
|
{
|
|
get
|
|
{
|
|
if (AnimController.MainLimb == null) { return Vector2.Zero; }
|
|
return AnimController.MainLimb.body.DrawPosition;
|
|
}
|
|
}
|
|
|
|
public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
|
|
public OnDeathHandler OnDeath;
|
|
|
|
public delegate void OnAttackedHandler(Character attacker, AttackResult attackResult);
|
|
public OnAttackedHandler OnAttacked;
|
|
|
|
/// <summary>
|
|
/// Create a new character
|
|
/// </summary>
|
|
/// <param name="characterInfo">The name, gender, config file, etc of the character.</param>
|
|
/// <param name="position">Position in display units.</param>
|
|
/// <param name="seed">RNG seed to use if the character config has randomizable parameters.</param>
|
|
/// <param name="isRemotePlayer">Is the character controlled by a remote player.</param>
|
|
/// <param name="hasAi">Is the character controlled by AI.</param>
|
|
/// <param name="ragdoll">Ragdoll configuration file. If null, will select the default.</param>
|
|
public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null)
|
|
{
|
|
return Create(characterInfo.File, position, seed, characterInfo, isRemotePlayer, hasAi, true, ragdoll);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new character
|
|
/// </summary>
|
|
/// <param name="file">The path to the character's config file.</param>
|
|
/// <param name="position">Position in display units.</param>
|
|
/// <param name="seed">RNG seed to use if the character config has randomizable parameters.</param>
|
|
/// <param name="characterInfo">The name, gender, etc of the character. Only used for humans, and if the parameter is not given, a random CharacterInfo is generated.</param>
|
|
/// <param name="isRemotePlayer">Is the character controlled by a remote player.</param>
|
|
/// <param name="hasAi">Is the character controlled by AI.</param>
|
|
/// <param name="createNetworkEvent">Should clients receive a network event about the creation of this character?</param>
|
|
/// <param name="ragdoll">Ragdoll configuration file. If null, will select the default.</param>
|
|
public static Character Create(string file, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null)
|
|
{
|
|
#if LINUX
|
|
if (!System.IO.File.Exists(file))
|
|
{
|
|
//if the file was not found, attempt to convert the name of the folder to upper case
|
|
var splitPath = file.Split('/');
|
|
if (splitPath.Length > 2)
|
|
{
|
|
splitPath[splitPath.Length-2] =
|
|
splitPath[splitPath.Length-2].First().ToString().ToUpper() + splitPath[splitPath.Length-2].Substring(1);
|
|
|
|
file = string.Join("/", splitPath);
|
|
}
|
|
|
|
if (!System.IO.File.Exists(file))
|
|
{
|
|
DebugConsole.ThrowError("Spawning a character failed - file \""+file+"\" not found!");
|
|
return null;
|
|
}
|
|
}
|
|
#else
|
|
if (!System.IO.File.Exists(file))
|
|
{
|
|
DebugConsole.ThrowError("Spawning a character failed - file \"" + file + "\" not found!");
|
|
return null;
|
|
}
|
|
#endif
|
|
Character newCharacter = null;
|
|
if (file != HumanConfigFile)
|
|
{
|
|
var aiCharacter = new AICharacter(file, position, seed, characterInfo, isRemotePlayer, ragdoll);
|
|
var ai = new EnemyAIController(aiCharacter, file, seed);
|
|
aiCharacter.SetAI(ai);
|
|
|
|
//aiCharacter.minVitality = 0.0f;
|
|
|
|
newCharacter = aiCharacter;
|
|
}
|
|
else if (hasAi)
|
|
{
|
|
var aiCharacter = new AICharacter(file, position, seed, characterInfo, isRemotePlayer, ragdoll);
|
|
var ai = new HumanAIController(aiCharacter);
|
|
aiCharacter.SetAI(ai);
|
|
|
|
//aiCharacter.minVitality = -100.0f;
|
|
|
|
newCharacter = aiCharacter;
|
|
}
|
|
else
|
|
{
|
|
newCharacter = new Character(file, position, seed, characterInfo, isRemotePlayer, ragdoll);
|
|
//newCharacter.minVitality = -100.0f;
|
|
}
|
|
|
|
#if SERVER
|
|
if (GameMain.Server != null && Spawner != null && createNetworkEvent)
|
|
{
|
|
Spawner.CreateNetworkEvent(newCharacter, false);
|
|
}
|
|
#endif
|
|
return newCharacter;
|
|
}
|
|
|
|
protected Character(string file, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isRemotePlayer = false, RagdollParams ragdollParams = null)
|
|
: base(null)
|
|
{
|
|
ConfigPath = file;
|
|
this.seed = seed;
|
|
MTRandom random = new MTRandom(ToolBox.StringToInt(seed));
|
|
|
|
selectedItems = new Item[2];
|
|
|
|
IsRemotePlayer = isRemotePlayer;
|
|
|
|
oxygenAvailable = 100.0f;
|
|
aiTarget = new AITarget(this);
|
|
|
|
lowPassMultiplier = 1.0f;
|
|
|
|
Properties = SerializableProperty.GetProperties(this);
|
|
|
|
Info = characterInfo;
|
|
if (file == HumanConfigFile || file == GetConfigFile("humanhusk"))
|
|
{
|
|
if (characterInfo == null)
|
|
{
|
|
Info = new CharacterInfo(HumanConfigFile);
|
|
}
|
|
}
|
|
|
|
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
|
|
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
|
|
{
|
|
keys[i] = new Key((InputType)i);
|
|
}
|
|
|
|
XDocument doc = XMLExtensions.TryLoadXml(file);
|
|
if (doc == null || doc.Root == null) return;
|
|
|
|
InitProjSpecific(doc);
|
|
SpeciesName = doc.Root.GetAttributeString("name", "Unknown");
|
|
displayName = TextManager.Get($"Character.{Path.GetFileName(Path.GetDirectoryName(file))}", true);
|
|
|
|
IsHumanoid = doc.Root.GetAttributeBool("humanoid", false);
|
|
canSpeak = doc.Root.GetAttributeBool("canspeak", false);
|
|
needsAir = doc.Root.GetAttributeBool("needsair", false);
|
|
Noise = doc.Root.GetAttributeFloat("noise", 100f);
|
|
|
|
//List<XElement> ragdollElements = new List<XElement>();
|
|
//List<float> ragdollCommonness = new List<float>();
|
|
//foreach (XElement element in doc.Root.Elements())
|
|
//{
|
|
// if (element.Name.ToString().ToLowerInvariant() != "ragdoll") continue;
|
|
// ragdollElements.Add(element);
|
|
// ragdollCommonness.Add(element.GetAttributeFloat("commonness", 1.0f));
|
|
//}
|
|
|
|
////choose a random ragdoll element
|
|
//XElement ragdollElement = ragdollElements.Count == 1 ?
|
|
// ragdollElements[0] : ToolBox.SelectWeightedRandom(ragdollElements, ragdollCommonness, random);
|
|
|
|
if (IsHumanoid)
|
|
{
|
|
AnimController = new HumanoidAnimController(this, seed, ragdollParams as HumanRagdollParams);
|
|
AnimController.TargetDir = Direction.Right;
|
|
|
|
}
|
|
else
|
|
{
|
|
AnimController = new FishAnimController(this, seed, ragdollParams as FishRagdollParams);
|
|
PressureProtection = 100.0f;
|
|
}
|
|
|
|
foreach (XElement subElement in doc.Root.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "inventory":
|
|
Inventory = new CharacterInventory(subElement, this);
|
|
break;
|
|
case "health":
|
|
CharacterHealth = new CharacterHealth(subElement, this);
|
|
break;
|
|
case "statuseffect":
|
|
statusEffects.Add(StatusEffect.Load(subElement, Name));
|
|
break;
|
|
}
|
|
}
|
|
|
|
List<XElement> healthElements = new List<XElement>();
|
|
List<float> healthCommonness = new List<float>();
|
|
foreach (XElement element in doc.Root.Elements())
|
|
{
|
|
if (element.Name.ToString().ToLowerInvariant() != "health") continue;
|
|
healthElements.Add(element);
|
|
healthCommonness.Add(element.GetAttributeFloat("commonness", 1.0f));
|
|
}
|
|
|
|
if (healthElements.Count == 0)
|
|
{
|
|
CharacterHealth = new CharacterHealth(this);
|
|
}
|
|
else
|
|
{
|
|
CharacterHealth = new CharacterHealth(
|
|
healthElements.Count == 1 ? healthElements[0] : ToolBox.SelectWeightedRandom(healthElements, healthCommonness, random),
|
|
this);
|
|
}
|
|
|
|
AnimController.SetPosition(ConvertUnits.ToSimUnits(position));
|
|
|
|
AnimController.FindHull(null);
|
|
if (AnimController.CurrentHull != null) Submarine = AnimController.CurrentHull.Submarine;
|
|
|
|
CharacterList.Add(this);
|
|
|
|
//characters start disabled in the multiplayer mode, and are enabled if/when
|
|
// - controlled by the player
|
|
// - client receives a position update from the server
|
|
// - server receives an input message from the client controlling the character
|
|
// - if an AICharacter, the server enables it when close enough to any of the players
|
|
Enabled = GameMain.NetworkMember == null;
|
|
|
|
if (Info != null)
|
|
{
|
|
LoadHeadAttachments();
|
|
}
|
|
}
|
|
partial void InitProjSpecific(XDocument doc);
|
|
|
|
public void ReloadHead(int? headId = null, int hairIndex = -1, int beardIndex = -1, int moustacheIndex = -1, int faceAttachmentIndex = -1)
|
|
{
|
|
if (Info == null) { return; }
|
|
var head = AnimController.GetLimb(LimbType.Head);
|
|
if (head == null) { return; }
|
|
Info.RecreateHead(headId ?? Info.HeadSpriteId, Info.Race, Info.Gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
|
|
#if CLIENT
|
|
head.RecreateSprite();
|
|
#endif
|
|
LoadHeadAttachments();
|
|
}
|
|
|
|
public void LoadHeadAttachments()
|
|
{
|
|
if (AnimController == null) { return; }
|
|
var head = AnimController.GetLimb(LimbType.Head);
|
|
if (head == null) { return; }
|
|
// Note that if there are any other wearables on the head, they are removed here.
|
|
head.OtherWearables.ForEach(w => w.Sprite.Remove());
|
|
head.OtherWearables.Clear();
|
|
|
|
//if the element has not been set at this point, the character has no hair and the index should be zero (= no hair)
|
|
if (info.FaceAttachment == null) { info.FaceAttachmentIndex = 0; }
|
|
Info.FaceAttachment?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.FaceAttachment)));
|
|
if (info.BeardElement == null) { info.BeardIndex = 0; }
|
|
Info.BeardElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Beard)));
|
|
if (info.MoustacheElement == null) { info.MoustacheIndex = 0; }
|
|
Info.MoustacheElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Moustache)));
|
|
if (info.HairElement == null) { info.HairIndex = 0; }
|
|
Info.HairElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair)));
|
|
|
|
#if CLIENT
|
|
head.LoadHuskSprite();
|
|
#endif
|
|
}
|
|
|
|
private static string humanConfigFile;
|
|
public static string HumanConfigFile
|
|
{
|
|
get
|
|
{
|
|
if (string.IsNullOrEmpty(humanConfigFile))
|
|
{
|
|
humanConfigFile = GetConfigFile("Human");
|
|
}
|
|
return humanConfigFile;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<string> characterConfigFiles;
|
|
private static IEnumerable<string> CharacterConfigFiles
|
|
{
|
|
get
|
|
{
|
|
if (characterConfigFiles == null)
|
|
{
|
|
characterConfigFiles = GameMain.Instance.GetFilesOfType(ContentType.Character);
|
|
}
|
|
return characterConfigFiles;
|
|
}
|
|
}
|
|
|
|
public static string GetConfigFile(string speciesName)
|
|
{
|
|
string configFile = CharacterConfigFiles.FirstOrDefault(c => Path.GetFileName(c).ToLowerInvariant() == $"{speciesName.ToLowerInvariant()}.xml");
|
|
if (configFile == null)
|
|
{
|
|
DebugConsole.ThrowError($"Couldn't find a config file for {speciesName} from the selected content packages!");
|
|
DebugConsole.ThrowError($"(The config file must end with \"{speciesName}.xml\")");
|
|
return string.Empty;
|
|
}
|
|
return configFile;
|
|
}
|
|
|
|
public bool IsKeyHit(InputType inputType)
|
|
{
|
|
#if SERVER
|
|
if (GameMain.Server != null && IsRemotePlayer)
|
|
{
|
|
switch (inputType)
|
|
{
|
|
case InputType.Left:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Left)) && (prevDequeuedInput.HasFlag(InputNetFlags.Left));
|
|
case InputType.Right:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Right)) && (prevDequeuedInput.HasFlag(InputNetFlags.Right));
|
|
case InputType.Up:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Up)) && (prevDequeuedInput.HasFlag(InputNetFlags.Up));
|
|
case InputType.Down:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Down)) && (prevDequeuedInput.HasFlag(InputNetFlags.Down));
|
|
case InputType.Run:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Run)) && (prevDequeuedInput.HasFlag(InputNetFlags.Run));
|
|
case InputType.Crouch:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Crouch)) && (prevDequeuedInput.HasFlag(InputNetFlags.Crouch));
|
|
case InputType.Select:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Select); //TODO: clean up the way this input is registered
|
|
case InputType.Health:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Health);
|
|
case InputType.Grab:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Grab);
|
|
case InputType.Use:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Use)) && (prevDequeuedInput.HasFlag(InputNetFlags.Use));
|
|
case InputType.Ragdoll:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Ragdoll)) && (prevDequeuedInput.HasFlag(InputNetFlags.Ragdoll));
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return keys[(int)inputType].Hit;
|
|
}
|
|
|
|
public bool IsKeyDown(InputType inputType)
|
|
{
|
|
#if SERVER
|
|
if (GameMain.Server != null && IsRemotePlayer)
|
|
{
|
|
switch (inputType)
|
|
{
|
|
case InputType.Left:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Left);
|
|
case InputType.Right:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Right);
|
|
case InputType.Up:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Up);
|
|
case InputType.Down:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Down);
|
|
case InputType.Run:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Run);
|
|
case InputType.Crouch:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Crouch);
|
|
case InputType.Select:
|
|
return false; //TODO: clean up the way this input is registered
|
|
case InputType.Aim:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Aim);
|
|
case InputType.Use:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Use);
|
|
case InputType.Attack:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Attack);
|
|
case InputType.Ragdoll:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Ragdoll);
|
|
}
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
return keys[(int)inputType].Held;
|
|
}
|
|
|
|
public void SetInput(InputType inputType, bool hit, bool held)
|
|
{
|
|
keys[(int)inputType].Hit = hit;
|
|
keys[(int)inputType].Held = held;
|
|
keys[(int)inputType].SetState(hit, held);
|
|
}
|
|
|
|
public void ClearInput(InputType inputType)
|
|
{
|
|
keys[(int)inputType].Hit = false;
|
|
keys[(int)inputType].Held = false;
|
|
}
|
|
|
|
public void ClearInputs()
|
|
{
|
|
if (keys == null) return;
|
|
foreach (Key key in keys)
|
|
{
|
|
key.Hit = false;
|
|
key.Held = false;
|
|
}
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return (info != null && !string.IsNullOrWhiteSpace(info.Name)) ? info.Name : SpeciesName;
|
|
}
|
|
|
|
public void GiveJobItems(WayPoint spawnPoint = null)
|
|
{
|
|
if (info == null || info.Job == null) return;
|
|
|
|
info.Job.GiveJobItems(this, spawnPoint);
|
|
}
|
|
|
|
public float GetSkillLevel(string skillIdentifier)
|
|
{
|
|
return (Info == null || Info.Job == null) ? 0.0f : Info.Job.GetSkillLevel(skillIdentifier);
|
|
}
|
|
|
|
// TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner
|
|
public Vector2? OverrideMovement { get; set; }
|
|
public bool ForceRun { get; set; }
|
|
|
|
public bool IsClimbing => AnimController.Anim == AnimController.Animation.Climbing;
|
|
|
|
public Vector2 GetTargetMovement()
|
|
{
|
|
Vector2 targetMovement = Vector2.Zero;
|
|
if (OverrideMovement.HasValue)
|
|
{
|
|
targetMovement = OverrideMovement.Value;
|
|
}
|
|
else
|
|
{
|
|
if (IsKeyDown(InputType.Left)) targetMovement.X -= 1.0f;
|
|
if (IsKeyDown(InputType.Right)) targetMovement.X += 1.0f;
|
|
if (IsKeyDown(InputType.Up)) targetMovement.Y += 1.0f;
|
|
if (IsKeyDown(InputType.Down)) targetMovement.Y -= 1.0f;
|
|
}
|
|
|
|
//the vertical component is only used for falling through platforms and climbing ladders when not in water,
|
|
//so the movement can't be normalized or the Character would walk slower when pressing down/up
|
|
if (AnimController.InWater)
|
|
{
|
|
float length = targetMovement.Length();
|
|
if (length > 0.0f) targetMovement = targetMovement / length;
|
|
}
|
|
|
|
bool run = false;
|
|
if ((IsKeyDown(InputType.Run) && AnimController.ForceSelectAnimationType == AnimationType.NotDefined) || ForceRun)
|
|
{
|
|
//can't run if
|
|
// - dragging someone
|
|
// - crouching
|
|
// - moving backwards
|
|
run = (SelectedCharacter == null || !SelectedCharacter.CanBeDragged) &&
|
|
(!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) &&
|
|
!AnimController.IsMovingBackwards;
|
|
}
|
|
|
|
float currentSpeed = AnimController.GetCurrentSpeed(run);
|
|
targetMovement *= currentSpeed;
|
|
float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed);
|
|
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
|
|
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
|
|
|
|
//apply speed multiplier if
|
|
// a. it's boosting the movement speed and the character is trying to move fast (= running)
|
|
// b. it's a debuff that decreases movement speed
|
|
float speedMultiplier = SpeedMultiplier;
|
|
if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier;
|
|
|
|
ResetSpeedMultiplier(); // Reset, items will set the value before the next update
|
|
|
|
return targetMovement;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can be used to modify the character's speed via StatusEffects
|
|
/// </summary>
|
|
public float SpeedMultiplier
|
|
{
|
|
get
|
|
{
|
|
if (speedMultipliers.Count == 0) return 1f;
|
|
|
|
float greatestPositive = 1f;
|
|
float greatestNegative = 1f;
|
|
|
|
for (int i = 0; i < speedMultipliers.Count; i++)
|
|
{
|
|
float val = speedMultipliers[i];
|
|
if (val < 1f)
|
|
{
|
|
if (val < greatestNegative)
|
|
{
|
|
greatestNegative = val;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (val > greatestPositive)
|
|
{
|
|
greatestPositive = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
return greatestPositive - (1f - greatestNegative);
|
|
}
|
|
set
|
|
{
|
|
if (value == 1f) return;
|
|
speedMultipliers.Add(value);
|
|
}
|
|
}
|
|
|
|
public void ResetSpeedMultiplier()
|
|
{
|
|
speedMultipliers.Clear();
|
|
}
|
|
|
|
public float ApplyTemporarySpeedLimits(float speed)
|
|
{
|
|
var leftFoot = AnimController.GetLimb(LimbType.LeftFoot);
|
|
if (leftFoot != null)
|
|
{
|
|
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", leftFoot, true);
|
|
speed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f));
|
|
}
|
|
|
|
var rightFoot = AnimController.GetLimb(LimbType.RightFoot);
|
|
if (rightFoot != null)
|
|
{
|
|
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", rightFoot, true);
|
|
speed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f));
|
|
}
|
|
|
|
return speed;
|
|
}
|
|
|
|
public void Control(float deltaTime, Camera cam)
|
|
{
|
|
ViewTarget = null;
|
|
if (!AllowInput) return;
|
|
|
|
if (Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer))
|
|
{
|
|
SmoothedCursorPosition = cursorPosition;
|
|
}
|
|
else
|
|
{
|
|
//apply some smoothing to the cursor positions of remote players when playing as a client
|
|
//to make aiming look a little less choppy
|
|
Vector2 smoothedCursorDiff = cursorPosition - SmoothedCursorPosition;
|
|
smoothedCursorDiff = NetConfig.InterpolateCursorPositionError(smoothedCursorDiff);
|
|
SmoothedCursorPosition = cursorPosition - smoothedCursorDiff;
|
|
}
|
|
|
|
if (!(this is AICharacter) || Controlled == this || IsRemotePlayer)
|
|
{
|
|
Vector2 targetMovement = GetTargetMovement();
|
|
|
|
AnimController.TargetMovement = targetMovement;
|
|
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
|
|
}
|
|
|
|
if (AnimController is HumanoidAnimController)
|
|
{
|
|
((HumanoidAnimController) AnimController).Crouching = IsKeyDown(InputType.Crouch);
|
|
}
|
|
|
|
if (AnimController.onGround &&
|
|
!AnimController.InWater &&
|
|
AnimController.Anim != AnimController.Animation.UsingConstruction &&
|
|
AnimController.Anim != AnimController.Animation.CPR &&
|
|
(GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient || Controlled == this))
|
|
{
|
|
//Limb head = AnimController.GetLimb(LimbType.Head);
|
|
// Values lower than this seem to cause constantious flipping when the mouse is near the player and the player is running, because the root collider moves after flipping.
|
|
float followMargin = 30;
|
|
if (dontFollowCursor)
|
|
{
|
|
AnimController.TargetDir = Direction.Right;
|
|
}
|
|
else if (cursorPosition.X < AnimController.Collider.Position.X - followMargin)
|
|
{
|
|
AnimController.TargetDir = Direction.Left;
|
|
}
|
|
else if (cursorPosition.X > AnimController.Collider.Position.X + followMargin)
|
|
{
|
|
AnimController.TargetDir = Direction.Right;
|
|
}
|
|
}
|
|
|
|
if (GameMain.NetworkMember != null)
|
|
{
|
|
if (GameMain.NetworkMember.IsServer)
|
|
{
|
|
if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft))
|
|
{
|
|
AnimController.TargetDir = Direction.Left;
|
|
}
|
|
else
|
|
{
|
|
AnimController.TargetDir = Direction.Right;
|
|
}
|
|
}
|
|
else if (GameMain.NetworkMember.IsClient && Controlled != this)
|
|
{
|
|
if (memState.Count > 0)
|
|
{
|
|
AnimController.TargetDir = memState[0].Direction;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.F))
|
|
{
|
|
AnimController.ReleaseStuckLimbs();
|
|
}
|
|
#endif
|
|
|
|
if (attackCoolDown > 0.0f)
|
|
{
|
|
attackCoolDown -= deltaTime;
|
|
}
|
|
else if (IsKeyDown(InputType.Attack))
|
|
{
|
|
AttackContext currentContext = GetAttackContext();
|
|
var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContext));
|
|
var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition));
|
|
// Select closest
|
|
var attackLimb = sortedLimbs.FirstOrDefault();
|
|
if (attackLimb != null)
|
|
{
|
|
Vector2 attackPos = attackLimb.SimPosition + Vector2.Normalize(cursorPosition - attackLimb.Position) * ConvertUnits.ToSimUnits(attackLimb.attack.Range);
|
|
|
|
List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
|
|
ignoredBodies.Add(AnimController.Collider.FarseerBody);
|
|
|
|
var body = Submarine.PickBody(
|
|
attackLimb.SimPosition,
|
|
attackPos,
|
|
ignoredBodies,
|
|
Physics.CollisionCharacter | Physics.CollisionWall);
|
|
|
|
IDamageable attackTarget = null;
|
|
if (body != null)
|
|
{
|
|
attackPos = Submarine.LastPickedPosition;
|
|
|
|
if (body.UserData is Submarine sub)
|
|
{
|
|
body = Submarine.PickBody(
|
|
attackLimb.SimPosition - ((Submarine)body.UserData).SimPosition,
|
|
attackPos - ((Submarine)body.UserData).SimPosition,
|
|
ignoredBodies,
|
|
Physics.CollisionWall);
|
|
|
|
if (body != null)
|
|
{
|
|
attackPos = Submarine.LastPickedPosition + sub.SimPosition;
|
|
attackTarget = body.UserData as IDamageable;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (body.UserData is IDamageable)
|
|
{
|
|
attackTarget = (IDamageable)body.UserData;
|
|
}
|
|
else if (body.UserData is Limb)
|
|
{
|
|
attackTarget = ((Limb)body.UserData).character;
|
|
}
|
|
}
|
|
}
|
|
|
|
attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult);
|
|
|
|
if (!attackLimb.attack.IsRunning)
|
|
{
|
|
attackCoolDown = 1.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (SelectedConstruction == null || !SelectedConstruction.Prefab.DisableItemUsageWhenSelected)
|
|
{
|
|
for (int i = 0; i < selectedItems.Length; i++ )
|
|
{
|
|
if (selectedItems[i] == null) continue;
|
|
if (i == 1 && selectedItems[0] == selectedItems[1]) continue;
|
|
|
|
if (IsKeyDown(InputType.Use)) selectedItems[i].Use(deltaTime, this);
|
|
if (IsKeyDown(InputType.Aim) && selectedItems[i] != null) selectedItems[i].SecondaryUse(deltaTime, this);
|
|
}
|
|
}
|
|
|
|
#if CLIENT
|
|
if (SelectedConstruction != null && SelectedConstruction.ActiveHUDs.Any(ic => ic.GuiFrame != null && HUD.CloseHUD(ic.GuiFrame.Rect)))
|
|
{
|
|
//emulate a Select input to get the character to deselect the item server-side
|
|
keys[(int)InputType.Select].Hit = true;
|
|
SelectedConstruction = null;
|
|
}
|
|
#endif
|
|
if (SelectedConstruction != null)
|
|
{
|
|
if (IsKeyDown(InputType.Use)) SelectedConstruction.Use(deltaTime, this);
|
|
if (SelectedConstruction != null && IsKeyDown(InputType.Aim)) SelectedConstruction.SecondaryUse(deltaTime, this);
|
|
}
|
|
|
|
if (SelectedCharacter != null)
|
|
{
|
|
if (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > 90000.0f || !SelectedCharacter.CanBeSelected)
|
|
{
|
|
DeselectCharacter();
|
|
}
|
|
}
|
|
|
|
|
|
if (IsRemotePlayer && keys!=null)
|
|
{
|
|
foreach (Key key in keys)
|
|
{
|
|
key.ResetHit();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool CanSeeCharacter(Character character)
|
|
{
|
|
Limb selfLimb = AnimController.GetLimb(LimbType.Head);
|
|
if (selfLimb == null) selfLimb = AnimController.GetLimb(LimbType.Torso);
|
|
if (selfLimb == null) selfLimb = AnimController.Limbs[0];
|
|
|
|
Limb targetLimb = character.AnimController.GetLimb(LimbType.Head);
|
|
if (targetLimb == null) targetLimb = character.AnimController.GetLimb(LimbType.Torso);
|
|
if (targetLimb == null) targetLimb = character.AnimController.Limbs[0];
|
|
|
|
if (selfLimb != null && targetLimb != null)
|
|
{
|
|
Vector2 diff = ConvertUnits.ToSimUnits(targetLimb.WorldPosition - selfLimb.WorldPosition);
|
|
|
|
Body closestBody = null;
|
|
//both inside the same sub (or both outside)
|
|
//OR the we're inside, the other character outside
|
|
if (character.Submarine == Submarine || character.Submarine == null)
|
|
{
|
|
closestBody = Submarine.CheckVisibility(selfLimb.SimPosition, selfLimb.SimPosition + diff);
|
|
if (closestBody == null) return true;
|
|
}
|
|
//we're outside, the other character inside
|
|
else if (Submarine == null)
|
|
{
|
|
closestBody = Submarine.CheckVisibility(targetLimb.SimPosition, targetLimb.SimPosition - diff);
|
|
if (closestBody == null) return true;
|
|
}
|
|
//both inside different subs
|
|
else
|
|
{
|
|
closestBody = Submarine.CheckVisibility(selfLimb.SimPosition, selfLimb.SimPosition + diff);
|
|
if (closestBody != null && closestBody.UserData is Structure)
|
|
{
|
|
if (((Structure)closestBody.UserData).CastShadow) return false;
|
|
}
|
|
closestBody = Submarine.CheckVisibility(targetLimb.SimPosition, targetLimb.SimPosition - diff);
|
|
if (closestBody == null) return true;
|
|
|
|
}
|
|
|
|
Structure wall = closestBody.UserData as Structure;
|
|
return wall == null || !wall.CastShadow;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool CanSeeCharacter(Character character, Vector2 sourceWorldPos)
|
|
{
|
|
Vector2 diff = ConvertUnits.ToSimUnits(character.WorldPosition - sourceWorldPos);
|
|
|
|
Body closestBody = null;
|
|
if (character.Submarine == null)
|
|
{
|
|
closestBody = Submarine.CheckVisibility(sourceWorldPos, sourceWorldPos + diff);
|
|
if (closestBody == null) return true;
|
|
}
|
|
else
|
|
{
|
|
closestBody = Submarine.CheckVisibility(character.WorldPosition, character.WorldPosition - diff);
|
|
if (closestBody == null) return true;
|
|
}
|
|
|
|
Structure wall = closestBody.UserData as Structure;
|
|
return wall == null || !wall.CastShadow;
|
|
}
|
|
|
|
public bool HasEquippedItem(Item item)
|
|
{
|
|
for (int i = 0; i < Inventory.Capacity; i++)
|
|
{
|
|
if (Inventory.Items[i] == item && Inventory.SlotTypes[i] != InvSlotType.Any) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool HasEquippedItem(string itemIdentifier, bool allowBroken = true)
|
|
{
|
|
if (Inventory == null) return false;
|
|
for (int i = 0; i < Inventory.Capacity; i++)
|
|
{
|
|
if (Inventory.SlotTypes[i] == InvSlotType.Any || Inventory.Items[i] == null) continue;
|
|
if (!allowBroken && Inventory.Items[i].Condition <= 0.0f) continue;
|
|
if (Inventory.Items[i].Prefab.Identifier == itemIdentifier || Inventory.Items[i].HasTag(itemIdentifier)) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool HasSelectedItem(Item item)
|
|
{
|
|
return selectedItems.Contains(item);
|
|
}
|
|
|
|
public bool TrySelectItem(Item item)
|
|
{
|
|
bool rightHand = Inventory.IsInLimbSlot(item, InvSlotType.RightHand);
|
|
bool leftHand = Inventory.IsInLimbSlot(item, InvSlotType.LeftHand);
|
|
|
|
bool selected = false;
|
|
if (rightHand && SelectedItems[0] == null)
|
|
{
|
|
selectedItems[0] = item;
|
|
selected = true;
|
|
}
|
|
if (leftHand && SelectedItems[1] == null)
|
|
{
|
|
selectedItems[1] = item;
|
|
selected = true;
|
|
}
|
|
|
|
return selected;
|
|
}
|
|
|
|
public bool TrySelectItem(Item item, int index)
|
|
{
|
|
if (selectedItems[index] != null) return false;
|
|
|
|
selectedItems[index] = item;
|
|
return true;
|
|
}
|
|
|
|
public void DeselectItem(Item item)
|
|
{
|
|
for (int i = 0; i < selectedItems.Length; i++)
|
|
{
|
|
if (selectedItems[i] == item) selectedItems[i] = null;
|
|
}
|
|
}
|
|
|
|
public bool CanAccessInventory(Inventory inventory)
|
|
{
|
|
if (!CanInteract) return false;
|
|
|
|
//the inventory belongs to some other character
|
|
if (inventory.Owner is Character && inventory.Owner != this)
|
|
{
|
|
var owner = (Character)inventory.Owner;
|
|
|
|
//can only be accessed if the character is incapacitated and has been selected
|
|
return SelectedCharacter == owner && owner.CanInventoryBeAccessed;
|
|
}
|
|
|
|
if (inventory.Owner is Item)
|
|
{
|
|
var owner = (Item)inventory.Owner;
|
|
if (!CanInteractWith(owner))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public bool CanInteractWith(Character c, float maxDist = 200.0f)
|
|
{
|
|
if (c == this || Removed || !c.Enabled || !c.CanBeSelected) return false;
|
|
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) return false;
|
|
|
|
maxDist = ConvertUnits.ToSimUnits(maxDist);
|
|
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool CanInteractWith(Item item)
|
|
{
|
|
return CanInteractWith(item, out float distanceToItem, checkLinked: true);
|
|
}
|
|
|
|
public bool CanInteractWith(Item item, out float distanceToItem, bool checkLinked)
|
|
{
|
|
distanceToItem = -1.0f;
|
|
|
|
if (!CanInteract || item.HiddenInGame) return false;
|
|
|
|
if (item.ParentInventory != null)
|
|
{
|
|
return CanAccessInventory(item.ParentInventory);
|
|
}
|
|
|
|
Wire wire = item.GetComponent<Wire>();
|
|
if (wire != null)
|
|
{
|
|
//locked wires are never interactable
|
|
if (wire.Locked) return false;
|
|
|
|
//wires are interactable if the character has selected either of the items the wire is connected to
|
|
if (wire.Connections[0]?.Item != null && SelectedConstruction == wire.Connections[0].Item) return true;
|
|
if (wire.Connections[1]?.Item != null && SelectedConstruction == wire.Connections[1].Item) return true;
|
|
}
|
|
|
|
if (checkLinked && item.DisplaySideBySideWhenLinked)
|
|
{
|
|
foreach (MapEntity linked in item.linkedTo)
|
|
{
|
|
if (linked is Item linkedItem)
|
|
{
|
|
if (CanInteractWith(linkedItem, out float distToLinked, checkLinked: false))
|
|
{
|
|
distanceToItem = distToLinked;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) return false;
|
|
|
|
Pickable pickableComponent = item.GetComponent<Pickable>();
|
|
if (pickableComponent != null && (pickableComponent.Picker != null && !pickableComponent.Picker.IsDead)) return false;
|
|
|
|
Vector2 characterDirection = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(AnimController.Collider.Rotation));
|
|
|
|
Vector2 upperBodyPosition = Position + (characterDirection * 20.0f);
|
|
Vector2 lowerBodyPosition = Position - (characterDirection * 60.0f);
|
|
|
|
if (Submarine != null)
|
|
{
|
|
upperBodyPosition += Submarine.Position;
|
|
lowerBodyPosition += Submarine.Position;
|
|
}
|
|
|
|
bool insideTrigger = item.IsInsideTrigger(upperBodyPosition) || item.IsInsideTrigger(lowerBodyPosition);
|
|
if (item.Prefab.Triggers.Count > 0 && !insideTrigger) return false;
|
|
|
|
Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height);
|
|
|
|
// Get the point along the line between lowerBodyPosition and upperBodyPosition which is closest to the center of itemDisplayRect
|
|
Vector2 playerDistanceCheckPosition = Vector2.Clamp(itemDisplayRect.Center.ToVector2(), lowerBodyPosition, upperBodyPosition);
|
|
|
|
// If playerDistanceCheckPosition is inside the itemDisplayRect then we consider the character to within 0 distance of the item
|
|
if (itemDisplayRect.Contains(playerDistanceCheckPosition))
|
|
{
|
|
distanceToItem = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
// Here we get the point on the itemDisplayRect which is closest to playerDistanceCheckPosition
|
|
Vector2 rectIntersectionPoint = new Vector2(
|
|
MathHelper.Clamp(playerDistanceCheckPosition.X, itemDisplayRect.X, itemDisplayRect.Right),
|
|
MathHelper.Clamp(playerDistanceCheckPosition.Y, itemDisplayRect.Y, itemDisplayRect.Bottom));
|
|
distanceToItem = Vector2.Distance(rectIntersectionPoint, playerDistanceCheckPosition);
|
|
}
|
|
|
|
if (distanceToItem > item.InteractDistance && item.InteractDistance > 0.0f) return false;
|
|
|
|
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
|
|
{
|
|
Vector2 itemPosition = item.SimPosition;
|
|
if (Submarine == null && item.Submarine != null)
|
|
{
|
|
//character is outside, item inside
|
|
itemPosition += item.Submarine.SimPosition;
|
|
}
|
|
else if (Submarine != null && item.Submarine == null)
|
|
{
|
|
//character is inside, item outside
|
|
itemPosition -= Submarine.SimPosition;
|
|
}
|
|
else if (Submarine != item.Submarine)
|
|
{
|
|
//character and the item are inside different subs
|
|
itemPosition += item.Submarine.SimPosition;
|
|
itemPosition -= Submarine.SimPosition;
|
|
}
|
|
var body = Submarine.CheckVisibility(SimPosition, itemPosition, true);
|
|
if (body != null && body.UserData as Item != item) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set an action that's invoked when another character interacts with this one.
|
|
/// </summary>
|
|
/// <param name="onCustomInteract">Action invoked when another character interacts with this one. T1 = this character, T2 = the interacting character</param>
|
|
/// <param name="hudText">Displayed on the character when highlighted.</param>
|
|
public void SetCustomInteract(Action<Character, Character> onCustomInteract, string hudText)
|
|
{
|
|
this.onCustomInteract = onCustomInteract;
|
|
customInteractHUDText = hudText;
|
|
}
|
|
|
|
private void TransformCursorPos()
|
|
{
|
|
if (Submarine == null)
|
|
{
|
|
//character is outside but cursor position inside
|
|
if (cursorPosition.Y > Level.Loaded.Size.Y)
|
|
{
|
|
var sub = Submarine.FindContaining(cursorPosition);
|
|
if (sub != null) cursorPosition += sub.Position;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//character is inside but cursor position is outside
|
|
if (cursorPosition.Y < Level.Loaded.Size.Y)
|
|
{
|
|
cursorPosition -= Submarine.Position;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SelectCharacter(Character character)
|
|
{
|
|
if (character == null) return;
|
|
|
|
SelectedCharacter = character;
|
|
}
|
|
|
|
public void DeselectCharacter()
|
|
{
|
|
if (SelectedCharacter == null) return;
|
|
SelectedCharacter.AnimController?.ResetPullJoints();
|
|
SelectedCharacter = null;
|
|
}
|
|
|
|
public void DoInteractionUpdate(float deltaTime, Vector2 mouseSimPos)
|
|
{
|
|
bool isLocalPlayer = Controlled == this;
|
|
|
|
if (!isLocalPlayer && (this is AICharacter && !IsRemotePlayer))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ResetInteract)
|
|
{
|
|
ResetInteract = false;
|
|
return;
|
|
}
|
|
|
|
if (!CanInteract)
|
|
{
|
|
SelectedConstruction = null;
|
|
focusedItem = null;
|
|
if (!AllowInput)
|
|
{
|
|
focusedCharacter = null;
|
|
if (SelectedCharacter != null) DeselectCharacter();
|
|
return;
|
|
}
|
|
}
|
|
|
|
#if CLIENT
|
|
if (isLocalPlayer)
|
|
{
|
|
if (GUI.MouseOn == null && !CharacterInventory.IsMouseOnInventory())
|
|
{
|
|
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
|
|
{
|
|
focusedCharacter = FindCharacterAtPosition(mouseSimPos);
|
|
focusedItem = CanInteract ?
|
|
FindItemAtPosition(mouseSimPos, GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f)) : null;
|
|
findFocusedTimer = 0.05f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
focusedItem = null;
|
|
}
|
|
findFocusedTimer -= deltaTime;
|
|
}
|
|
#endif
|
|
//climb ladders automatically when pressing up/down inside their trigger area
|
|
Ladder currentLadder = SelectedConstruction?.GetComponent<Ladder>();
|
|
if ((SelectedConstruction == null || currentLadder != null) &&
|
|
!AnimController.InWater && Screen.Selected != GameMain.SubEditorScreen)
|
|
{
|
|
bool climbInput = IsKeyDown(InputType.Up) || IsKeyDown(InputType.Down);
|
|
bool isControlled = Controlled == this;
|
|
|
|
Ladder nearbyLadder = null;
|
|
if (isControlled || climbInput)
|
|
{
|
|
float minDist = float.PositiveInfinity;
|
|
foreach (Ladder ladder in Ladder.List)
|
|
{
|
|
if (ladder == currentLadder)
|
|
{
|
|
continue;
|
|
}
|
|
else if (currentLadder != null)
|
|
{
|
|
//only switch from ladder to another if the ladders are above the current ladders and pressing up, or vice versa
|
|
if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y != IsKeyDown(InputType.Up))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (CanInteractWith(ladder.Item, out float dist, checkLinked: false) && dist < minDist)
|
|
{
|
|
minDist = dist;
|
|
nearbyLadder = ladder;
|
|
if (isControlled) ladder.Item.IsHighlighted = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nearbyLadder != null && climbInput)
|
|
{
|
|
if (nearbyLadder.Select(this)) SelectedConstruction = nearbyLadder.Item;
|
|
}
|
|
}
|
|
|
|
if (SelectedCharacter != null && IsKeyHit(InputType.Grab)) //Let people use ladders and buttons and stuff when dragging chars
|
|
{
|
|
DeselectCharacter();
|
|
}
|
|
else if (focusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanInventoryBeAccessed)
|
|
{
|
|
SelectCharacter(focusedCharacter);
|
|
}
|
|
else if (focusedCharacter != null && IsKeyHit(InputType.Health) && focusedCharacter.CharacterHealth.UseHealthWindow)
|
|
{
|
|
if (focusedCharacter == SelectedCharacter)
|
|
{
|
|
DeselectCharacter();
|
|
#if CLIENT
|
|
if (Controlled == this) CharacterHealth.OpenHealthWindow = null;
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
SelectCharacter(focusedCharacter);
|
|
#if CLIENT
|
|
if (Controlled == this) CharacterHealth.OpenHealthWindow = focusedCharacter.CharacterHealth;
|
|
#endif
|
|
}
|
|
}
|
|
else if (focusedCharacter != null && IsKeyHit(InputType.Select) && FocusedCharacter.onCustomInteract != null)
|
|
{
|
|
FocusedCharacter.onCustomInteract(focusedCharacter, this);
|
|
}
|
|
else if (focusedItem != null)
|
|
{
|
|
bool canInteract = focusedItem.TryInteract(this);
|
|
#if CLIENT
|
|
if (Controlled == this)
|
|
{
|
|
focusedItem.IsHighlighted = true;
|
|
if (canInteract)
|
|
{
|
|
CharacterHealth.OpenHealthWindow = null;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
else if (IsKeyHit(InputType.Select) && SelectedConstruction != null)
|
|
{
|
|
SelectedConstruction = null;
|
|
#if CLIENT
|
|
CharacterHealth.OpenHealthWindow = null;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public static void UpdateAnimAll(float deltaTime)
|
|
{
|
|
foreach (Character c in CharacterList)
|
|
{
|
|
if (!c.Enabled || c.AnimController.Frozen) continue;
|
|
|
|
c.AnimController.UpdateAnim(deltaTime);
|
|
}
|
|
}
|
|
|
|
public static void UpdateAll(float deltaTime, Camera cam)
|
|
{
|
|
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
|
{
|
|
foreach (Character c in CharacterList)
|
|
{
|
|
if (!(c is AICharacter) && !c.IsRemotePlayer) continue;
|
|
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
//disable AI characters that are far away from all clients and the host's character and not controlled by anyone
|
|
if (c == Controlled || c.IsRemotePlayer)
|
|
{
|
|
c.Enabled = true;
|
|
}
|
|
else
|
|
{
|
|
float distSqr = float.MaxValue;
|
|
foreach (Character otherCharacter in CharacterList)
|
|
{
|
|
if (otherCharacter == c || !otherCharacter.IsRemotePlayer) { continue; }
|
|
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(otherCharacter.WorldPosition, c.WorldPosition));
|
|
}
|
|
|
|
if (distSqr > NetConfig.DisableCharacterDistSqr)
|
|
{
|
|
c.Enabled = false;
|
|
}
|
|
else if (distSqr < NetConfig.EnableCharacterDistSqr)
|
|
{
|
|
c.Enabled = true;
|
|
}
|
|
}
|
|
}
|
|
else if (Submarine.MainSub != null)
|
|
{
|
|
//disable AI characters that are far away from the sub and the controlled character
|
|
float distSqr = Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition);
|
|
if (Controlled != null)
|
|
{
|
|
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(Controlled.WorldPosition, c.WorldPosition));
|
|
}
|
|
|
|
if (distSqr > NetConfig.DisableCharacterDistSqr)
|
|
{
|
|
c.Enabled = false;
|
|
}
|
|
else if ( distSqr < NetConfig.EnableCharacterDistSqr)
|
|
{
|
|
c.Enabled = true;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < CharacterList.Count; i++)
|
|
{
|
|
CharacterList[i].Update(deltaTime, cam);
|
|
}
|
|
}
|
|
|
|
public virtual void Update(float deltaTime, Camera cam)
|
|
{
|
|
UpdateProjSpecific(deltaTime, cam);
|
|
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) return;
|
|
|
|
if (!Enabled) return;
|
|
|
|
if (Level.Loaded != null && WorldPosition.Y < Level.MaxEntityDepth ||
|
|
(Submarine != null && Submarine.WorldPosition.Y < Level.MaxEntityDepth))
|
|
{
|
|
Enabled = false;
|
|
Kill(CauseOfDeathType.Pressure, null);
|
|
return;
|
|
}
|
|
|
|
ApplyStatusEffects(ActionType.Always, deltaTime);
|
|
|
|
PreviousHull = CurrentHull;
|
|
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, true);
|
|
|
|
speechBubbleTimer = Math.Max(0.0f, speechBubbleTimer - deltaTime);
|
|
|
|
obstructVisionAmount = Math.Max(obstructVisionAmount - deltaTime, 0.0f);
|
|
|
|
if (Inventory != null)
|
|
{
|
|
foreach (Item item in Inventory.Items)
|
|
{
|
|
if (item == null || item.body == null || item.body.Enabled) continue;
|
|
|
|
item.SetTransform(SimPosition, 0.0f);
|
|
item.Submarine = Submarine;
|
|
}
|
|
}
|
|
|
|
HideFace = false;
|
|
|
|
if (IsDead) return;
|
|
|
|
if (GameMain.NetworkMember != null)
|
|
{
|
|
UpdateNetInput();
|
|
}
|
|
else
|
|
{
|
|
AnimController.Frozen = false;
|
|
}
|
|
|
|
DisableImpactDamageTimer -= deltaTime;
|
|
|
|
if (!speechImpedimentSet)
|
|
{
|
|
//if no statuseffect or anything else has set a speech impediment, allow speaking normally
|
|
speechImpediment = 0.0f;
|
|
}
|
|
speechImpedimentSet = false;
|
|
|
|
|
|
|
|
if (needsAir)
|
|
{
|
|
bool protectedFromPressure = PressureProtection > 0.0f;
|
|
//cannot be protected from pressure when below crush depth
|
|
protectedFromPressure = protectedFromPressure && WorldPosition.Y > CharacterHealth.CrushDepth;
|
|
//implode if not protected from pressure, and either outside or in a high-pressure hull
|
|
if (!protectedFromPressure &&
|
|
(AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f))
|
|
{
|
|
PressureTimer += ((AnimController.CurrentHull == null) ?
|
|
100.0f : AnimController.CurrentHull.LethalPressure) * deltaTime;
|
|
|
|
if (PressureTimer >= 100.0f)
|
|
{
|
|
if (Controlled == this) cam.Zoom = 5.0f;
|
|
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
|
{
|
|
Implode();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PressureTimer = 0.0f;
|
|
}
|
|
}
|
|
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && WorldPosition.Y < CharacterHealth.CrushDepth)
|
|
{
|
|
//implode if below crush depth, and either outside or in a high-pressure hull
|
|
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
|
|
{
|
|
Implode();
|
|
return;
|
|
}
|
|
}
|
|
|
|
ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
|
|
|
|
UpdateControlled(deltaTime, cam);
|
|
|
|
//Health effects
|
|
if (needsAir) UpdateOxygen(deltaTime);
|
|
CharacterHealth.Update(deltaTime);
|
|
|
|
if (IsUnconscious)
|
|
{
|
|
UpdateUnconscious(deltaTime);
|
|
return;
|
|
}
|
|
|
|
UpdateAIChatMessages(deltaTime);
|
|
|
|
//Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us!
|
|
bool allowRagdoll = GameMain.NetworkMember != null ? GameMain.NetworkMember.ServerSettings.AllowRagdollButton : true;
|
|
if (IsForceRagdolled)
|
|
{
|
|
IsRagdolled = IsForceRagdolled;
|
|
}
|
|
//Keep us ragdolled if we were forced or we're too speedy to unragdoll
|
|
else if (allowRagdoll && (!IsRagdolled || AnimController.Collider.LinearVelocity.LengthSquared() < 1f))
|
|
{
|
|
if (ragdollingLockTimer > 0.0f)
|
|
{
|
|
ragdollingLockTimer -= deltaTime;
|
|
}
|
|
else
|
|
{
|
|
bool wasRagdolled = IsRagdolled;
|
|
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
|
|
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.25f; }
|
|
}
|
|
}
|
|
|
|
UpdateSightRange();
|
|
UpdateSoundRange();
|
|
|
|
lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f);
|
|
|
|
//ragdoll button
|
|
if (IsRagdolled)
|
|
{
|
|
if (AnimController is HumanoidAnimController) ((HumanoidAnimController)AnimController).Crouching = false;
|
|
/*if(GameMain.Server != null)
|
|
GameMain.Server.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });*/
|
|
AnimController.ResetPullJoints();
|
|
SelectedConstruction = null;
|
|
return;
|
|
}
|
|
|
|
//AI and control stuff
|
|
|
|
Control(deltaTime, cam);
|
|
|
|
bool isNotControlled = Controlled != this;
|
|
|
|
if (isNotControlled && (!(this is AICharacter) || IsRemotePlayer))
|
|
{
|
|
Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition);
|
|
DoInteractionUpdate(deltaTime, mouseSimPos);
|
|
}
|
|
|
|
if (SelectedConstruction != null && !CanInteractWith(SelectedConstruction))
|
|
{
|
|
SelectedConstruction = null;
|
|
}
|
|
|
|
if (!IsDead) LockHands = false;
|
|
}
|
|
|
|
partial void UpdateControlled(float deltaTime, Camera cam);
|
|
|
|
partial void UpdateProjSpecific(float deltaTime, Camera cam);
|
|
|
|
private void UpdateOxygen(float deltaTime)
|
|
{
|
|
PressureProtection -= deltaTime * 100.0f;
|
|
float hullAvailableOxygen = 0.0f;
|
|
if (!AnimController.HeadInWater && AnimController.CurrentHull != null)
|
|
{
|
|
//don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull
|
|
//(i.e. if the character has some external source of oxygen)
|
|
if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage)
|
|
{
|
|
AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime;
|
|
}
|
|
hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage;
|
|
}
|
|
|
|
OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
|
|
}
|
|
partial void UpdateOxygenProjSpecific(float prevOxygen);
|
|
|
|
private void UpdateUnconscious(float deltaTime)
|
|
{
|
|
Stun = Math.Max(5.0f, Stun);
|
|
|
|
AnimController.ResetPullJoints();
|
|
SelectedConstruction = null;
|
|
}
|
|
|
|
private void UpdateSightRange()
|
|
{
|
|
if (aiTarget == null) { return; }
|
|
// TODO: the formula might need some tweaking
|
|
float range = (float)Math.Sqrt(Mass) * 1000.0f + AnimController.Collider.LinearVelocity.Length() * 500.0f;
|
|
aiTarget.SightRange = MathHelper.Clamp(range, 0, 15000.0f);
|
|
}
|
|
|
|
private void UpdateSoundRange()
|
|
{
|
|
if (aiTarget == null) { return; }
|
|
float range = Mass / 5 * AnimController.TargetMovement.Length() * Noise;
|
|
aiTarget.SoundRange = MathHelper.Clamp(range, 0f, 5000f);
|
|
}
|
|
|
|
public void SetOrder(Order order, string orderOption, Character orderGiver, bool speak = true)
|
|
{
|
|
if (orderGiver != null)
|
|
{
|
|
//set the character order only if the character is close enough to hear the message
|
|
ChatMessageType messageType = ChatMessage.CanUseRadio(orderGiver) && ChatMessage.CanUseRadio(this) ?
|
|
ChatMessageType.Radio : ChatMessageType.Default;
|
|
if (string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, orderGiver, this))) return;
|
|
}
|
|
|
|
HumanAIController humanAI = AIController as HumanAIController;
|
|
humanAI?.SetOrder(order, orderOption, orderGiver, speak);
|
|
|
|
currentOrder = order;
|
|
currentOrderOption = orderOption;
|
|
}
|
|
|
|
private List<AIChatMessage> aiChatMessageQueue = new List<AIChatMessage>();
|
|
private List<AIChatMessage> prevAiChatMessages = new List<AIChatMessage>();
|
|
|
|
public void DisableLine(string identifier)
|
|
{
|
|
var dummyMsg = new AIChatMessage("", ChatMessageType.Default, identifier)
|
|
{
|
|
SendTime = Timing.TotalTime
|
|
};
|
|
prevAiChatMessages.Add(dummyMsg);
|
|
}
|
|
|
|
public void Speak(string message, ChatMessageType? messageType = null, float delay = 0.0f, string identifier = "", float minDurationBetweenSimilar = 0.0f)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
|
|
|
|
//already sent a similar message a moment ago
|
|
if (!string.IsNullOrEmpty(identifier) && minDurationBetweenSimilar > 0.0f &&
|
|
(aiChatMessageQueue.Any(m => m.Identifier == identifier) ||
|
|
prevAiChatMessages.Any(m => m.Identifier == identifier && m.SendTime > Timing.TotalTime - minDurationBetweenSimilar)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
aiChatMessageQueue.Add(new AIChatMessage(message, messageType, identifier, delay));
|
|
}
|
|
|
|
private void UpdateAIChatMessages(float deltaTime)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
|
|
|
|
List<AIChatMessage> sentMessages = new List<AIChatMessage>();
|
|
foreach (AIChatMessage message in aiChatMessageQueue)
|
|
{
|
|
message.SendDelay -= deltaTime;
|
|
if (message.SendDelay > 0.0f) continue;
|
|
|
|
if (message.MessageType == null)
|
|
{
|
|
message.MessageType = ChatMessage.CanUseRadio(this) ? ChatMessageType.Radio : ChatMessageType.Default;
|
|
}
|
|
#if CLIENT
|
|
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
|
|
{
|
|
string modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Message, message.MessageType.Value, this, Controlled);
|
|
if (!string.IsNullOrEmpty(modifiedMessage))
|
|
{
|
|
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(info.Name, modifiedMessage, message.MessageType.Value, this);
|
|
}
|
|
}
|
|
#endif
|
|
#if SERVER
|
|
if (GameMain.Server != null && message.MessageType != ChatMessageType.Order)
|
|
{
|
|
GameMain.Server.SendChatMessage(message.Message, message.MessageType.Value, null, this);
|
|
}
|
|
#endif
|
|
ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.MessageType.Value]);
|
|
sentMessages.Add(message);
|
|
}
|
|
|
|
foreach (AIChatMessage sent in sentMessages)
|
|
{
|
|
sent.SendTime = Timing.TotalTime;
|
|
aiChatMessageQueue.Remove(sent);
|
|
prevAiChatMessages.Add(sent);
|
|
}
|
|
|
|
for (int i = prevAiChatMessages.Count - 1; i >= 0; i--)
|
|
{
|
|
if (prevAiChatMessages[i].SendTime < Timing.TotalTime - 60.0f)
|
|
{
|
|
prevAiChatMessages.RemoveRange(0, i + 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public void ShowSpeechBubble(float duration, Color color)
|
|
{
|
|
speechBubbleTimer = Math.Max(speechBubbleTimer, duration);
|
|
speechBubbleColor = color;
|
|
}
|
|
|
|
partial void AdjustKarma(Character attacker, AttackResult attackResult);
|
|
|
|
partial void DamageHUD(float amount);
|
|
|
|
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
|
|
{
|
|
CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount);
|
|
}
|
|
|
|
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
|
|
{
|
|
return ApplyAttack(attacker, worldPosition, attack, deltaTime, playSound, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to worldPosition will receive the damage.
|
|
/// </summary>
|
|
public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null)
|
|
{
|
|
if (Removed)
|
|
{
|
|
string errorMsg = "Tried to apply an attack to a removed character (" + Name + ").\n" + Environment.StackTrace;
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
|
return new AttackResult();
|
|
}
|
|
|
|
Limb limbHit = targetLimb;
|
|
|
|
float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime;
|
|
|
|
var attackResult = targetLimb == null ?
|
|
AddDamage(worldPosition, attack.Afflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker) :
|
|
DamageLimb(worldPosition, targetLimb, attack.Afflictions, attack.Stun, playSound, attackImpulse, attacker);
|
|
|
|
if (limbHit == null) return new AttackResult();
|
|
|
|
limbHit.body?.ApplyLinearImpulse(attack.TargetImpulseWorld + attack.TargetForceWorld * deltaTime);
|
|
#if SERVER
|
|
if (attacker is Character attackingCharacter && attackingCharacter.AIController == null)
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.Append(LogName + " attacked by " + attackingCharacter.LogName + ".");
|
|
if (attackResult.Afflictions != null)
|
|
{
|
|
foreach (Affliction affliction in attackResult.Afflictions)
|
|
{
|
|
if (affliction.Strength == 0.0f) continue;
|
|
sb.Append($" {affliction.Prefab.Name}: {affliction.Strength}");
|
|
}
|
|
}
|
|
GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
|
|
}
|
|
#endif
|
|
|
|
bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient;
|
|
|
|
if (isNotClient &&
|
|
IsDead && Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability)
|
|
{
|
|
foreach (LimbJoint joint in AnimController.LimbJoints)
|
|
{
|
|
if (joint.CanBeSevered && (joint.LimbA == limbHit || joint.LimbB == limbHit))
|
|
{
|
|
#if CLIENT
|
|
if (CurrentHull != null)
|
|
{
|
|
CurrentHull.AddDecal("blood", WorldPosition, Rand.Range(0.5f, 1.5f));
|
|
}
|
|
#endif
|
|
|
|
AnimController.SeverLimbJoint(joint);
|
|
|
|
if (joint.LimbA == limbHit)
|
|
{
|
|
joint.LimbB.body.LinearVelocity += limbHit.LinearVelocity * 0.5f;
|
|
}
|
|
else
|
|
{
|
|
joint.LimbA.body.LinearVelocity += limbHit.LinearVelocity * 0.5f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return attackResult;
|
|
}
|
|
|
|
public AttackResult AddDamage(Vector2 worldPosition, List<Affliction> afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null)
|
|
{
|
|
return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker);
|
|
}
|
|
|
|
public AttackResult AddDamage(Vector2 worldPosition, List<Affliction> afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null)
|
|
{
|
|
hitLimb = null;
|
|
|
|
if (Removed) return new AttackResult();
|
|
|
|
float closestDistance = 0.0f;
|
|
foreach (Limb limb in AnimController.Limbs)
|
|
{
|
|
float distance = Vector2.DistanceSquared(worldPosition, limb.WorldPosition);
|
|
if (hitLimb == null || distance < closestDistance)
|
|
{
|
|
hitLimb = limb;
|
|
closestDistance = distance;
|
|
}
|
|
}
|
|
|
|
return DamageLimb(worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker);
|
|
}
|
|
|
|
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, List<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null)
|
|
{
|
|
if (Removed) return new AttackResult();
|
|
|
|
SetStun(stun);
|
|
Vector2 dir = hitLimb.WorldPosition - worldPosition;
|
|
if (Math.Abs(attackImpulse) > 0.0f)
|
|
{
|
|
Vector2 diff = dir;
|
|
if (diff == Vector2.Zero) diff = Rand.Vector(1.0f);
|
|
hitLimb.body.ApplyLinearImpulse(Vector2.Normalize(diff) * attackImpulse, hitLimb.SimPosition + ConvertUnits.ToSimUnits(diff));
|
|
}
|
|
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
|
|
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound);
|
|
CharacterHealth.ApplyDamage(hitLimb, attackResult);
|
|
if (attacker != this)
|
|
{
|
|
OnAttacked?.Invoke(attacker, attackResult);
|
|
OnAttackedProjSpecific(attacker, attackResult);
|
|
};
|
|
AdjustKarma(attacker, attackResult);
|
|
|
|
if (attacker != null && attackResult.Damage > 0.0f)
|
|
{
|
|
LastAttacker = attacker;
|
|
}
|
|
|
|
return attackResult;
|
|
}
|
|
|
|
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult);
|
|
|
|
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) return;
|
|
|
|
if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) return;
|
|
|
|
if (Math.Sign(newStun) != Math.Sign(Stun)) AnimController.ResetPullJoints();
|
|
|
|
CharacterHealth.StunTimer = newStun;
|
|
if (newStun > 0.0f)
|
|
{
|
|
SelectedConstruction = null;
|
|
}
|
|
}
|
|
|
|
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
|
|
{
|
|
foreach (StatusEffect statusEffect in statusEffects)
|
|
{
|
|
if (statusEffect.type != actionType) { continue; }
|
|
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
|
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
|
{
|
|
var targets = new List<ISerializableEntity>();
|
|
statusEffect.GetNearbyTargets(WorldPosition, targets);
|
|
statusEffect.Apply(ActionType.OnActive, deltaTime, this, targets);
|
|
}
|
|
else
|
|
{
|
|
statusEffect.Apply(actionType, deltaTime, this, this);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Implode(bool isNetworkMessage = false)
|
|
{
|
|
if (CharacterHealth.Unkillable) { return; }
|
|
|
|
if (!isNetworkMessage)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
|
|
}
|
|
|
|
Kill(CauseOfDeathType.Pressure, null, isNetworkMessage);
|
|
CharacterHealth.PressureAffliction.Strength = CharacterHealth.PressureAffliction.Prefab.MaxStrength;
|
|
CharacterHealth.SetAllDamage(200.0f, 0.0f, 0.0f);
|
|
BreakJoints();
|
|
}
|
|
|
|
public void BreakJoints()
|
|
{
|
|
Vector2 centerOfMass = AnimController.GetCenterOfMass();
|
|
foreach (Limb limb in AnimController.Limbs)
|
|
{
|
|
limb.AddDamage(limb.SimPosition, 500.0f, 0.0f, 0.0f, false);
|
|
|
|
Vector2 diff = centerOfMass - limb.SimPosition;
|
|
|
|
if (!MathUtils.IsValid(diff))
|
|
{
|
|
string errorMsg = "Attempted to apply an invalid impulse to a limb in Character.BreakJoints (" + diff + "). Limb position: " + limb.SimPosition + ", center of mass: " + centerOfMass + ".";
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
|
return;
|
|
}
|
|
|
|
if (diff == Vector2.Zero) { continue; }
|
|
limb.body.ApplyLinearImpulse(diff * 50.0f);
|
|
}
|
|
|
|
ImplodeFX();
|
|
|
|
foreach (var joint in AnimController.LimbJoints)
|
|
{
|
|
joint.LimitEnabled = false;
|
|
}
|
|
}
|
|
|
|
partial void ImplodeFX();
|
|
|
|
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false)
|
|
{
|
|
if (IsDead || CharacterHealth.Unkillable) { return; }
|
|
|
|
HealthUpdateInterval = 0.0f;
|
|
|
|
//clients aren't allowed to kill characters unless they receive a network message
|
|
if (!isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
|
|
|
|
AnimController.Frozen = false;
|
|
|
|
if (GameSettings.SendUserStatistics)
|
|
{
|
|
string characterType = "Unknown";
|
|
|
|
if (this == Controlled)
|
|
characterType = "Player";
|
|
else if (IsRemotePlayer)
|
|
characterType = "RemotePlayer";
|
|
else if (AIController is EnemyAIController)
|
|
characterType = "Enemy";
|
|
else if (AIController is HumanAIController)
|
|
characterType = "AICrew";
|
|
|
|
string causeOfDeathStr = causeOfDeathAffliction == null ?
|
|
causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Name.Replace(" ", "");
|
|
GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + SpeciesName + ":" + causeOfDeathStr);
|
|
}
|
|
|
|
CauseOfDeath = new CauseOfDeath(
|
|
causeOfDeath, causeOfDeathAffliction?.Prefab,
|
|
causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource);
|
|
OnDeath?.Invoke(this, CauseOfDeath);
|
|
|
|
SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath);
|
|
|
|
KillProjSpecific(causeOfDeath, causeOfDeathAffliction);
|
|
|
|
IsDead = true;
|
|
|
|
if (info != null) info.CauseOfDeath = CauseOfDeath;
|
|
AnimController.movement = Vector2.Zero;
|
|
AnimController.TargetMovement = Vector2.Zero;
|
|
|
|
for (int i = 0; i < selectedItems.Length; i++ )
|
|
{
|
|
if (selectedItems[i] != null) selectedItems[i].Drop(this);
|
|
}
|
|
|
|
AnimController.ResetPullJoints();
|
|
|
|
foreach (RevoluteJoint joint in AnimController.LimbJoints)
|
|
{
|
|
joint.MotorEnabled = false;
|
|
}
|
|
|
|
if (GameMain.GameSession != null)
|
|
{
|
|
GameMain.GameSession.KillCharacter(this);
|
|
}
|
|
}
|
|
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction);
|
|
|
|
public void Revive()
|
|
{
|
|
if (Removed)
|
|
{
|
|
DebugConsole.ThrowError("Attempting to revive an already removed character\n" + Environment.StackTrace);
|
|
return;
|
|
}
|
|
|
|
IsDead = false;
|
|
|
|
if (aiTarget != null)
|
|
{
|
|
aiTarget.Remove();
|
|
}
|
|
|
|
aiTarget = new AITarget(this);
|
|
SetAllDamage(0.0f, 0.0f, 0.0f);
|
|
CharacterHealth.RemoveAllAfflictions();
|
|
|
|
foreach (LimbJoint joint in AnimController.LimbJoints)
|
|
{
|
|
joint.MotorEnabled = true;
|
|
joint.Enabled = true;
|
|
joint.IsSevered = false;
|
|
}
|
|
|
|
foreach (Limb limb in AnimController.Limbs)
|
|
{
|
|
#if CLIENT
|
|
if (limb.LightSource != null) limb.LightSource.Color = limb.InitialLightSourceColor;
|
|
#endif
|
|
limb.body.Enabled = true;
|
|
limb.IsSevered = false;
|
|
}
|
|
|
|
if (GameMain.GameSession != null)
|
|
{
|
|
GameMain.GameSession.ReviveCharacter(this);
|
|
}
|
|
}
|
|
|
|
public override void Remove()
|
|
{
|
|
if (Removed)
|
|
{
|
|
DebugConsole.ThrowError("Attempting to remove an already removed character\n" + Environment.StackTrace);
|
|
return;
|
|
}
|
|
DebugConsole.Log("Removing character " + Name + " (ID: " + ID + ")");
|
|
|
|
base.Remove();
|
|
|
|
if (selectedItems[0] != null) selectedItems[0].Drop(this);
|
|
if (selectedItems[1] != null) selectedItems[1].Drop(this);
|
|
|
|
if (info != null) info.Remove();
|
|
|
|
#if CLIENT
|
|
GameMain.GameSession?.CrewManager?.RemoveCharacter(this);
|
|
#endif
|
|
|
|
CharacterList.Remove(this);
|
|
|
|
if (Inventory != null)
|
|
{
|
|
foreach (Item item in Inventory.Items)
|
|
{
|
|
if (item != null)
|
|
{
|
|
Spawner?.AddToRemoveQueue(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
DisposeProjSpecific();
|
|
|
|
aiTarget?.Remove();
|
|
AnimController?.Remove();
|
|
CharacterHealth?.Remove();
|
|
|
|
foreach (Character c in CharacterList)
|
|
{
|
|
if (c.focusedCharacter == this) c.focusedCharacter = null;
|
|
if (c.SelectedCharacter == this) c.SelectedCharacter = null;
|
|
}
|
|
}
|
|
partial void DisposeProjSpecific();
|
|
|
|
public void TeleportTo(Vector2 worldPos)
|
|
{
|
|
AnimController.CurrentHull = null;
|
|
Submarine = null;
|
|
AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), false);
|
|
AnimController.FindHull(worldPos, true);
|
|
}
|
|
|
|
public void SaveInventory(Inventory inventory, XElement parentElement)
|
|
{
|
|
var items = Array.FindAll(inventory.Items, i => i != null).Distinct();
|
|
foreach (Item item in items)
|
|
{
|
|
item.Submarine = inventory.Owner.Submarine;
|
|
var itemElement = item.Save(parentElement);
|
|
|
|
List<int> slotIndices = new List<int>();
|
|
for (int i = 0; i < inventory.Capacity; i++)
|
|
{
|
|
if (inventory.Items[i] == item) { slotIndices.Add(i); }
|
|
}
|
|
|
|
itemElement.Add(new XAttribute("i", string.Join(",", slotIndices)));
|
|
|
|
foreach (ItemContainer container in item.GetComponents<ItemContainer>())
|
|
{
|
|
XElement childInvElement = new XElement("inventory");
|
|
itemElement.Add(childInvElement);
|
|
SaveInventory(container.Inventory, childInvElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
public AttackContext GetAttackContext() => AnimController.CurrentAnimationParams.IsGroundedAnimation ? AttackContext.Ground : AttackContext.Water;
|
|
}
|
|
}
|