using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma.Items.Components { partial class StatusHUD : ItemComponent { private static readonly LocalizedString[] BleedingTexts = { TextManager.Get("MinorBleeding"), TextManager.Get("Bleeding"), TextManager.Get("HeavyBleeding"), TextManager.Get("CatastrophicBleeding") }; private static readonly LocalizedString[] OxygenTexts = { TextManager.Get("OxygenNormal"), TextManager.Get("OxygenReduced"), TextManager.Get("OxygenLow"), TextManager.Get("NotBreathing") }; [Serialize(500.0f, IsPropertySaveable.No, description: "How close to a target the user must be to see their health data (in pixels).")] public float Range { get; private set; } [Serialize(50.0f, IsPropertySaveable.No, description: "The range within which the health info texts fades out.")] public float FadeOutRange { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool ThermalGoggles { get; private set; } [Serialize(false, IsPropertySaveable.No)] public bool DebugWiring { get; private set; } [Serialize(true, IsPropertySaveable.No)] public bool ShowDeadCharacters { get; private set; } [Serialize(true, IsPropertySaveable.No)] public bool ShowTexts { get; private set; } [Serialize("72,119,72,120", IsPropertySaveable.No)] public Color OverlayColor { get; private set; } private readonly List visibleCharacters = new List(); private const float UpdateInterval = 0.5f; private float updateTimer; private Character equipper; private bool isEquippable; private float thermalEffectState; public IEnumerable VisibleCharacters { get { if (equipper == null || equipper.Removed) { return Enumerable.Empty(); } return visibleCharacters; } } public override void OnItemLoaded() { isEquippable = item.GetComponent() != null; if (!isEquippable) { IsActive = true; } } public override void Update(float deltaTime, Camera cam) { base.Update(deltaTime, cam); Entity refEntity = equipper; if (isEquippable) { if (equipper == null || equipper.Removed) { IsActive = false; return; } } else { refEntity = item; } if (equipper != null && equipper == Character.Controlled && DebugWiring) { ConnectionPanel.DebugWiringEnabledUntil = Timing.TotalTimeUnpaused + 0.5; } thermalEffectState += deltaTime; thermalEffectState %= 10000.0f; if (updateTimer > 0.0f) { updateTimer -= deltaTime; return; } visibleCharacters.Clear(); foreach (Character c in Character.CharacterList) { if (c == equipper || !c.Enabled || c.Removed) { continue; } if (!ShowDeadCharacters && c.IsDead) { continue; } if (c.InDetectable) { continue; } float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); if (dist < Range * Range) { Vector2 diff = c.WorldPosition - refEntity.WorldPosition; if (Submarine.CheckVisibility(refEntity.SimPosition, refEntity.SimPosition + ConvertUnits.ToSimUnits(diff)) == null) { visibleCharacters.Add(c); } } } updateTimer = UpdateInterval; } public override void Equip(Character character) { updateTimer = 0.0f; equipper = character; IsActive = true; } public override void Unequip(Character character) { equipper = null; IsActive = false; } public override void Drop(Character dropper, bool setTransform = true) { Unequip(dropper); } public override void DrawHUD(SpriteBatch spriteBatch, Character character) { if (character == null) { return; } base.DrawHUD(spriteBatch, character); if (OverlayColor.A > 0) { GUIStyle.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), OverlayColor); } if (ShowTexts) { Character closestCharacter = null; float closestDist = float.PositiveInfinity; foreach (Character c in visibleCharacters) { if (c == character || !c.Enabled || c.Removed) { continue; } float dist = Vector2.DistanceSquared(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), c.WorldPosition); if (dist < closestDist) { closestCharacter = c; closestDist = dist; } } if (closestCharacter != null) { float dist = Vector2.Distance(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), closestCharacter.WorldPosition); DrawCharacterInfo(spriteBatch, closestCharacter, 1.0f - MathHelper.Max((dist - (Range - FadeOutRange)) / FadeOutRange, 0.0f)); } } if (ThermalGoggles) { Entity refEntity = equipper; if (!isEquippable || refEntity == null) { refEntity = item; } DrawThermalOverlay(spriteBatch, refEntity, character, OverlayColor, Range, thermalEffectState, ShowDeadCharacters); } } public static void DrawThermalOverlay(SpriteBatch spriteBatch, Entity refEntity, Character user, Color overlayColor, float range, float effectState, bool showDeadCharacters) { spriteBatch.End(); float colorIntensityBase = 0.5f; //Multiplies the overlay color by this amount, the higher the value, the more bright/vibrant the color. float colorIntensityVariance = 0.05f; //The variance of the pulse effect affecting the color's brightness/vibrance GameMain.LightManager.SolidColorEffect.Parameters["color"].SetValue(overlayColor.ToVector4() * (colorIntensityBase + MathF.Sin(effectState) * colorIntensityVariance)); GameMain.LightManager.SolidColorEffect.CurrentTechnique = GameMain.LightManager.SolidColorEffect.Techniques["SolidColorBlur"]; GameMain.LightManager.SolidColorEffect.Parameters["blurDistance"].SetValue(0.01f + MathF.Sin(effectState) * 0.005f); GameMain.LightManager.SolidColorEffect.CurrentTechnique.Passes[0].Apply(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: Screen.Selected.Cam.Transform, effect: GameMain.LightManager.SolidColorEffect); foreach (Character c in Character.CharacterList) { if (c == user || !c.Enabled || c.Removed || c.Params.HideInThermalGoggles) { continue; } if (!showDeadCharacters && c.IsDead) { continue; } float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); if (dist > range * range) { continue; } Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite; foreach (Limb limb in c.AnimController.Limbs) { if (limb.Mass < 0.5f && limb != c.AnimController.MainLimb) { continue; } float noise1 = PerlinNoise.GetPerlin((effectState + limb.Params.ID + c.ID) * 0.01f, (effectState + limb.Params.ID + c.ID) * 0.02f); float noise2 = PerlinNoise.GetPerlin((effectState + limb.Params.ID + c.ID) * 0.01f, (effectState + limb.Params.ID + c.ID) * 0.008f); Vector2 spriteScale = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) / pingCircle.size * (noise1 * 0.5f + 2f); Vector2 drawPos = new Vector2(limb.body.DrawPosition.X + (noise1 - 0.5f) * 100, -limb.body.DrawPosition.Y + (noise2 - 0.5f) * 100); pingCircle.Draw(spriteBatch, drawPos, 0.0f, scale: Math.Max(spriteScale.X, spriteScale.Y)); } } spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); } private void DrawCharacterInfo(SpriteBatch spriteBatch, Character target, float alpha = 1.0f) { Vector2 hudPos = GameMain.GameScreen.Cam.WorldToScreen(target.DrawPosition); hudPos += Vector2.UnitX * 50.0f; List texts = new List(); List textColors = new List(); texts.Add(target.Info == null ? target.DisplayName : target.Info.DisplayName); Color nameColor = GUIStyle.TextColorNormal; if (Character.Controlled != null && target.TeamID != Character.Controlled.TeamID) { nameColor = target.TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUIStyle.Red; } textColors.Add(nameColor); if (target.IsDead) { texts.Add(TextManager.Get("Deceased")); textColors.Add(GUIStyle.Red); if (target.CauseOfDeath != null) { texts.Add( target.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? TextManager.AddPunctuation(':', TextManager.Get("CauseOfDeath"), TextManager.Get("CauseOfDeath." + target.CauseOfDeath.Type.ToString()))); textColors.Add(GUIStyle.Red); } } else { if (target.ShouldShowCustomInteractText) { texts.Add(target.CustomInteractHUDText); textColors.Add(GUIStyle.Green); } if (!target.IsIncapacitated && target.IsPet) { texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use)); textColors.Add(GUIStyle.Green); } if (equipper?.FocusedCharacter == target && target.CanBeHealedBy(equipper, checkFriendlyTeam: false)) { texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health)); textColors.Add(GUIStyle.Green); } if (target.CanBeDraggedBy(Character.Controlled)) { texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab)); textColors.Add(GUIStyle.Green); } if (target.IsUnconscious) { texts.Add(TextManager.Get("Unconscious")); textColors.Add(GUIStyle.Orange); } if (target.Stun > 0.01f) { texts.Add(TextManager.Get("Stunned")); textColors.Add(GUIStyle.Orange); } int oxygenTextIndex = MathHelper.Clamp((int)Math.Floor((1.0f - (target.Oxygen / 100.0f)) * OxygenTexts.Length), 0, OxygenTexts.Length - 1); texts.Add(OxygenTexts[oxygenTextIndex]); textColors.Add(Color.Lerp(GUIStyle.Red, GUIStyle.Green, target.Oxygen / 100.0f)); if (target.Bleeding > 0.0f) { int bleedingTextIndex = MathHelper.Clamp((int)Math.Floor(target.Bleeding / 100.0f * BleedingTexts.Length), 0, BleedingTexts.Length - 1); texts.Add(BleedingTexts[bleedingTextIndex]); textColors.Add(Color.Lerp(GUIStyle.Orange, GUIStyle.Red, target.Bleeding / 100.0f)); } var allAfflictions = target.CharacterHealth.GetAllAfflictions(); Dictionary combinedAfflictionStrengths = new Dictionary(); foreach (Affliction affliction in allAfflictions) { if (affliction.Strength <= 0f) { continue; } if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold) { if (target.IsHuman || target.IsOnPlayerTeam || (affliction.Prefab.AfflictionType != AfflictionPrefab.PoisonType && affliction.Prefab.AfflictionType != AfflictionPrefab.ParalysisType)) { // Always show the poisons on monsters, because poisoning bigger monsters require multiple doses. // The solution is hacky, but didn't want to introduce an extra property for this. // We also want to have a relatively high thershold for showing the poisons on the scanner on humans, so that it's not instantly clear that a target is poisoned and especially not which poison was used. // Paralysis is treated like a poison but isn't technically a poison, so that we can have multiple afflictions that still are treated the same. continue; } } if (combinedAfflictionStrengths.ContainsKey(affliction.Prefab)) { combinedAfflictionStrengths[affliction.Prefab] += affliction.Strength; } else { combinedAfflictionStrengths[affliction.Prefab] = affliction.Strength; } } foreach (AfflictionPrefab affliction in combinedAfflictionStrengths.Keys) { texts.Add(TextManager.AddPunctuation(':', affliction.Name, Math.Max((int)combinedAfflictionStrengths[affliction], 1).ToString() + " %")); textColors.Add(Color.Lerp(GUIStyle.Orange, GUIStyle.Red, combinedAfflictionStrengths[affliction] / affliction.MaxStrength)); } } GUI.DrawString(spriteBatch, hudPos, texts[0].Value, textColors[0] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); hudPos.X += 5.0f * GUI.Scale; hudPos.Y += GUIStyle.SubHeadingFont.MeasureString(texts[0].Value).Y; hudPos.X = (int)hudPos.X; hudPos.Y = (int)hudPos.Y; for (int i = 1; i < texts.Count; i++) { GUI.DrawString(spriteBatch, hudPos, texts[i], textColors[i] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SmallFont); hudPos.Y += (int)(GUIStyle.SubHeadingFont.MeasureString(texts[i].Value).Y); } } } }