b48aa89004
commit e6715d605db9bb1d608e4a4990ac41f6214f61d1 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Tue Mar 5 17:22:31 2019 +0200 Fixed the "control" console command not being usable by clients, changed the way arguments are given to the "setclientcharacter" command (no semicolon to separate the names, quotation marks have to be used for multi-word names just like with any other command). Closes #1224 commit acb7c1e0dc05bf619e7ec4875196cc45647d3fb4 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Tue Mar 5 16:30:38 2019 +0200 Steam Workshop fixes: - Install content packages for items that only contain a sub. Otherwise the system can't determine if the item has been updated (and might also be useful for toggling off incompatible subs when switching from mod to another). - Log an error instead of crashing the game if CheckWorkshopItemEnabled or CheckWorkshopItemUpToDate fails due to a missing filelist.xml. - Show a messagebox when a workshop item is updated succesfully. commit b2e8ed565bc03f466930799268c13b2fca4bb9c9 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Tue Mar 5 14:43:42 2019 +0200 Fixed nullref exception when disabling a workshop item that doesn't have an update button (or when enabling the item fails) commit 26f1f285cd80ca6f023b12e6dd80dc71e87ee9c3 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Tue Mar 5 14:15:14 2019 +0200 Fixed console command aliases not being taken into account in GameClient.HasConsoleCommandPermission (meaning that the client needed a permission for each name variant of a command, making it impossible to for example use "fixwalls" instead of "fixhulls"). Closes #1225 commit dee02de681a212efd0e0a82c14619f3fe4839cc4 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Tue Mar 5 13:35:48 2019 +0200 Fixed traitor rounds failing to start if there's no owner client, fixed occasional "traitorCount somehow ended up less than 1" errors due to Rand.Int using 0 as the minimum value. Closes #1217
2540 lines
95 KiB
C#
2540 lines
95 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>();
|
|
|
|
private List<StatusEffect> statusEffects = new List<StatusEffect>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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 = null, int? beardIndex = null, int? moustacheIndex = null, int? faceAttachmentIndex = null)
|
|
{
|
|
if (Info == null) { return; }
|
|
var head = AnimController.GetLimb(LimbType.Head);
|
|
if (head == null) { return; }
|
|
if (headId.HasValue)
|
|
{
|
|
Info.RecreateHead(headId.Value, Info.Race, Info.Gender, hairIndex ?? -1, beardIndex ?? -1, moustacheIndex ?? -1, faceAttachmentIndex ?? -1);
|
|
}
|
|
#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)));
|
|
}
|
|
|
|
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 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;
|
|
}
|
|
|
|
targetMovement *= AnimController.GetCurrentSpeed(run);
|
|
float maxSpeed = GetCurrentMaxSpeed(run);
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies temporary limits to the speed (damage).
|
|
/// </summary>
|
|
public float GetCurrentMaxSpeed(bool run)
|
|
{
|
|
float currMaxSpeed = AnimController.GetCurrentSpeed(run);
|
|
|
|
//?
|
|
//currMaxSpeed *= 1.5f;
|
|
|
|
var leftFoot = AnimController.GetLimb(LimbType.LeftFoot);
|
|
if (leftFoot != null)
|
|
{
|
|
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", leftFoot, true);
|
|
currMaxSpeed *= 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);
|
|
currMaxSpeed *= MathHelper.Lerp(1.0f, 0.25f, MathHelper.Clamp(footAfflictionStrength / 100.0f, 0.0f, 1.0f));
|
|
}
|
|
|
|
return currMaxSpeed;
|
|
}
|
|
|
|
public void Control(float deltaTime, Camera cam)
|
|
{
|
|
ViewTarget = null;
|
|
if (!AllowInput) return;
|
|
|
|
Vector2 smoothedCursorDiff = cursorPosition - SmoothedCursorPosition;
|
|
if (Controlled == this)
|
|
{
|
|
SmoothedCursorPosition = cursorPosition;
|
|
}
|
|
else
|
|
{
|
|
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
|
|
if (SelectedConstruction == 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 (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;
|
|
else if (allowRagdoll && (!IsRagdolled || AnimController.Collider.LinearVelocity.LengthSquared() < 1f)) //Keep us ragdolled if we were forced or we're too speedy to unragdoll
|
|
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
|
|
|
|
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, 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);
|
|
itemElement.Add(new XAttribute("i", Array.IndexOf(inventory.Items, item)));
|
|
|
|
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;
|
|
}
|
|
}
|