1889 lines
92 KiB
C#
1889 lines
92 KiB
C#
using System.Reflection;
|
|
using System.Collections.Generic;
|
|
using System;
|
|
using Barotrauma;
|
|
using Barotrauma.Sounds;
|
|
using Barotrauma.Extensions;
|
|
using Barotrauma.Items.Components;
|
|
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using System.Linq;
|
|
|
|
namespace BetterHealthUI {
|
|
partial class BetterHealthUIMod {
|
|
|
|
private int healthGUIRefreshTimer = 0;
|
|
public static bool NeurotraumaEnabled = false;
|
|
private static Random rnd = new Random();
|
|
private static CharacterHealth prevOpenHealthWindow = null;
|
|
|
|
public void InitClient() {
|
|
|
|
// Check if Neurotrauma is enabled
|
|
foreach (ContentPackage package in ContentPackageManager.EnabledPackages.All)
|
|
{
|
|
if (package.NameMatches("Neurotrauma"))
|
|
{
|
|
NeurotraumaEnabled = true;
|
|
break;
|
|
}
|
|
}
|
|
if (NeurotraumaEnabled)
|
|
{
|
|
ecgCurve = new CCurve();
|
|
ecgCurve.Keys.Add(new CCurveKey(0,0)); // start
|
|
ecgCurve.Keys.Add(new CCurveKey(0.1f,-0.1f)); // first low
|
|
ecgCurve.Keys.Add(new CCurveKey(0.2f,1f)); // high
|
|
ecgCurve.Keys.Add(new CCurveKey(0.3f,-0.3f)); // second low
|
|
ecgCurve.Keys.Add(new CCurveKey(0.4f, 0f)); // end
|
|
|
|
ecgCurveFib = new CCurve();
|
|
ecgCurveFib.Keys.Add(new CCurveKey(0, -0.9f));
|
|
ecgCurveFib.Keys.Add(new CCurveKey(0.05f, -0.2f));
|
|
ecgCurveFib.Keys.Add(new CCurveKey(0.1f, 0.3f));
|
|
ecgCurveFib.Keys.Add(new CCurveKey(0.15f, -0.2f));
|
|
ecgCurveFib.Keys.Add(new CCurveKey(0.2f, -0.9f));
|
|
}
|
|
|
|
//LuaCsSetup.PrintCsMessage("BetterHealthUIMod.InitClient");
|
|
foreach (Character character in Character.CharacterList)
|
|
{
|
|
CharacterHealth charHealth = character.CharacterHealth;
|
|
ForceCustomized(charHealth);
|
|
}
|
|
|
|
void ForceCustomized(CharacterHealth selfHealth)
|
|
{
|
|
GUIListBox afflictionIconContainer = (GUIListBox)((typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance)).GetValue(selfHealth));
|
|
|
|
var afflictionIconContainer2 = afflictionIconContainer != null ? (afflictionIconContainer.Parent.GetChildByUserData("afflictionIconContainer2")) : null;
|
|
if (afflictionIconContainer2 == null && afflictionIconContainer!= null)
|
|
{
|
|
// we havent added our custom stuff yet, do it NOW!
|
|
LuaCsSetup.PrintCsMessage("Forcing customization on " + selfHealth.Character.Name);
|
|
|
|
Dictionary<string, object> args = new Dictionary<string, object>();
|
|
args.Add("character", selfHealth.Character);
|
|
InitProjSpecific(selfHealth, args);
|
|
}
|
|
}
|
|
|
|
// health window init
|
|
// changes dimensions of the health window
|
|
// changes job icon, character portrait and name layout and color
|
|
GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_InitProjSpecific",
|
|
typeof(CharacterHealth).GetMethod("InitProjSpecific", BindingFlags.Instance | BindingFlags.NonPublic),
|
|
(object self, Dictionary<string, object> args) => {
|
|
InitProjSpecific(self, args);
|
|
return null;
|
|
}, LuaCsHook.HookMethodType.After, this);
|
|
void InitProjSpecific(object self, Dictionary<string, object> args)
|
|
{
|
|
#region Reflection crap
|
|
// get arguments
|
|
Character character = (Character)(args["character"]);
|
|
// get members
|
|
CharacterHealth selfHealth = (CharacterHealth)self;
|
|
GUITextBlock characterName = (GUITextBlock)(typeof(CharacterHealth).GetField("characterName", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
GUIFrame healthWindow = (GUIFrame)(typeof(CharacterHealth).GetField("healthWindow", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
#endregion
|
|
|
|
var healthWindowVerticalLayout = (GUILayoutGroup)(healthWindow.GetChild(0));
|
|
var characterIndicatorArea = (GUILayoutGroup)(healthWindowVerticalLayout.GetChild(3));
|
|
|
|
// set size of health window
|
|
healthWindow.RectTransform.RelativeSize = new Vector2(0.8f, 0.6f);
|
|
|
|
// clear portrait, name and icon
|
|
var topContainer = characterName.RectTransform.Parent;
|
|
topContainer.ClearChildren();
|
|
|
|
// create own layout
|
|
topContainer.Parent.RelativeSize = new Vector2(0.975f, 0.95f);
|
|
|
|
// job icon
|
|
var jobIcon = new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), topContainer),
|
|
onDraw: (spriteBatch, component) =>
|
|
{
|
|
character.Info?.DrawJobIcon(spriteBatch, component.Rect, character != Character.Controlled);
|
|
});
|
|
//jobIcon.RectTransform.RelativeSize *= 1f;
|
|
//jobIcon.RectTransform.SetPosition(Anchor.TopLeft);
|
|
|
|
// char portrait
|
|
var characterPortrait = new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), topContainer, Anchor.CenterLeft),
|
|
onDraw: (spriteBatch, component) =>
|
|
{
|
|
character.Info?.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), new Vector2(-40, 15), component.Rect.Width, false, character != Character.Controlled);
|
|
});
|
|
characterPortrait.RectTransform.RelativeSize *= 0.7f;
|
|
//characterPortrait.RectTransform.Anchor = Anchor.TopLeft;
|
|
//characterPortrait.RectTransform.RelativeOffset = new Vector2(0f, 0.3f);
|
|
|
|
// char name
|
|
characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), topContainer, anchor: Anchor.CenterLeft), "", textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont)
|
|
{
|
|
AutoScaleHorizontal = true,
|
|
AutoScaleVertical = true
|
|
};
|
|
characterName.TextOffset = new Vector2(-50, 0);
|
|
|
|
// color the name according to the job
|
|
if (character.Info?.Job?.Prefab.UIColor != null)
|
|
characterName.TextColor = character.Info.Job.Prefab.UIColor;
|
|
|
|
// resize the gene splicer slot
|
|
selfHealth.InventorySlotContainer.RectTransform.RelativeSize *= 0.5f;
|
|
|
|
// resize the cpr button
|
|
var cprButton = (GUIButton)(characterIndicatorArea.GetChild(1));
|
|
cprButton.RectTransform.RelativeSize = new Vector2(0.17f, 0.17f);
|
|
|
|
// resize the limb man area
|
|
var limbSelection = (GUICustomComponent)(characterIndicatorArea.GetChild(2));
|
|
limbSelection.RectTransform.RelativeSize = new Vector2(0.3f, 1.0f);
|
|
|
|
// resize the first affliction info list (non-buff)
|
|
afflictionIconContainer.RectTransform.RelativeSize = new Vector2(0.4f, 1);
|
|
// create the second affliction info list (buff)
|
|
var afflictionIconContainer2 = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform), style: null)
|
|
{
|
|
UserData = "afflictionIconContainer2",
|
|
};
|
|
|
|
// not sure why i need to do this twice but if i dont then the inventoryslot isnt at the right place
|
|
// the first time the health window is opened
|
|
// nvm, even then it doesnt if in singleplayer
|
|
characterIndicatorArea.Recalculate();
|
|
|
|
// Neurotrauma specific elements
|
|
if (NeurotraumaEnabled && !BetterHealthUIMod.DisableHeartbeatBar)
|
|
{
|
|
var children = healthWindow.RectTransform.Children;
|
|
List<RectTransform> newChildren = new List<RectTransform>();
|
|
|
|
var graphArea = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.15f), healthWindow.RectTransform, anchor : Anchor.TopRight))
|
|
{
|
|
Stretch = true,
|
|
RelativeSpacing = 0.02f,
|
|
};
|
|
|
|
// rearrange the health windows children so that the graph gets rendered behind the name
|
|
newChildren.Add(graphArea.RectTransform);
|
|
foreach(RectTransform child in healthWindow.RectTransform.Children)
|
|
{
|
|
if (child == graphArea.RectTransform) continue;
|
|
newChildren.Add(child);
|
|
}
|
|
|
|
((List<RectTransform>)(healthWindow.RectTransform.Children)).Clear();
|
|
foreach (RectTransform child in newChildren)
|
|
{
|
|
((List<RectTransform>)(healthWindow.RectTransform.Children)).Add(child);
|
|
}
|
|
|
|
var graph = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "GUIFrameListBox");
|
|
new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graph.RectTransform, Anchor.Center), DrawGraph, null);
|
|
|
|
void DrawGraph(SpriteBatch spriteBatch, GUICustomComponent container)
|
|
{
|
|
//if (item.Removed) { return; }
|
|
float maxLoad = 100;// loadGraph.Max();
|
|
float xOffset = 0;// graphTimer / updateGraphInterval;
|
|
Rectangle graphRect = new Rectangle(container.Rect.X, container.Rect.Y, container.Rect.Width, container.Rect.Height - (int)(5 * GUI.yScale));
|
|
DrawHeartGraph(heartGraph, spriteBatch, graphRect, xOffset);
|
|
}
|
|
|
|
void DrawHeartGraph(IList<float> graph, SpriteBatch spriteBatch, Rectangle rect, float xOffset)
|
|
{
|
|
Color color = Color.Green;
|
|
const float maxVal = 2;
|
|
|
|
Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
|
|
spriteBatch.End();
|
|
spriteBatch.GraphicsDevice.ScissorRectangle = rect;
|
|
spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable);
|
|
|
|
float lineWidth = (float)rect.Width / (float)(graph.Count - 2);
|
|
float yScale = (float)rect.Height / maxVal;
|
|
|
|
Vector2 prevPoint = new Vector2(rect.Left, rect.Bottom - (graph[1] + (graph[0] - graph[1]) * xOffset) * yScale);
|
|
|
|
float currX = rect.Left + ((xOffset - 1.0f) * lineWidth);
|
|
|
|
for (int i = 1; i < graph.Count - 1; i++)
|
|
{
|
|
float age = 1 - ((i - heartGraphProgress + graph.Count) % graph.Count) / (float)graph.Count;
|
|
|
|
currX += lineWidth;
|
|
|
|
Vector2 newPoint = new Vector2(currX, rect.Bottom - graph[i] * yScale);
|
|
|
|
color = age < 0.025 ? Color.White:Color.Lerp(Color.LimeGreen, Color.Black, age);
|
|
GUI.DrawLine(spriteBatch, prevPoint, newPoint + new Vector2(1.0f, 0), color,0,age < 0.025?4:2);
|
|
|
|
prevPoint = newPoint;
|
|
}
|
|
|
|
Vector2 lastPoint = new Vector2(rect.Right,
|
|
rect.Bottom - (graph[graph.Count - 1] + (graph[graph.Count - 2] - graph[graph.Count - 1]) * xOffset) * yScale);
|
|
|
|
|
|
GUI.DrawLine(spriteBatch, prevPoint, lastPoint, color);
|
|
|
|
|
|
spriteBatch.End();
|
|
spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
|
|
spriteBatch.Begin(SpriteSortMode.Deferred);
|
|
}
|
|
}
|
|
|
|
// reflection apply
|
|
typeof(CharacterHealth).GetField("characterName", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(self, characterName);
|
|
}
|
|
|
|
GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_UpdateOxygenProjSpecific",
|
|
typeof(CharacterHealth).GetMethod("UpdateOxygenProjSpecific", BindingFlags.Instance | BindingFlags.NonPublic),
|
|
(object self, Dictionary<string, object> args) => {
|
|
// prevent heart sounds through low oxygen if neurotrauma is enabled
|
|
if(NeurotraumaEnabled)
|
|
{
|
|
if (CharacterHealth.OpenHealthWindow == null&& flatlineSoundChannel!=null)
|
|
{
|
|
flatlineSoundChannel.Dispose();
|
|
flatlineSoundChannel = null;
|
|
}
|
|
return true;
|
|
}
|
|
return null;
|
|
}, LuaCsHook.HookMethodType.Before, this);
|
|
|
|
// CreateRecommendedTreatments override
|
|
// sets the max amount of displayed treatments to 10
|
|
const int maxDisplayedSuitableTreatments = 10;
|
|
GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_CreateRecommendedTreatments",
|
|
typeof(CharacterHealth).GetMethod("CreateRecommendedTreatments", BindingFlags.Instance | BindingFlags.NonPublic),
|
|
(object self, Dictionary<string, object> args) => {
|
|
// get members
|
|
CharacterHealth selfHealth = (CharacterHealth)self;
|
|
GUIListBox recommendedTreatmentContainer = (GUIListBox)(typeof(CharacterHealth).GetField("recommendedTreatmentContainer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
int selectedLimbIndex = (int)(typeof(CharacterHealth).GetField("selectedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
//GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconContainer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
|
|
// override begins
|
|
ItemPrefab prevHighlightedItem = null;
|
|
if (GUI.MouseOn?.UserData is ItemPrefab && recommendedTreatmentContainer.Content.IsParentOf(GUI.MouseOn))
|
|
{
|
|
prevHighlightedItem = (ItemPrefab)GUI.MouseOn.UserData;
|
|
}
|
|
|
|
recommendedTreatmentContainer.Content.ClearChildren();
|
|
|
|
float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel("medical");
|
|
|
|
//key = item identifier
|
|
//float = suitability
|
|
Dictionary<Identifier, float> treatmentSuitability = new Dictionary<Identifier, float>();
|
|
selfHealth.GetSuitableTreatments(treatmentSuitability,
|
|
ignoreHiddenAfflictions: true,
|
|
limb: selectedLimbIndex == -1 ? null : selfHealth.Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex),
|
|
user : Character.Controlled);
|
|
|
|
foreach (Identifier treatment in treatmentSuitability.Keys.ToList())
|
|
{
|
|
//prefer suggestions for items the player has
|
|
if (Character.Controlled.Inventory.FindItemByIdentifier(treatment, recursive: true) != null)
|
|
{
|
|
treatmentSuitability[treatment] *= 10.0f;
|
|
}
|
|
}
|
|
|
|
if (!treatmentSuitability.Any())
|
|
{
|
|
new GUITextBlock(new RectTransform(Vector2.One, recommendedTreatmentContainer.Content.RectTransform), TextManager.Get("none"), textAlignment: Alignment.Center)
|
|
{
|
|
CanBeFocused = false
|
|
};
|
|
recommendedTreatmentContainer.ScrollBarVisible = false;
|
|
recommendedTreatmentContainer.AutoHideScrollBar = false;
|
|
}
|
|
else
|
|
{
|
|
recommendedTreatmentContainer.ScrollBarVisible = true;
|
|
recommendedTreatmentContainer.AutoHideScrollBar = true;
|
|
}
|
|
|
|
List<KeyValuePair<Identifier, float>> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList();
|
|
|
|
int count = 0;
|
|
foreach (KeyValuePair<Identifier, float> treatment in treatmentSuitabilities)
|
|
{
|
|
if (treatment.Value < 0) { continue; }
|
|
count++;
|
|
if (count > maxDisplayedSuitableTreatments) { break; }
|
|
if (!(MapEntityPrefab.Find(name: null, identifier: treatment.Key, showErrorMessages: false) is ItemPrefab item)) { continue; }
|
|
|
|
var itemSlot = new GUIFrame(new RectTransform(new Vector2(1.0f / (maxDisplayedSuitableTreatments+1.0f), 1.0f), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopLeft),
|
|
style: null)
|
|
{
|
|
UserData = item
|
|
};
|
|
|
|
var innerFrame = new GUIButton(new RectTransform(Vector2.One, itemSlot.RectTransform, Anchor.Center, Pivot.Center, scaleBasis: ScaleBasis.Smallest), style: "SubtreeHeader")
|
|
{
|
|
UserData = item,
|
|
DisabledColor = Color.White * 0.1f,
|
|
OnClicked = (btn, userdata) =>
|
|
{
|
|
if (!(userdata is ItemPrefab itemPrefab)) { return false; }
|
|
var item = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true);
|
|
if (item == null) { return false; }
|
|
Limb targetLimb = selfHealth.Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex);
|
|
item.ApplyTreatment(Character.Controlled, selfHealth.Character, targetLimb);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
new GUIImage(new RectTransform(Vector2.One, innerFrame.RectTransform, Anchor.Center), style: "TalentBackgroundGlow")
|
|
{
|
|
CanBeFocused = false,
|
|
Color = GUIStyle.Green,
|
|
HoverColor = Color.White,
|
|
PressedColor = Color.DarkGray,
|
|
SelectedColor = Color.Transparent,
|
|
DisabledColor = Color.Transparent
|
|
};
|
|
|
|
Sprite itemSprite = item.InventoryIcon ?? item.Sprite;
|
|
Color itemColor = itemSprite == item.Sprite ? item.SpriteColor : item.InventoryIconColor;
|
|
var itemIcon = new GUIImage(new RectTransform(new Vector2(0.8f, 0.8f), innerFrame.RectTransform, Anchor.Center),
|
|
itemSprite, scaleToFit: true)
|
|
{
|
|
CanBeFocused = false,
|
|
Color = itemColor * 0.9f,
|
|
HoverColor = itemColor,
|
|
SelectedColor = itemColor,
|
|
DisabledColor = itemColor * 0.8f
|
|
};
|
|
|
|
if (item == prevHighlightedItem)
|
|
{
|
|
innerFrame.State = GUIComponent.ComponentState.Hover;
|
|
innerFrame.Children.ForEach(c => c.State = GUIComponent.ComponentState.Hover);
|
|
}
|
|
}
|
|
|
|
recommendedTreatmentContainer.RecalculateChildren();
|
|
|
|
// got rid of this because it makes the afflictions spazz around
|
|
/*afflictionIconContainer.Content.RectTransform.SortChildren((r1, r2) =>
|
|
{
|
|
var first = r1.GUIComponent.UserData as Affliction;
|
|
var second = r2.GUIComponent.UserData as Affliction;
|
|
int dmgPerSecond = Math.Sign(second.DamagePerSecond - first.DamagePerSecond);
|
|
return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(second.Strength - first.Strength);
|
|
});*/
|
|
|
|
if (count > 0)
|
|
{
|
|
var treatmentIconSize = recommendedTreatmentContainer.Content.Children.Sum(c => c.Rect.Width + recommendedTreatmentContainer.Spacing);
|
|
if (treatmentIconSize < recommendedTreatmentContainer.Content.Rect.Width)
|
|
{
|
|
var spacing = new GUIFrame(new RectTransform(new Point((recommendedTreatmentContainer.Content.Rect.Width - treatmentIconSize) / 2, 0), recommendedTreatmentContainer.Content.RectTransform), style: null)
|
|
{
|
|
CanBeFocused = false
|
|
};
|
|
spacing.RectTransform.SetAsFirstChild();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}, LuaCsHook.HookMethodType.Before, this);
|
|
|
|
// CreateAfflictionInfoElements override
|
|
// The method responsible for the little info box that appears when hovering over an affliction in the health interface
|
|
GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_CreateAfflictionInfoElements",
|
|
typeof(CharacterHealth).GetMethod("CreateAfflictionInfoElements", BindingFlags.Instance | BindingFlags.NonPublic),
|
|
(object self, Dictionary<string, object> args) => {
|
|
// get arguments
|
|
GUIComponent parent = (GUIComponent)(args["parent"]);
|
|
Affliction affliction = (Affliction)(args["affliction"]);
|
|
|
|
// get members
|
|
CharacterHealth selfHealth = (CharacterHealth)self;
|
|
//LocalizedString[] strengthTexts = (LocalizedString[])(typeof(CharacterHealth).GetField("strengthTexts", BindingFlags.NonPublic | BindingFlags.Static).GetValue(self));
|
|
|
|
// override begins
|
|
var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), parent.RectTransform), isHorizontal: true)
|
|
{
|
|
Stretch = true,
|
|
AbsoluteSpacing = 10,
|
|
UserData = "label",
|
|
CanBeFocused = false
|
|
};
|
|
|
|
var afflictionName = new GUITextBlock(new RectTransform(new Vector2(0.65f, 1.0f), labelContainer.RectTransform), affliction.Prefab.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont)
|
|
{
|
|
CanBeFocused = false,
|
|
AutoScaleHorizontal = true
|
|
};
|
|
var afflictionStrength = new GUITextBlock(new RectTransform(new Vector2(0.35f, 0.6f), labelContainer.RectTransform), "", textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont)
|
|
{
|
|
UserData = "strength",
|
|
CanBeFocused = false
|
|
};
|
|
var vitality = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), labelContainer.RectTransform, Anchor.BottomRight), "", textAlignment: Alignment.BottomRight)
|
|
{
|
|
Padding = afflictionStrength.Padding,
|
|
IgnoreLayoutGroups = true,
|
|
UserData = "vitality",
|
|
CanBeFocused = false
|
|
};
|
|
|
|
var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform),
|
|
affliction.Prefab.GetDescription(
|
|
affliction.Strength,
|
|
selfHealth.Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter)
|
|
, textAlignment: Alignment.TopLeft, wrap: true)
|
|
{
|
|
CanBeFocused = false
|
|
};
|
|
|
|
if (description.Font.MeasureString(description.WrappedText).Y > description.Rect.Height)
|
|
{
|
|
description.Font = GUIStyle.SmallFont;
|
|
}
|
|
|
|
Point nameDims = new Point(afflictionName.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f));
|
|
|
|
afflictionStrength.Text = affliction.GetStrengthText();
|
|
|
|
Vector2 strengthDims = GUIStyle.SubHeadingFont.MeasureString(afflictionStrength.Text);
|
|
|
|
labelContainer.RectTransform.Resize(new Point(labelContainer.Rect.Width, nameDims.Y));
|
|
afflictionName.RectTransform.Resize(new Point((int)(labelContainer.Rect.Width - strengthDims.X * 0.99f), nameDims.Y));
|
|
afflictionStrength.RectTransform.Resize(new Point(labelContainer.Rect.Width - afflictionName.Rect.Width, nameDims.Y));
|
|
|
|
afflictionStrength.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red,
|
|
affliction.Strength / affliction.Prefab.MaxStrength);
|
|
|
|
description.RectTransform.Resize(new Point(description.Rect.Width, (int)(description.TextSize.Y + 10)));
|
|
|
|
int vitalityDecrease = (int)affliction.GetVitalityDecrease(selfHealth);
|
|
if (vitalityDecrease == 0)
|
|
{
|
|
vitality.Visible = false;
|
|
}
|
|
else
|
|
{
|
|
vitality.Visible = true;
|
|
vitality.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease;
|
|
vitality.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green :
|
|
Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength);
|
|
}
|
|
|
|
vitality.AutoDraw = true;
|
|
|
|
return true;
|
|
}, LuaCsHook.HookMethodType.Before, this);
|
|
|
|
// DrawHealthWindow override
|
|
// makes it so more than the most severe affliction is displayed (like in legacy (good))
|
|
// also responsible for the limbs color in the health interface
|
|
Vector2 limbGuyOffset = new Vector2(0, 0);
|
|
GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_DrawHealthWindow",
|
|
typeof(CharacterHealth).GetMethod("DrawHealthWindow", BindingFlags.Instance | BindingFlags.NonPublic),
|
|
(object self, Dictionary<string, object> args) => {
|
|
// get arguments
|
|
SpriteBatch spriteBatch = (SpriteBatch)(args["spriteBatch"]);
|
|
Rectangle drawArea = (Rectangle)(args["drawArea"]);
|
|
bool allowHighlight = (bool)(args["allowHighlight"]);
|
|
|
|
DrawHealthWindow(spriteBatch, drawArea, allowHighlight,self);
|
|
return true;
|
|
}, LuaCsHook.HookMethodType.Before, this);
|
|
void DrawHealthWindow(SpriteBatch spriteBatch, Rectangle drawArea, bool allowHighlight,object self)
|
|
{
|
|
#region Reflection crap
|
|
// get members
|
|
CharacterHealth selfHealth = (CharacterHealth)self;
|
|
ForceCustomized(selfHealth);
|
|
//LocalizedString[] strengthTexts = (LocalizedString[])(typeof(CharacterHealth).GetField("strengthTexts", BindingFlags.NonPublic | BindingFlags.Static).GetValue(self));
|
|
List<CharacterHealth.LimbHealth> limbHealths = (List<CharacterHealth.LimbHealth>)(typeof(CharacterHealth).GetField("limbHealths", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
Dictionary<Affliction, CharacterHealth.LimbHealth> afflictions = (Dictionary<Affliction, CharacterHealth.LimbHealth>)(typeof(CharacterHealth).GetField("afflictions", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
SpriteSheet limbIndicatorOverlay = (SpriteSheet)(typeof(CharacterHealth).GetField("limbIndicatorOverlay", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
float limbIndicatorOverlayAnimState = (float)(typeof(CharacterHealth).GetField("limbIndicatorOverlayAnimState", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
int highlightedLimbIndex = (int)(typeof(CharacterHealth).GetField("highlightedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
int selectedLimbIndex = (int)(typeof(CharacterHealth).GetField("selectedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
List<Affliction> afflictionsDisplayedOnLimb = (List<Affliction>)(typeof(CharacterHealth).GetField("afflictionsDisplayedOnLimb", BindingFlags.NonPublic | BindingFlags.Static).GetValue(self));
|
|
GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
GUIListBox afflictionIconContainer2 = (GUIListBox)(afflictionIconContainer.Parent.GetChildByUserData("afflictionIconContainer2"));
|
|
#endregion
|
|
// override begins
|
|
|
|
// clear heart graph if the open window changed
|
|
if (prevOpenHealthWindow!= CharacterHealth.OpenHealthWindow)
|
|
{
|
|
prevOpenHealthWindow = CharacterHealth.OpenHealthWindow;
|
|
if (NeurotraumaEnabled)
|
|
{
|
|
for (int j = 0; j < heartGraph.Length; j++) heartGraph[j] = 0;
|
|
heartGraphProgress = 0;
|
|
}
|
|
}
|
|
|
|
if (selfHealth.Character.Removed) { return; }
|
|
|
|
spriteBatch.End();
|
|
spriteBatch.Begin(SpriteSortMode.Immediate, blendState: BlendState.NonPremultiplied, rasterizerState: GameMain.ScissorTestEnable, effect: GameMain.GameScreen.GradientEffect);
|
|
|
|
int i = 0;
|
|
foreach (CharacterHealth.LimbHealth limbHealth in limbHealths)
|
|
{
|
|
if (limbHealth.IndicatorSprite == null) { continue; }
|
|
|
|
Rectangle limbEffectiveArea = new Rectangle(limbHealth.IndicatorSprite.SourceRect.X + limbHealth.HighlightArea.X,
|
|
limbHealth.IndicatorSprite.SourceRect.Y + limbHealth.HighlightArea.Y,
|
|
limbHealth.HighlightArea.Width,
|
|
limbHealth.HighlightArea.Height);
|
|
|
|
float totalDamage = GetTotalDamage(limbHealth, afflictions, selfHealth);
|
|
|
|
//float negativeEffect = 0.0f, positiveEffect = 0.0f;
|
|
|
|
List<object[]> bottomcolors = new List<object[]>();
|
|
bottomcolors.Add(new object[] { Color.Gray/*new Color(50 / 255f, 66 / 255f, 168 / 255f)*/, 0.02f });
|
|
List<object[]> topcolors = new List<object[]>();
|
|
topcolors.Add(new object[] { Color.Gray/*new Color(50/255f, 66/255f, 168/255f)*/, 0.02f });
|
|
|
|
float topstrength = 0.02f;
|
|
float bottomstrength = 0.02f;
|
|
|
|
foreach (KeyValuePair<Affliction, CharacterHealth.LimbHealth> kvp in afflictions)
|
|
{
|
|
var affliction = kvp.Key;
|
|
if (affliction.Prefab.LimbSpecific)
|
|
{
|
|
if (kvp.Value != limbHealth) continue;
|
|
}
|
|
else
|
|
{
|
|
LimbType displayedType = LimbHealthToLimbType(limbHealth, limbHealths, selfHealth.Character.AnimController.Limbs);
|
|
if (NormalizeLimbType(affliction.Prefab.IndicatorLimb) != NormalizeLimbType(displayedType)) continue;
|
|
}
|
|
|
|
if (!affliction.ShouldShowIcon(selfHealth.Character)) { continue; }
|
|
|
|
float strength = MathHelper.Lerp(0.2f, 1, MathHelper.Clamp(affliction.Strength / affliction.Prefab.MaxStrength * 2, 0, 1));
|
|
|
|
if (affliction.Prefab.IsBuff)
|
|
{
|
|
topcolors.Add(new object[] { CharacterHealth.GetAfflictionIconColor(affliction), strength });
|
|
topstrength += strength;
|
|
}
|
|
else
|
|
{
|
|
bottomcolors.Add(new object[] { CharacterHealth.GetAfflictionIconColor(affliction), strength });
|
|
bottomstrength += strength;
|
|
}
|
|
}
|
|
|
|
float midPoint = (float)(limbEffectiveArea.Center.Y - limbEffectiveArea.Height / 4) / (float)limbHealth.IndicatorSprite.Texture.Height;
|
|
float fadeDist = 0.3f * (float)limbEffectiveArea.Height / (float)limbHealth.IndicatorSprite.Texture.Height;
|
|
|
|
Color color1 = topstrength < bottomstrength ? AverageColor(bottomcolors) : Color.Lerp(AverageColor(topcolors), AverageColor(bottomcolors), bottomstrength);
|
|
Color color2 = Color.Lerp(color1, AverageColor(topcolors), topstrength);
|
|
|
|
GameMain.GameScreen.GradientEffect.Parameters["color1"].SetValue(color1.ToVector4());
|
|
GameMain.GameScreen.GradientEffect.Parameters["color2"].SetValue(color2.ToVector4());
|
|
GameMain.GameScreen.GradientEffect.Parameters["midPoint"].SetValue(midPoint);
|
|
GameMain.GameScreen.GradientEffect.Parameters["fadeDist"].SetValue(fadeDist);
|
|
|
|
float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height);
|
|
|
|
limbHealth.IndicatorSprite.Draw(spriteBatch,
|
|
drawArea.Center.ToVector2()+limbGuyOffset, Color.White,
|
|
limbHealth.IndicatorSprite.Origin,
|
|
0, scale);
|
|
|
|
if (GameMain.DebugDraw)
|
|
{
|
|
Rectangle highlightArea = GetLimbHighlightArea(limbHealth, drawArea,selfHealth);
|
|
|
|
GUI.DrawRectangle(spriteBatch, highlightArea, Color.Red, false);
|
|
GUI.DrawRectangle(spriteBatch, drawArea, Color.Red, false);
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
spriteBatch.End();
|
|
|
|
spriteBatch.Begin(SpriteSortMode.Deferred, Barotrauma.Lights.CustomBlendStates.Multiplicative);
|
|
|
|
if (limbIndicatorOverlay != null)
|
|
{
|
|
float overlayScale = Math.Min(
|
|
drawArea.Width / (float)limbIndicatorOverlay.FrameSize.X,
|
|
drawArea.Height / (float)limbIndicatorOverlay.FrameSize.Y);
|
|
|
|
int frame;
|
|
int frameCount = 17;
|
|
if (limbIndicatorOverlayAnimState >= frameCount * 2) limbIndicatorOverlayAnimState = 0.0f;
|
|
if (limbIndicatorOverlayAnimState < frameCount)
|
|
{
|
|
frame = (int)limbIndicatorOverlayAnimState;
|
|
}
|
|
else
|
|
{
|
|
frame = frameCount - (int)(limbIndicatorOverlayAnimState - (frameCount - 1));
|
|
}
|
|
|
|
limbIndicatorOverlay.Draw(spriteBatch, frame, drawArea.Center.ToVector2()+ limbGuyOffset, Color.Gray, origin: limbIndicatorOverlay.FrameSize.ToVector2() / 2, rotate: 0.0f,
|
|
scale: Vector2.One * overlayScale);
|
|
}
|
|
|
|
if (allowHighlight)
|
|
{
|
|
i = 0;
|
|
foreach (CharacterHealth.LimbHealth limbHealth in limbHealths)
|
|
{
|
|
if (limbHealth.HighlightSprite == null) { continue; }
|
|
|
|
float scale = Math.Min(drawArea.Width / (float)limbHealth.HighlightSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.HighlightSprite.SourceRect.Height);
|
|
|
|
int drawCount = 0;
|
|
if (i == highlightedLimbIndex) { drawCount++; }
|
|
if (i == selectedLimbIndex) { drawCount++; }
|
|
for (int j = 0; j < drawCount; j++)
|
|
{
|
|
limbHealth.HighlightSprite.Draw(spriteBatch,
|
|
drawArea.Center.ToVector2()+limbGuyOffset, Color.White,
|
|
limbHealth.HighlightSprite.Origin,
|
|
0, scale);
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
spriteBatch.End();
|
|
spriteBatch.Begin(SpriteSortMode.Deferred, blendState: BlendState.NonPremultiplied, rasterizerState: GameMain.ScissorTestEnable);
|
|
|
|
// drawing the preview icons on the limbs
|
|
i = 0;
|
|
foreach (CharacterHealth.LimbHealth limbHealth in limbHealths)
|
|
{
|
|
afflictionsDisplayedOnLimb.Clear();
|
|
int negativecount = 0;
|
|
int positivecount = 0;
|
|
int undrawncount = 0;
|
|
foreach (var affliction in afflictions)
|
|
{
|
|
if (ShouldDisplayAfflictionOnLimb(affliction, limbHealth,selfHealth,limbHealths))
|
|
{
|
|
if (affliction.Key.Prefab.IsBuff)
|
|
{
|
|
if (positivecount >= 4) { undrawncount++; continue; }
|
|
positivecount++;
|
|
}
|
|
else
|
|
{
|
|
if (negativecount >= 4) { undrawncount++; continue; }
|
|
negativecount++;
|
|
}
|
|
|
|
afflictionsDisplayedOnLimb.Add(affliction.Key);
|
|
|
|
}
|
|
}
|
|
|
|
if (!afflictionsDisplayedOnLimb.Any()) { i++; continue; }
|
|
if (limbHealth.IndicatorSprite == null) { continue; }
|
|
|
|
float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height);
|
|
|
|
Rectangle highlightArea = GetLimbHighlightArea(limbHealth, drawArea,selfHealth);
|
|
|
|
float iconScale = 0.25f * scale;
|
|
|
|
int drawnPositve = 0;
|
|
int drawnNegative = 0;
|
|
foreach (Affliction affliction in afflictionsDisplayedOnLimb)
|
|
{
|
|
bool isBuff = affliction.Prefab.IsBuff;
|
|
Vector2 iconPos = highlightArea.Center.ToVector2();
|
|
if (negativecount > 0 && positivecount > 0)
|
|
iconPos += new Vector2(10 * (isBuff ? 1 : -1), 0);
|
|
|
|
float spacing = MathHelper.Clamp(40 / (Math.Max(negativecount, positivecount) / 1.5f), 10, 40);
|
|
|
|
iconPos += new Vector2(0, spacing * ((isBuff ? drawnPositve : drawnNegative) - 0.5f * (Math.Max(negativecount, positivecount) - 1)));
|
|
|
|
DrawLimbAfflictionIcon(spriteBatch, affliction, iconScale, ref iconPos);
|
|
|
|
if (isBuff) drawnPositve++;
|
|
else drawnNegative++;
|
|
}
|
|
|
|
// draw the "+x" if theres too many afflictions
|
|
if (undrawncount > 0)
|
|
{
|
|
string additionalAfflictionCount = $"+{undrawncount}";
|
|
Vector2 displace = GUIStyle.SubHeadingFont.MeasureString(additionalAfflictionCount);
|
|
|
|
Vector2 iconPos = highlightArea.Center.ToVector2();
|
|
if (negativecount > 0 && positivecount > 0)
|
|
iconPos += new Vector2(10, 0);
|
|
|
|
GUIStyle.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X * 1.1f, -displace.Y * 0.45f), Color.Black * 0.75f);
|
|
GUIStyle.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X, -displace.Y * 0.5f), Color.White);
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
if (selectedLimbIndex > -1 && (afflictionIconContainer.Content.CountChildren > 0 || afflictionIconContainer2.Content.CountChildren > 0))
|
|
{
|
|
CharacterHealth.LimbHealth limbHealth = limbHealths[selectedLimbIndex];
|
|
if (limbHealth?.IndicatorSprite != null)
|
|
{
|
|
var target = afflictionIconContainer.Content.CountChildren > 0 ? afflictionIconContainer : afflictionIconContainer2;
|
|
Rectangle selectedLimbArea = GetLimbHighlightArea(limbHealth, drawArea,selfHealth);
|
|
GUI.DrawLine(spriteBatch,
|
|
new Vector2(target.Rect.X, target.Rect.Y),
|
|
selectedLimbArea.Center.ToVector2(),
|
|
Color.LightGray * 0.5f, width: 4);
|
|
}
|
|
}
|
|
|
|
if (NeurotraumaEnabled)
|
|
UpdateGraph();
|
|
|
|
void DrawLimbAfflictionIcon(SpriteBatch spriteBatch, Affliction affliction, float iconScale, ref Vector2 iconPos)
|
|
{
|
|
if (!affliction.ShouldShowIcon(selfHealth.Character) || affliction.Prefab.Icon == null) { return; }
|
|
Vector2 iconSize = affliction.Prefab.Icon.size * iconScale;
|
|
|
|
float showIconThreshold = Character.Controlled?.CharacterHealth == selfHealth ? affliction.Prefab.ShowIconThreshold : affliction.Prefab.ShowIconToOthersThreshold;
|
|
|
|
//afflictions that have a strength of less than 10 are faded out slightly
|
|
float alpha = MathHelper.Lerp(0.3f, 1.0f,
|
|
(affliction.Strength - showIconThreshold) / Math.Min(affliction.Prefab.MaxStrength - showIconThreshold, 10.0f));
|
|
|
|
affliction.Prefab.Icon.Draw(spriteBatch, iconPos - iconSize / 2.0f, CharacterHealth.GetAfflictionIconColor(affliction) * alpha, 0, iconScale);
|
|
iconPos += new Vector2(10.0f, 20.0f) * iconScale;
|
|
}
|
|
}
|
|
|
|
bool ShouldDisplayAfflictionOnLimb(KeyValuePair<Affliction, CharacterHealth.LimbHealth> kvp, CharacterHealth.LimbHealth limbHealth, CharacterHealth selfHealth, List<CharacterHealth.LimbHealth> limbHealths)
|
|
{
|
|
if (!kvp.Key.ShouldShowIcon(selfHealth.Character)) { return false; }
|
|
if (kvp.Value == limbHealth)
|
|
{
|
|
return true;
|
|
}
|
|
else if (kvp.Value == null)
|
|
{
|
|
Limb indicatorLimb = selfHealth.Character.AnimController.GetLimb(kvp.Key.Prefab.IndicatorLimb);
|
|
return indicatorLimb != null && indicatorLimb.HealthIndex == limbHealths.IndexOf(limbHealth);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// CreateAfflictionInfos override
|
|
// makes it so the affliction descriptions are next to each other if buffs are present
|
|
// also makes them smaller so theres room for more of them
|
|
const int displayedAfflictionCountMax = 8;
|
|
GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_CreateAfflictionInfos",
|
|
typeof(CharacterHealth).GetMethod("CreateAfflictionInfos", BindingFlags.Instance | BindingFlags.NonPublic),
|
|
(object self, Dictionary<string, object> args) => {
|
|
CreateAfflictionInfos(self, args);
|
|
return true;
|
|
}, LuaCsHook.HookMethodType.Before, this);
|
|
void CreateAfflictionInfos(object self, Dictionary<string, object> args)
|
|
{
|
|
ForceCustomized((CharacterHealth)self);
|
|
|
|
#region Reflection crap
|
|
// get arguments
|
|
IEnumerable<Affliction> afflictions = (IEnumerable<Affliction>)(args["afflictions"]);
|
|
// get members
|
|
GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
GUIListBox afflictionIconContainer2 = (GUIListBox)(afflictionIconContainer.Parent.GetChildByUserData("afflictionIconContainer2"));
|
|
List<(Affliction affliction, float strength)> displayedAfflictions = (List<(Affliction affliction, float strength)>)(typeof(CharacterHealth).GetField("displayedAfflictions", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
#endregion
|
|
// override begins
|
|
afflictionIconContainer.ClearChildren();
|
|
afflictionIconContainer2.ClearChildren();
|
|
displayedAfflictions.Clear();
|
|
|
|
afflictions = CharacterHealth.SortAfflictionsBySeverity(afflictions, false);
|
|
Affliction mostSevereAffliction = afflictions.FirstOrDefault();
|
|
GUIButton buttonToSelect = null;
|
|
|
|
foreach (Affliction affliction in afflictions)
|
|
{
|
|
displayedAfflictions.Add((affliction, affliction.Strength));
|
|
|
|
bool isBuff = affliction.Prefab.IsBuff;
|
|
var newParent = (isBuff ? afflictionIconContainer2 : afflictionIconContainer);
|
|
|
|
var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 1f / displayedAfflictionCountMax),newParent.Content.RectTransform), style: "ListBoxElement")
|
|
{
|
|
UserData = affliction,
|
|
OnClicked = SelectAffliction
|
|
};
|
|
|
|
new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: "GUIFrameListBox") { CanBeFocused = false };
|
|
|
|
// houses the progress bar
|
|
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.75f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft)
|
|
{
|
|
Stretch = true,
|
|
CanBeFocused = false,
|
|
IsHorizontal = true
|
|
};
|
|
// spacing
|
|
new GUIFrame(new RectTransform(new Vector2(0.1f, 1f), content.RectTransform), style: "GUIFrameListBox") { CanBeFocused = false };
|
|
|
|
// houses the affliction icon and text
|
|
var content2 = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft)
|
|
{
|
|
Stretch = true,
|
|
CanBeFocused = false,
|
|
IsHorizontal = true
|
|
};
|
|
|
|
var progressbarBg = new GUIProgressBar(new RectTransform(new Vector2(0.5f, 1), content.RectTransform), 0.0f, GUIStyle.Green, style: "GUIAfflictionBar")
|
|
{
|
|
UserData = "afflictionstrengthprediction",
|
|
CanBeFocused = false,
|
|
IsHorizontal = true
|
|
};
|
|
var afflictionStrengthBar = new GUIProgressBar(new RectTransform(Vector2.One, progressbarBg.RectTransform), 0.0f, Color.Transparent, showFrame: false, style: "GUIAfflictionBar")
|
|
{
|
|
UserData = "afflictionstrength",
|
|
CanBeFocused = false,
|
|
IsHorizontal = true
|
|
};
|
|
afflictionStrengthBar.BarSize = affliction.Strength / affliction.Prefab.MaxStrength;
|
|
|
|
//spacing
|
|
//new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), style: null) { CanBeFocused = false };
|
|
|
|
if (affliction == mostSevereAffliction)
|
|
{
|
|
buttonToSelect = frame;
|
|
}
|
|
|
|
var afflictionIcon = new GUIImage(new RectTransform(Vector2.One * 1f, content2.RectTransform, anchor: Anchor.CenterLeft, pivot: Pivot.CenterLeft, scaleBasis: ScaleBasis.BothHeight), affliction.Prefab.Icon, scaleToFit: true)
|
|
{
|
|
Color = CharacterHealth.GetAfflictionIconColor(affliction),
|
|
CanBeFocused = false
|
|
};
|
|
afflictionIcon.PressedColor = afflictionIcon.Color;
|
|
afflictionIcon.HoverColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.6f);
|
|
afflictionIcon.SelectedColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.5f);
|
|
|
|
var nameText = new GUITextBlock(new RectTransform(new Vector2(1.1f, 0.0f), content2.RectTransform),
|
|
$"{affliction.Prefab.Name}\n({Math.Round(affliction.Strength / affliction.Prefab.MaxStrength * 100)}% | {Math.Round(affliction.Strength)}/{Math.Round(affliction.Prefab.MaxStrength)})", font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft)
|
|
{
|
|
UserData = "afflictionname",
|
|
CanBeFocused = false,
|
|
OutlineColor = Color.Black,
|
|
Shadow = true
|
|
};
|
|
nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width);
|
|
nameText.RectTransform.MinSize = new Point(0, (int)(nameText.TextSize.Y));
|
|
nameText.RectTransform.SizeChanged += () =>
|
|
{
|
|
nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width);
|
|
};
|
|
|
|
content.Recalculate();
|
|
content2.Recalculate();
|
|
}
|
|
|
|
buttonToSelect?.OnClicked(buttonToSelect, buttonToSelect.UserData);
|
|
afflictionIconContainer.RecalculateChildren();
|
|
afflictionIconContainer.ForceLayoutRecalculation();
|
|
afflictionIconContainer2.RecalculateChildren();
|
|
afflictionIconContainer2.ForceLayoutRecalculation();
|
|
|
|
bool SelectAffliction(GUIButton button, object userData)
|
|
{
|
|
bool selected = button.Selected;
|
|
foreach (var child in afflictionIconContainer.Content.Children)
|
|
{
|
|
GUIButton btn = child.GetChild<GUIButton>();
|
|
if (btn != null)
|
|
{
|
|
btn.Selected = btn == button && !selected;
|
|
}
|
|
}
|
|
foreach (var child in afflictionIconContainer2.Content.Children)
|
|
{
|
|
GUIButton btn = child.GetChild<GUIButton>();
|
|
if (btn != null)
|
|
{
|
|
btn.Selected = btn == button && !selected;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// UpdateAfflictionContainer override
|
|
// got some custom double list crap going on here
|
|
GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_UpdateAfflictionContainer",
|
|
typeof(CharacterHealth).GetMethod("UpdateAfflictionContainer", BindingFlags.Instance | BindingFlags.NonPublic),
|
|
(object self, Dictionary<string, object> args) => {
|
|
UpdateAfflictionContainer((CharacterHealth.LimbHealth)(args["selectedLimb"]), (CharacterHealth)self);
|
|
return true;
|
|
}, LuaCsHook.HookMethodType.Before, this);
|
|
void UpdateAfflictionContainer(CharacterHealth.LimbHealth selectedLimb,CharacterHealth selfHealth)
|
|
{
|
|
ForceCustomized(selfHealth);
|
|
GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
|
|
GUIListBox afflictionIconContainer2 = (GUIListBox)(afflictionIconContainer.Parent.GetChildByUserData("afflictionIconContainer2"));
|
|
List<(Affliction affliction, float strength)> displayedAfflictions = (List<(Affliction affliction, float strength)>)(typeof(CharacterHealth).GetField("displayedAfflictions", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
|
|
Dictionary<Affliction, CharacterHealth.LimbHealth> afflictions = (Dictionary<Affliction, CharacterHealth.LimbHealth>)(typeof(CharacterHealth).GetField("afflictions", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
|
|
CharacterHealth.LimbHealth currentDisplayedLimb = (CharacterHealth.LimbHealth)(typeof(CharacterHealth).GetField("currentDisplayedLimb", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
|
|
List<CharacterHealth.LimbHealth> limbHealths = (List<CharacterHealth.LimbHealth>)(typeof(CharacterHealth).GetField("limbHealths", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
|
|
|
|
if (selectedLimb == null)
|
|
{
|
|
afflictionIconContainer.Content.ClearChildren();
|
|
afflictionIconContainer2.Content.ClearChildren();
|
|
return;
|
|
}
|
|
|
|
if (afflictionsDirty() || selectedLimb != currentDisplayedLimb)
|
|
{
|
|
var currentAfflictions = afflictions.Where(a => ShouldDisplayAfflictionOnLimb(a, selectedLimb,selfHealth,limbHealths)).Select(a => a.Key);
|
|
Dictionary<string, object> args = new Dictionary<string, object>();
|
|
args.Add("afflictions", currentAfflictions);
|
|
CreateAfflictionInfos(selfHealth, args);
|
|
CreateRecommendedTreatments();
|
|
}
|
|
//update recommended treatments if the strength of some displayed affliction has changed by > 1
|
|
else if (displayedAfflictions.Any(d => Math.Abs(d.strength - d.affliction.Strength) > 1.0f))
|
|
{
|
|
CreateRecommendedTreatments();
|
|
}
|
|
|
|
bool afflictionsDirty()
|
|
{
|
|
//not displaying one of the current afflictions -> dirty
|
|
foreach (KeyValuePair<Affliction, CharacterHealth.LimbHealth> kvp in afflictions)
|
|
{
|
|
if (!ShouldDisplayAfflictionOnLimb(kvp, selectedLimb,selfHealth,limbHealths)) { continue; }
|
|
if (!displayedAfflictions.Any(d => d.affliction == kvp.Key)) { return true; }
|
|
}
|
|
//displaying an affliction we no longer have -> dirty
|
|
foreach ((Affliction affliction, float strength) in displayedAfflictions)
|
|
{
|
|
if (!afflictions.Any(a => a.Key == affliction)) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CreateRecommendedTreatments()
|
|
{
|
|
typeof(CharacterHealth).GetMethod("CreateRecommendedTreatments", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(selfHealth, null);
|
|
}
|
|
}
|
|
|
|
// UpdateAfflictionInfos override
|
|
// makes it so the affliction progress bar is nicely colored
|
|
GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_UpdateAfflictionInfos",
|
|
typeof(CharacterHealth).GetMethod("UpdateAfflictionInfos", BindingFlags.Instance | BindingFlags.NonPublic),
|
|
(object self, Dictionary<string, object> args) => {
|
|
try
|
|
{
|
|
#region Reflection crap
|
|
// get arguments
|
|
IEnumerable<Affliction> afflictions = (IEnumerable<Affliction>)(args["afflictions"]);
|
|
// get members
|
|
CharacterHealth selfHealth = (CharacterHealth)self;
|
|
ForceCustomized(selfHealth);
|
|
GUIListBox afflictionIconContainer = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionIconList", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
GUIListBox afflictionIconContainer2 = (GUIListBox)(afflictionIconContainer.Parent.GetChildByUserData("afflictionIconContainer2"));
|
|
GUIListBox afflictionTooltip = (GUIListBox)(typeof(CharacterHealth).GetField("afflictionTooltip", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(self));
|
|
//LocalizedString[] strengthTexts = (LocalizedString[])(typeof(CharacterHealth).GetField("strengthTexts", BindingFlags.NonPublic | BindingFlags.Static).GetValue(self));
|
|
#endregion
|
|
// override begins
|
|
|
|
var potentialTreatment = Inventory.DraggingItems.FirstOrDefault();
|
|
if (potentialTreatment == null && GUI.MouseOn?.UserData is ItemPrefab itemPrefab)
|
|
{
|
|
potentialTreatment = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true);
|
|
}
|
|
potentialTreatment ??= Inventory.SelectedSlot?.Item;
|
|
|
|
foreach (Affliction affliction in afflictions)
|
|
{
|
|
float afflictionVitalityDecrease = affliction.GetVitalityDecrease(selfHealth);
|
|
Color afflictionEffectColor = Color.Lerp(CharacterHealth.GetAfflictionIconColor(affliction), Color.Black, 0.5f);
|
|
|
|
bool isBuff = affliction.Prefab.IsBuff;
|
|
|
|
var child = (isBuff ? afflictionIconContainer2 : afflictionIconContainer).Content.FindChild(affliction);
|
|
|
|
var afflictionName = child.FindChild("afflictionname", true) as GUITextBlock;
|
|
afflictionName.Text = $"{affliction.Prefab.Name}\n({Math.Round(affliction.Strength / affliction.Prefab.MaxStrength * 100)}% | {Math.Round(affliction.Strength)}/{Math.Round(affliction.Prefab.MaxStrength)})";
|
|
|
|
var afflictionStrengthPredictionBar = child.FindChild("afflictionstrengthprediction", true) as GUIProgressBar;
|
|
afflictionStrengthPredictionBar.BarSize = 0.0f;
|
|
var afflictionStrengthBar = afflictionStrengthPredictionBar.GetChildByUserData("afflictionstrength") as GUIProgressBar;
|
|
afflictionStrengthBar.BarSize = MathHelper.Lerp(afflictionStrengthBar.BarSize, affliction.Strength / affliction.Prefab.MaxStrength, 0.1f);
|
|
afflictionStrengthBar.Color = afflictionEffectColor;
|
|
|
|
float afflictionStrengthPrediction = GetAfflictionStrengthPrediction(potentialTreatment, affliction);
|
|
if (!MathUtils.NearlyEqual(afflictionStrengthPrediction, affliction.Strength))
|
|
{
|
|
float t = (float)Math.Max(0.5f, (Math.Sin(Timing.TotalTime * 5) + 1.0f) / 2.0f);
|
|
if (afflictionStrengthPrediction < affliction.Strength)
|
|
{
|
|
afflictionStrengthBar.Color = afflictionEffectColor;
|
|
afflictionStrengthPredictionBar.Color = GUIStyle.Blue * t;
|
|
afflictionStrengthPredictionBar.BarSize = afflictionStrengthBar.BarSize;
|
|
afflictionStrengthBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength;
|
|
}
|
|
else
|
|
{
|
|
afflictionStrengthPredictionBar.Color = Color.Red * t;
|
|
afflictionStrengthPredictionBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength;
|
|
}
|
|
}
|
|
|
|
if (afflictionTooltip != null && afflictionTooltip.UserData == affliction)
|
|
{
|
|
UpdateAfflictionInfo(afflictionTooltip.Content, affliction);
|
|
}
|
|
}
|
|
|
|
void UpdateAfflictionInfo(GUIComponent parent, Affliction affliction)
|
|
{
|
|
var labelContainer = parent.GetChildByUserData("label");
|
|
|
|
var strengthText = labelContainer.GetChildByUserData("strength") as GUITextBlock;
|
|
|
|
strengthText.Text = affliction.GetStrengthText();
|
|
|
|
strengthText.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red,
|
|
affliction.Strength / affliction.Prefab.MaxStrength);
|
|
|
|
var vitalityText = labelContainer.GetChildByUserData("vitality") as GUITextBlock;
|
|
int vitalityDecrease = (int)affliction.GetVitalityDecrease(selfHealth);
|
|
if (vitalityDecrease == 0)
|
|
{
|
|
vitalityText.Visible = false;
|
|
}
|
|
else
|
|
{
|
|
vitalityText.Visible = true;
|
|
vitalityText.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease;
|
|
vitalityText.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green :
|
|
Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength);
|
|
}
|
|
}
|
|
|
|
float GetAfflictionStrengthPrediction(Item item, Affliction affliction)
|
|
{
|
|
float strength = affliction.Strength;
|
|
if (item == null) { return strength; }
|
|
|
|
foreach (ItemComponent ic in item.Components)
|
|
{
|
|
if (ic.statusEffectLists == null) { continue; }
|
|
if (!ic.statusEffectLists.TryGetValue(ActionType.OnUse, out List<StatusEffect> statusEffects)) { continue; }
|
|
foreach (StatusEffect effect in statusEffects)
|
|
{
|
|
foreach (var reduceAffliction in effect.ReduceAffliction)
|
|
{
|
|
if (reduceAffliction.AfflictionIdentifier != affliction.Identifier && reduceAffliction.AfflictionIdentifier != affliction.Prefab.AfflictionType) { continue; }
|
|
strength -= reduceAffliction.ReduceAmount * (effect.Duration > 0 ? effect.Duration : 1.0f);
|
|
}
|
|
foreach (var addAffliction in effect.Afflictions)
|
|
{
|
|
if (addAffliction.Prefab != affliction.Prefab) { continue; }
|
|
strength += addAffliction.Strength * (effect.Duration > 0 ? effect.Duration : 1.0f);
|
|
}
|
|
}
|
|
}
|
|
return strength;
|
|
}
|
|
|
|
healthGUIRefreshTimer--;
|
|
if (healthGUIRefreshTimer <= 0)
|
|
{
|
|
healthGUIRefreshTimer = 60;
|
|
afflictionIconContainer.Content.RectTransform.SortChildren((r1, r2) =>
|
|
{
|
|
var first = r1.GUIComponent.UserData as Affliction;
|
|
var second = r2.GUIComponent.UserData as Affliction;
|
|
return Math.Sign(second.Strength / second.Prefab.MaxStrength - first.Strength / first.Prefab.MaxStrength);
|
|
});
|
|
afflictionIconContainer2.Content.RectTransform.SortChildren((r1, r2) =>
|
|
{
|
|
var first = r1.GUIComponent.UserData as Affliction;
|
|
var second = r2.GUIComponent.UserData as Affliction;
|
|
return Math.Sign(second.Strength / second.Prefab.MaxStrength - first.Strength / first.Prefab.MaxStrength);
|
|
});
|
|
}
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LuaCsSetup.PrintCsMessage("shit. " + e);
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
}, LuaCsHook.HookMethodType.Before, this);
|
|
|
|
// UpdateLimbIndicators override
|
|
// makes it so i can move the funny limb man
|
|
GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_UpdateLimbIndicators",
|
|
typeof(CharacterHealth).GetMethod("UpdateLimbIndicators", BindingFlags.Instance | BindingFlags.NonPublic),
|
|
(object self, Dictionary<string, object> args) => {
|
|
// get arguments
|
|
float deltaTime = (float)(args["deltaTime"]);
|
|
Rectangle drawArea = (Rectangle)(args["drawArea"]);
|
|
|
|
CharacterHealth selfHealth = (CharacterHealth)self;
|
|
|
|
UpdateLimbIndicators(deltaTime, drawArea, selfHealth);
|
|
|
|
return true;
|
|
}, LuaCsHook.HookMethodType.Before, this);
|
|
void UpdateLimbIndicators(float deltaTime, Rectangle drawArea, CharacterHealth selfHealth)
|
|
{
|
|
// get members
|
|
float limbIndicatorOverlayAnimState = (float)(typeof(CharacterHealth).GetField("limbIndicatorOverlayAnimState", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
|
|
int highlightedLimbIndex = (int)(typeof(CharacterHealth).GetField("highlightedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
|
|
int selectedLimbIndex = (int)(typeof(CharacterHealth).GetField("selectedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
|
|
List<CharacterHealth.LimbHealth> limbHealths = (List<CharacterHealth.LimbHealth>)(typeof(CharacterHealth).GetField("limbHealths", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfHealth));
|
|
// override begins
|
|
|
|
if (!GameMain.Instance.Paused)
|
|
{
|
|
limbIndicatorOverlayAnimState += deltaTime * 8.0f;
|
|
// reflection apply
|
|
typeof(CharacterHealth).GetField("limbIndicatorOverlayAnimState", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(selfHealth, limbIndicatorOverlayAnimState);
|
|
}
|
|
|
|
highlightedLimbIndex = -1;
|
|
int i = 0;
|
|
foreach (CharacterHealth.LimbHealth limbHealth in limbHealths)
|
|
{
|
|
if (limbHealth.IndicatorSprite == null) { continue; }
|
|
|
|
float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height);
|
|
|
|
Rectangle highlightArea = GetLimbHighlightArea(limbHealth, drawArea, selfHealth);
|
|
|
|
if (highlightArea.Contains(PlayerInput.MousePosition))
|
|
{
|
|
highlightedLimbIndex = i;
|
|
}
|
|
i++;
|
|
}
|
|
// reflection apply
|
|
typeof(CharacterHealth).GetField("highlightedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(selfHealth, highlightedLimbIndex);
|
|
|
|
if (PlayerInput.PrimaryMouseButtonClicked() && highlightedLimbIndex > -1)
|
|
{
|
|
selectedLimbIndex = highlightedLimbIndex;
|
|
// reflection apply
|
|
typeof(CharacterHealth).GetField("selectedLimbIndex", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(selfHealth, selectedLimbIndex);
|
|
}
|
|
}
|
|
|
|
// SortAfflictionsBySeverity override
|
|
// makes it so the afflictions dont spazz around
|
|
GameMain.LuaCs.Hook.HookMethod("BetterHealthUIMod_SortAfflictionsBySeverity",
|
|
typeof(CharacterHealth).GetMethod("SortAfflictionsBySeverity", BindingFlags.Static | BindingFlags.Public),
|
|
(object self, Dictionary<string, object> args) => {
|
|
#region Reflection crap
|
|
// get arguments
|
|
IEnumerable<Affliction> afflictions = (IEnumerable<Affliction>)(args["afflictions"]);
|
|
bool excludeBuffs = (bool)(args["excludeBuffs"]);
|
|
#endregion
|
|
// override begins
|
|
return afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.Strength / a.Prefab.MaxStrength);
|
|
}, LuaCsHook.HookMethodType.Before, this);
|
|
|
|
float GetTotalDamage(CharacterHealth.LimbHealth limbHealth, Dictionary<Affliction, CharacterHealth.LimbHealth> afflictions,CharacterHealth self)
|
|
{
|
|
float totalDamage = 0.0f;
|
|
foreach (KeyValuePair<Affliction, CharacterHealth.LimbHealth> kvp in afflictions)
|
|
{
|
|
if (kvp.Value != limbHealth) { continue; }
|
|
var affliction = kvp.Key;
|
|
totalDamage += affliction.GetVitalityDecrease(self);
|
|
}
|
|
return totalDamage;
|
|
}
|
|
|
|
Rectangle GetLimbHighlightArea(CharacterHealth.LimbHealth limbHealth, Rectangle drawArea, CharacterHealth selfHealth)
|
|
{
|
|
float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height);
|
|
return new Rectangle(
|
|
(int)(drawArea.Center.X + limbGuyOffset.X - (limbHealth.IndicatorSprite.SourceRect.Width / 2 - limbHealth.HighlightArea.X) * scale),
|
|
(int)(drawArea.Center.Y + limbGuyOffset.Y - (limbHealth.IndicatorSprite.SourceRect.Height / 2 - limbHealth.HighlightArea.Y) * scale),
|
|
(int)(limbHealth.HighlightArea.Width * scale),
|
|
(int)(limbHealth.HighlightArea.Height * scale));
|
|
}
|
|
|
|
Color AverageColor(List<object[]> colors)
|
|
{
|
|
float num = 0;
|
|
float r = 0;
|
|
float g = 0;
|
|
float b = 0;
|
|
|
|
foreach (object[] colarr in colors)
|
|
{
|
|
Color col = (Color)colarr[0];
|
|
float strength = (float)colarr[1];
|
|
r += col.R * col.R * strength;
|
|
g += col.G * col.G * strength;
|
|
b += col.B * col.B * strength;
|
|
num += strength;
|
|
}
|
|
|
|
return new Color((float)Math.Sqrt(r / num)/255f, (float)Math.Sqrt(g / num) / 255f, (float)Math.Sqrt(b / num) / 255f);
|
|
}
|
|
|
|
LimbType NormalizeLimbType(LimbType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case LimbType.Waist: return LimbType.Torso;
|
|
case LimbType.LeftHand:
|
|
case LimbType.LeftForearm:
|
|
return LimbType.LeftArm;
|
|
case LimbType.RightHand:
|
|
case LimbType.RightForearm:
|
|
return LimbType.RightArm;
|
|
case LimbType.LeftFoot:
|
|
case LimbType.LeftThigh:
|
|
return LimbType.LeftLeg;
|
|
case LimbType.RightFoot:
|
|
case LimbType.RightThigh:
|
|
return LimbType.RightLeg;
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
LimbType LimbHealthToLimbType(CharacterHealth.LimbHealth limbHealth, List<CharacterHealth.LimbHealth> limbHealths, Limb[] Limbs)
|
|
{
|
|
int healthIndex = limbHealths.IndexOf(limbHealth);
|
|
Limb l = Limbs.Find(l => l.HealthIndex == healthIndex);
|
|
if (l == null) return LimbType.None;
|
|
return l.type;
|
|
}
|
|
|
|
}
|
|
|
|
// update heartrate monitor (Neurotrauma)
|
|
const int GraphSize = 256;
|
|
const float updateGraphInterval = 1 / 60f; // update at 60 fps
|
|
const float NormalHeartrate = 60;
|
|
const float MaxTachycardiaHeartrate = 180;
|
|
const float MaxFibrillationHeartrate = 300;
|
|
|
|
private static float graphTimer = 0;
|
|
private static float[] heartGraph = new float[GraphSize];
|
|
private static int heartGraphProgress = 0;
|
|
private static float timeSinceBeat = 0;
|
|
private static CCurve ecgCurve = null;
|
|
private static CCurve ecgCurveFib = null;
|
|
private static SoundChannel flatlineSoundChannel = null;
|
|
|
|
private static void UpdateGraph()
|
|
{
|
|
const float deltaTime = 1 / 60f;
|
|
|
|
graphTimer += deltaTime;
|
|
timeSinceBeat += deltaTime;
|
|
|
|
if (graphTimer >= updateGraphInterval)
|
|
{
|
|
Character character = CharacterHealth.OpenHealthWindow?.Character;
|
|
UpdateHeartrateGraphData(heartGraph, GetHeartbeatAmplitude(character)+1);
|
|
|
|
graphTimer = 0.0f;
|
|
}
|
|
}
|
|
private static float GetHeartbeatAmplitude(Character character)
|
|
{
|
|
if (character == null) return 0.0f;
|
|
|
|
// Проверяем, отключен ли звук пульса в конфигурации или частота равна 0
|
|
if (BetterHealthUIMod.DisableHeartbeatSound || character.IsDead)
|
|
{
|
|
// Останавливаем плоский звук, если он включен
|
|
if (flatlineSoundChannel != null)
|
|
{
|
|
flatlineSoundChannel.Dispose();
|
|
flatlineSoundChannel = null;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
(float rate, float stability) heartrate = GetHeartrate(character);
|
|
float chaos = MathHelper.Lerp(1, rnd.Next(0, 1000) / 1000f, Math.Min(1, (1 - heartrate.stability) * 2));
|
|
|
|
float timePerBeat = heartrate.rate > 0
|
|
? 1 / (heartrate.rate / 60 * (1 + (1 - heartrate.stability) * 2 * (rnd.Next(0, 1000) / 1000f)))
|
|
: float.PositiveInfinity;
|
|
|
|
// Плоская линия, если частота сердцебиения равна 0
|
|
if (heartrate.rate <= 0)
|
|
{
|
|
if (flatlineSoundChannel == null)
|
|
{
|
|
flatlineSoundChannel = SoundPlayer.PlaySound("flatline2", 0.06f);
|
|
if (flatlineSoundChannel != null) flatlineSoundChannel.Looping = true;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Остановка звука плоской линии, если частота нормализовалась
|
|
if (flatlineSoundChannel != null)
|
|
{
|
|
flatlineSoundChannel.Dispose();
|
|
flatlineSoundChannel = null;
|
|
}
|
|
|
|
// Звук "бип" при каждом ударе сердца
|
|
if (timeSinceBeat > timePerBeat)
|
|
{
|
|
timeSinceBeat -= timePerBeat;
|
|
if (heartrate.rate > NormalHeartrate + 10 && rnd.Next(0, 1000) / 1000f < heartrate.stability)
|
|
{
|
|
SoundPlayer.PlaySound("ecg1", 0.1f * Math.Min((heartrate.rate - NormalHeartrate) / 80, 1));
|
|
}
|
|
}
|
|
|
|
return MathHelper.Lerp(ecgCurve.Evaluate(timeSinceBeat * 1.5f) * chaos, ecgCurveFib.Evaluate(timeSinceBeat * 1.5f), 1 - heartrate.stability);
|
|
}
|
|
|
|
private static (float rate,float stability) GetHeartrate(Character character)
|
|
{
|
|
if (character == null || character.CharacterHealth==null || character.IsDead) return (0,0);
|
|
|
|
float rate = NormalHeartrate;
|
|
float stability = 1;
|
|
|
|
Affliction cardiacarrest = character.CharacterHealth.GetAffliction("cardiacarrest");
|
|
|
|
// return 0 rate and stability if in cardiac arrest
|
|
if (cardiacarrest != null && cardiacarrest.Strength >= 0.5f) return (0,0);
|
|
|
|
Affliction tachycardia = character.CharacterHealth.GetAffliction("tachycardia");
|
|
Affliction fibrillation = character.CharacterHealth.GetAffliction("fibrillation");
|
|
|
|
if (fibrillation!=null)
|
|
{
|
|
rate = MathHelper.Lerp(MaxTachycardiaHeartrate, MaxFibrillationHeartrate, fibrillation.Strength/100 * (0.25f+(rnd.Next(0, 1000) / 1000f)));
|
|
stability = 1 - fibrillation.Strength / 100;
|
|
}
|
|
else if (tachycardia != null)
|
|
{
|
|
rate = MathHelper.Lerp(NormalHeartrate, MaxTachycardiaHeartrate, tachycardia.Strength / 100);
|
|
}
|
|
|
|
return (rate,stability);
|
|
}
|
|
|
|
private static void UpdateHeartrateGraphData(IList<float> graph, float newValue)
|
|
{
|
|
graph[heartGraphProgress] = newValue*0.8f;
|
|
heartGraphProgress = (heartGraphProgress + 1) % graph.Count;
|
|
}
|
|
}
|
|
|
|
// i hate this
|
|
// trying to use the already existing curve class throws an error that its in two assemblies
|
|
// i dont know how to fix it so here comes the duplicate classes!
|
|
|
|
public enum CCurveLoopType
|
|
{
|
|
Constant,
|
|
Cycle,
|
|
CycleOffset,
|
|
Oscillate,
|
|
Linear
|
|
}
|
|
|
|
public enum CCurveContinuity
|
|
{
|
|
Smooth,
|
|
Step
|
|
}
|
|
|
|
public enum CCurveTangent
|
|
{
|
|
Flat,
|
|
Linear,
|
|
Smooth
|
|
}
|
|
|
|
public class CCurve
|
|
{
|
|
#region Private Fields
|
|
|
|
private CCurveKeyCollection keys;
|
|
private CCurveLoopType postLoop;
|
|
private CCurveLoopType preLoop;
|
|
|
|
#endregion Private Fields
|
|
|
|
#region Public Properties
|
|
|
|
public bool IsConstant
|
|
{
|
|
get { return keys.Count <= 1; }
|
|
}
|
|
|
|
public CCurveKeyCollection Keys
|
|
{
|
|
get { return keys; }
|
|
}
|
|
|
|
public CCurveLoopType PostLoop
|
|
{
|
|
get { return postLoop; }
|
|
set { postLoop = value; }
|
|
}
|
|
|
|
public CCurveLoopType PreLoop
|
|
{
|
|
get { return preLoop; }
|
|
set { preLoop = value; }
|
|
}
|
|
|
|
#endregion Public Properties
|
|
|
|
#region Public Constructors
|
|
|
|
public CCurve()
|
|
{
|
|
keys = new CCurveKeyCollection();
|
|
}
|
|
|
|
#endregion Public Constructors
|
|
|
|
#region Public Methods
|
|
|
|
public CCurve Clone()
|
|
{
|
|
CCurve curve = new CCurve();
|
|
|
|
curve.keys = keys.Clone();
|
|
curve.preLoop = preLoop;
|
|
curve.postLoop = postLoop;
|
|
|
|
return curve;
|
|
}
|
|
|
|
public float Evaluate(float position)
|
|
{
|
|
CCurveKey first = keys[0];
|
|
CCurveKey last = keys[keys.Count - 1];
|
|
|
|
if (position < first.Position)
|
|
{
|
|
switch (PreLoop)
|
|
{
|
|
case CCurveLoopType.Constant:
|
|
//constant
|
|
return first.Value;
|
|
|
|
case CCurveLoopType.Linear:
|
|
// linear y = a*x +b with a tangeant of last point
|
|
return first.Value - first.TangentIn * (first.Position - position);
|
|
|
|
case CCurveLoopType.Cycle:
|
|
//start -> end / start -> end
|
|
int cycle = GetNumberOfCycle(position);
|
|
float virtualPos = position - (cycle * (last.Position - first.Position));
|
|
return GetCurvePosition(virtualPos);
|
|
|
|
case CCurveLoopType.CycleOffset:
|
|
//make the curve continue (with no step) so must up the curve each cycle of delta(value)
|
|
cycle = GetNumberOfCycle(position);
|
|
virtualPos = position - (cycle * (last.Position - first.Position));
|
|
return (GetCurvePosition(virtualPos) + cycle * (last.Value - first.Value));
|
|
|
|
case CCurveLoopType.Oscillate:
|
|
//go back on curve from end and target start
|
|
// start-> end / end -> start
|
|
cycle = GetNumberOfCycle(position);
|
|
if (0 == cycle % 2f) //if pair
|
|
virtualPos = position - (cycle * (last.Position - first.Position));
|
|
else
|
|
virtualPos = last.Position - position + first.Position +
|
|
(cycle * (last.Position - first.Position));
|
|
return GetCurvePosition(virtualPos);
|
|
}
|
|
}
|
|
else if (position > last.Position)
|
|
{
|
|
int cycle;
|
|
switch (PostLoop)
|
|
{
|
|
case CCurveLoopType.Constant:
|
|
//constant
|
|
return last.Value;
|
|
|
|
case CCurveLoopType.Linear:
|
|
// linear y = a*x +b with a tangeant of last point
|
|
return last.Value + first.TangentOut * (position - last.Position);
|
|
|
|
case CCurveLoopType.Cycle:
|
|
//start -> end / start -> end
|
|
cycle = GetNumberOfCycle(position);
|
|
float virtualPos = position - (cycle * (last.Position - first.Position));
|
|
return GetCurvePosition(virtualPos);
|
|
|
|
case CCurveLoopType.CycleOffset:
|
|
//make the curve continue (with no step) so must up the curve each cycle of delta(value)
|
|
cycle = GetNumberOfCycle(position);
|
|
virtualPos = position - (cycle * (last.Position - first.Position));
|
|
return (GetCurvePosition(virtualPos) + cycle * (last.Value - first.Value));
|
|
|
|
case CCurveLoopType.Oscillate:
|
|
//go back on curve from end and target start
|
|
// start-> end / end -> start
|
|
cycle = GetNumberOfCycle(position);
|
|
virtualPos = position - (cycle * (last.Position - first.Position));
|
|
if (0 == cycle % 2f) //if pair
|
|
virtualPos = position - (cycle * (last.Position - first.Position));
|
|
else
|
|
virtualPos = last.Position - position + first.Position +
|
|
(cycle * (last.Position - first.Position));
|
|
return GetCurvePosition(virtualPos);
|
|
}
|
|
}
|
|
|
|
//in curve
|
|
return GetCurvePosition(position);
|
|
}
|
|
|
|
#endregion Public Methods
|
|
|
|
#region Private Methods
|
|
|
|
private int GetNumberOfCycle(float position)
|
|
{
|
|
float cycle = (position - keys[0].Position) / (keys[keys.Count - 1].Position - keys[0].Position);
|
|
if (cycle < 0f)
|
|
cycle--;
|
|
return (int)cycle;
|
|
}
|
|
|
|
private float GetCurvePosition(float position)
|
|
{
|
|
//only for position in curve
|
|
CCurveKey prev = keys[0];
|
|
CCurveKey next;
|
|
for (int i = 1; i < keys.Count; i++)
|
|
{
|
|
next = Keys[i];
|
|
if (next.Position >= position)
|
|
{
|
|
if (prev.Continuity == CCurveContinuity.Step)
|
|
{
|
|
if (position >= 1f)
|
|
{
|
|
return next.Value;
|
|
}
|
|
return prev.Value;
|
|
}
|
|
float t = (position - prev.Position) / (next.Position - prev.Position); //to have t in [0,1]
|
|
float ts = t * t;
|
|
float tss = ts * t;
|
|
//After a lot of search on internet I have found all about spline function
|
|
// and bezier (phi'sss ancien) but finaly use hermite curve
|
|
//http://en.wikipedia.org/wiki/Cubic_Hermite_spline
|
|
//P(t) = (2*t^3 - 3t^2 + 1)*P0 + (t^3 - 2t^2 + t)m0 + (-2t^3 + 3t^2)P1 + (t^3-t^2)m1
|
|
//with P0.value = prev.value , m0 = prev.tangentOut, P1= next.value, m1 = next.TangentIn
|
|
return (2 * tss - 3 * ts + 1f) * prev.Value + (tss - 2 * ts + t) * prev.TangentOut + (3 * ts - 2 * tss) * next.Value +
|
|
(tss - ts) * next.TangentIn;
|
|
}
|
|
prev = next;
|
|
}
|
|
return 0f;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
public class CCurveKey : IEquatable<CCurveKey>, IComparable<CCurveKey>
|
|
{
|
|
#region Private Fields
|
|
|
|
private CCurveContinuity continuity;
|
|
private float position;
|
|
private float tangentIn;
|
|
private float tangentOut;
|
|
private float value;
|
|
|
|
#endregion Private Fields
|
|
|
|
#region Properties
|
|
|
|
public CCurveContinuity Continuity
|
|
{
|
|
get { return continuity; }
|
|
set { continuity = value; }
|
|
}
|
|
|
|
public float Position
|
|
{
|
|
get { return position; }
|
|
}
|
|
|
|
public float TangentIn
|
|
{
|
|
get { return tangentIn; }
|
|
set { tangentIn = value; }
|
|
}
|
|
|
|
public float TangentOut
|
|
{
|
|
get { return tangentOut; }
|
|
set { tangentOut = value; }
|
|
}
|
|
|
|
public float Value
|
|
{
|
|
get { return value; }
|
|
set { this.value = value; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
public CCurveKey(float position, float value)
|
|
: this(position, value, 0, 0, CCurveContinuity.Smooth)
|
|
{
|
|
}
|
|
|
|
public CCurveKey(float position, float value, float tangentIn, float tangentOut)
|
|
: this(position, value, tangentIn, tangentOut, CCurveContinuity.Smooth)
|
|
{
|
|
}
|
|
|
|
public CCurveKey(float position, float value, float tangentIn, float tangentOut, CCurveContinuity continuity)
|
|
{
|
|
this.position = position;
|
|
this.value = value;
|
|
this.tangentIn = tangentIn;
|
|
this.tangentOut = tangentOut;
|
|
this.continuity = continuity;
|
|
}
|
|
|
|
#endregion Constructors
|
|
|
|
#region Public Methods
|
|
|
|
#region IComparable<CurveKey> Members
|
|
|
|
public int CompareTo(CCurveKey other)
|
|
{
|
|
return position.CompareTo(other.position);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IEquatable<CCurveKey> Members
|
|
|
|
public bool Equals(CCurveKey other)
|
|
{
|
|
return (this == other);
|
|
}
|
|
|
|
#endregion
|
|
|
|
public static bool operator !=(CCurveKey a, CCurveKey b)
|
|
{
|
|
return !(a == b);
|
|
}
|
|
|
|
public static bool operator ==(CCurveKey a, CCurveKey b)
|
|
{
|
|
if (Equals(a, null))
|
|
return Equals(b, null);
|
|
|
|
if (Equals(b, null))
|
|
return Equals(a, null);
|
|
|
|
return (a.position == b.position)
|
|
&& (a.value == b.value)
|
|
&& (a.tangentIn == b.tangentIn)
|
|
&& (a.tangentOut == b.tangentOut)
|
|
&& (a.continuity == b.continuity);
|
|
}
|
|
|
|
public CCurveKey Clone()
|
|
{
|
|
return new CCurveKey(position, value, tangentIn, tangentOut, continuity);
|
|
}
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
return (obj is CCurveKey) ? ((CCurveKey)obj) == this : false;
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return position.GetHashCode() ^ value.GetHashCode() ^ tangentIn.GetHashCode() ^
|
|
tangentOut.GetHashCode() ^ continuity.GetHashCode();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
public class CCurveKeyCollection
|
|
{
|
|
#region Private Fields
|
|
|
|
private List<CCurveKey> innerlist;
|
|
private bool isReadOnly = false;
|
|
|
|
#endregion Private Fields
|
|
|
|
#region Properties
|
|
|
|
public CCurveKey this[int index]
|
|
{
|
|
get { return innerlist[index]; }
|
|
set
|
|
{
|
|
if (value == null)
|
|
throw new ArgumentNullException();
|
|
|
|
if (index >= innerlist.Count)
|
|
throw new IndexOutOfRangeException();
|
|
|
|
if (innerlist[index].Position == value.Position)
|
|
innerlist[index] = value;
|
|
else
|
|
{
|
|
innerlist.RemoveAt(index);
|
|
innerlist.Add(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public int Count
|
|
{
|
|
get { return innerlist.Count; }
|
|
}
|
|
|
|
public bool IsReadOnly
|
|
{
|
|
get { return isReadOnly; }
|
|
}
|
|
|
|
#endregion Properties
|
|
|
|
#region Constructors
|
|
|
|
public CCurveKeyCollection()
|
|
{
|
|
innerlist = new List<CCurveKey>();
|
|
}
|
|
|
|
#endregion Constructors
|
|
|
|
#region Public Methods
|
|
|
|
public void Add(CCurveKey item)
|
|
{
|
|
if (item == null)
|
|
throw new ArgumentNullException("Value cannot be null.", (Exception)null);
|
|
|
|
if (innerlist.Count == 0)
|
|
{
|
|
innerlist.Add(item);
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < innerlist.Count; i++)
|
|
{
|
|
if (item.Position < innerlist[i].Position)
|
|
{
|
|
innerlist.Insert(i, item);
|
|
return;
|
|
}
|
|
}
|
|
|
|
innerlist.Add(item);
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
innerlist.Clear();
|
|
}
|
|
|
|
public bool Contains(CCurveKey item)
|
|
{
|
|
return innerlist.Contains(item);
|
|
}
|
|
|
|
public void CopyTo(CCurveKey[] array, int arrayIndex)
|
|
{
|
|
innerlist.CopyTo(array, arrayIndex);
|
|
}
|
|
|
|
public IEnumerator<CCurveKey> GetEnumerator()
|
|
{
|
|
return innerlist.GetEnumerator();
|
|
}
|
|
|
|
public bool Remove(CCurveKey item)
|
|
{
|
|
return innerlist.Remove(item);
|
|
}
|
|
|
|
|
|
|
|
public CCurveKeyCollection Clone()
|
|
{
|
|
CCurveKeyCollection ckc = new CCurveKeyCollection();
|
|
foreach (CCurveKey key in innerlist)
|
|
ckc.Add(key);
|
|
return ckc;
|
|
}
|
|
|
|
public int IndexOf(CCurveKey item)
|
|
{
|
|
return innerlist.IndexOf(item);
|
|
}
|
|
|
|
public void RemoveAt(int index)
|
|
{
|
|
if (index != Count && index > -1)
|
|
innerlist.RemoveAt(index);
|
|
else
|
|
throw new ArgumentOutOfRangeException(
|
|
"Index was out of range. Must be non-negative and less than the size of the collection.\r\nParameter name: index",
|
|
(Exception)null);
|
|
}
|
|
|
|
#endregion Public Methods
|
|
}
|
|
} |