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 CharacterList = new List(); 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 Properties; public Dictionary 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 statusEffects = new List(); private List speedMultipliers = new List(); public Entity ViewTarget { get; set; } public Vector2 AimRefPosition { get { if (ViewTarget == null) return AnimController.AimSourcePos; if (ViewTarget is Item targetItem) { Turret turret = targetItem.GetComponent(); 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 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; /// /// Create a new character /// /// The name, gender, config file, etc of the character. /// Position in display units. /// RNG seed to use if the character config has randomizable parameters. /// Is the character controlled by a remote player. /// Is the character controlled by AI. /// Ragdoll configuration file. If null, will select the default. 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); } /// /// Create a new character /// /// The path to the character's config file. /// Position in display units. /// RNG seed to use if the character config has randomizable parameters. /// The name, gender, etc of the character. Only used for humans, and if the parameter is not given, a random CharacterInfo is generated. /// Is the character controlled by a remote player. /// Is the character controlled by AI. /// Should clients receive a network event about the creation of this character? /// Ragdoll configuration file. If null, will select the default. 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 ragdollElements = new List(); //List ragdollCommonness = new List(); //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 healthElements = new List(); List healthCommonness = new List(); 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 characterConfigFiles; private static IEnumerable 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; } /// /// Can be used to modify the character's speed via StatusEffects /// 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 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(); 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(); 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; } /// /// Set an action that's invoked when another character interacts with this one. /// /// Action invoked when another character interacts with this one. T1 = this character, T2 = the interacting character /// Displayed on the character when highlighted. public void SetCustomInteract(Action 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(); 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 aiChatMessageQueue = new List(); private List prevAiChatMessages = new List(); 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 sentMessages = new List(); 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); } /// /// Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to worldPosition will receive the damage. /// 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 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 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 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(); 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 slotIndices = new List(); 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()) { XElement childInvElement = new XElement("inventory"); itemElement.Add(childInvElement); SaveInventory(container.Inventory, childInvElement); } } } public AttackContext GetAttackContext() => AnimController.CurrentAnimationParams.IsGroundedAnimation ? AttackContext.Ground : AttackContext.Water; } }