diff --git a/EulersRuler/CHANGELOG.md b/EulersRuler/CHANGELOG.md index 0947a83e..2d67a313 100644 --- a/EulersRuler/CHANGELOG.md +++ b/EulersRuler/CHANGELOG.md @@ -1,5 +1,14 @@ ## Changelog +### 1.9.1 + + * The `Stability` information on the `HoverPiecePanel` now also displays the material it's made out of (Stone, Wood, Marble, etc). + * Added a new enum `Resists` to the `hoverPiecePanelEnabledRows` config-option. It is disabled by default. + * When enabled, the `HoverPiecePanel` will report whether the piece resists rain, ash, and burning damage. + * Added new enum `Stability` to the `placementGhostPanelEnabledRows` config-option. It is enabled by default. + * When enabled, it displays the material of the current piece and estimates how far (in meters) the pieces can be snapped to the sides and top of the selected target. + * This calculation is not very accurate (the game's stability calculations are complex) but will provide a rough guide. + ### 1.9.0 * Updated for the `v0.219.14` patch. diff --git a/EulersRuler/Config/PluginConfig.cs b/EulersRuler/Config/PluginConfig.cs index 874bbe32..d1ce6b22 100644 --- a/EulersRuler/Config/PluginConfig.cs +++ b/EulersRuler/Config/PluginConfig.cs @@ -18,6 +18,7 @@ public enum HoverPiecePanelRow { Euler = 8, Quaternion = 16, PieceName = 32, + Resists = 64, } [Flags] @@ -29,6 +30,7 @@ public enum PlacementGhostPanelRow { PieceName = 32, Distance = 64, Position = 128, + Stability = 256, } public static ConfigEntry IsModEnabled { get; private set; } @@ -94,7 +96,7 @@ public static void BindConfig(ConfigFile config) { config.BindInOrder( "PlacementGhost.Panel", "placementGhostPanelEnabledRows", - PlacementGhostPanelRow.Name | PlacementGhostPanelRow.Euler | PlacementGhostPanelRow.Quaternion, + PlacementGhostPanelRow.Name | PlacementGhostPanelRow.Euler | PlacementGhostPanelRow.Quaternion | PlacementGhostPanelRow.Stability, "Which rows to display on the PlacementGhost properties panel."); PlacementGhostPanelFontSize = diff --git a/EulersRuler/Core/PanelUtils.cs b/EulersRuler/Core/PanelUtils.cs index 7f6902b6..c85108a6 100644 --- a/EulersRuler/Core/PanelUtils.cs +++ b/EulersRuler/Core/PanelUtils.cs @@ -35,7 +35,7 @@ public static IEnumerator UpdatePropertiesCoroutine() { } HoverPiecePanel.UpdateProperties(Player.m_localPlayer.m_hoveringPiece, HoverPiecePanelEnabledRows.Value); - PlacementGhostPanel.UpdateProperties(Player.m_localPlayer.m_placementGhost, PlacementGhostPanelEnabledRows.Value); + PlacementGhostPanel.UpdateProperties(Player.m_localPlayer.m_placementGhost, PlacementGhostPanelEnabledRows.Value, Player.m_localPlayer.m_hoveringPiece); } } diff --git a/EulersRuler/Core/UI/HoverPiecePanel.cs b/EulersRuler/Core/UI/HoverPiecePanel.cs index bd5c2f9d..6cd6245b 100644 --- a/EulersRuler/Core/UI/HoverPiecePanel.cs +++ b/EulersRuler/Core/UI/HoverPiecePanel.cs @@ -1,5 +1,6 @@ namespace EulersRuler; +using System.Text; using TMPro; using UnityEngine; @@ -18,6 +19,8 @@ public static class HoverPiecePanel { static TMP_Text _pieceEulerTextValue; static TMP_Text _pieceQuaternionTextLabel; static TMP_Text _pieceQuaternionTextValue; + static TMP_Text _pieceResistsTextLabel; + static TMP_Text _pieceResistsTextValue; public static void CreatePanel(Hud hud) { _hoverPiecePanel = @@ -27,6 +30,7 @@ public static void CreatePanel(Hud hud) { .AddPanelRow(out _pieceStabilityTextLabel, out _pieceStabilityTextValue) .AddPanelRow(out _pieceEulerTextLabel, out _pieceEulerTextValue) .AddPanelRow(out _pieceQuaternionTextLabel, out _pieceQuaternionTextValue) + .AddPanelRow(out _pieceResistsTextLabel, out _pieceResistsTextValue) .SetPosition(HoverPiecePanelPosition.Value) .SetAnchors(new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f)) .SetFontSize(HoverPiecePanelFontSize.Value); @@ -36,6 +40,7 @@ public static void CreatePanel(Hud hud) { _pieceStabilityTextLabel.text = "Stability \u2616"; _pieceEulerTextLabel.text = "Euler \u29bf"; _pieceQuaternionTextLabel.text = "Quaternion \u2318"; + _pieceResistsTextLabel.text = "Resists \u25A1"; } public static void DestroyPanel() { @@ -68,6 +73,7 @@ public static void UpdateProperties(Piece piece, HoverPiecePanelRow enabledRows) UpdateHoverPieceStabilityRow(wearNTear, enabledRows.HasFlag(HoverPiecePanelRow.Stability)); UpdateHoverPieceEulerRow(wearNTear, enabledRows.HasFlag(HoverPiecePanelRow.Euler)); UpdateHoverPieceQuaternionRow(wearNTear, enabledRows.HasFlag(HoverPiecePanelRow.Quaternion)); + UpdateHoverPieceResistsRow(wearNTear, enabledRows.HasFlag(HoverPiecePanelRow.Resists)); } static void UpdateHoverPieceNameRow(Piece piece, bool isRowEnabled, bool isPieceNameEnabled) { @@ -109,16 +115,19 @@ static void UpdateHoverPieceStabilityRow(WearNTear wearNTear, bool isRowEnabled) if (isRowEnabled) { float support = wearNTear.GetSupport(); float maxSupport = wearNTear.GetMaxSupport(); - float supportPrecent = Mathf.Clamp01(support / maxSupport); + float supportPercent = Mathf.Clamp01(support / maxSupport); + string materialType = wearNTear.m_materialType.ToString(); _pieceStabilityTextValue.text = string.Format( - "{1:N0} /{3} ({4:0%})", - ColorUtility.ToHtmlStringRGB(PanelUtils.StabilityPercentGradient.Evaluate(supportPrecent)), + "{1:N0} /{3} ({4:0%}, {6})", + ColorUtility.ToHtmlStringRGB(PanelUtils.StabilityPercentGradient.Evaluate(supportPercent)), support, "#FAFAFA", maxSupport, - supportPrecent); + supportPercent, + "#FAFAFA", + materialType); } } @@ -139,4 +148,36 @@ static void UpdateHoverPieceQuaternionRow(WearNTear wearNTear, bool isRowEnabled _pieceQuaternionTextValue.text = $"{wearNTear.transform.rotation:N2}"; } } + + static void UpdateHoverPieceResistsRow(WearNTear wearNTear, bool isRowEnabled) + { + _pieceResistsTextLabel.gameObject.SetActive(isRowEnabled); + _pieceResistsTextValue.gameObject.SetActive(isRowEnabled); + + if (isRowEnabled) { + StringBuilder sb = new StringBuilder(180); + + if (!wearNTear.m_noRoofWear) + sb.AppendFormat("{1}", "#4040f0", "no rain damage"); + + if (wearNTear.m_ashDamageImmune) { + if (sb.Length > 0) sb.Append(", "); + sb.AppendFormat("{1}", "#707070", "no ash damage"); + } + else if (wearNTear.m_ashDamageResist) { + if (sb.Length > 0) sb.Append(", "); + sb.AppendFormat("{1}", "#a0a0a0", "lower ash damage"); + } + + if (!wearNTear.m_burnable) { + if (sb.Length > 0) sb.Append(", "); + sb.AppendFormat("{1}", "#f07070", "no fire damage"); + } + + if (sb.Length == 0) + sb.AppendFormat("{1}", "#FAFAFA", "no resistances"); + + _pieceResistsTextValue.text = sb.ToString(); + } + } } diff --git a/EulersRuler/Core/UI/PlacementGhostPanel.cs b/EulersRuler/Core/UI/PlacementGhostPanel.cs index de7f8a02..5fc308d5 100644 --- a/EulersRuler/Core/UI/PlacementGhostPanel.cs +++ b/EulersRuler/Core/UI/PlacementGhostPanel.cs @@ -1,5 +1,6 @@ namespace EulersRuler; +using System.Text; using UnityEngine; using static PluginConfig; @@ -11,6 +12,7 @@ public static class PlacementGhostPanel { public static TwoColumnPanel.LabelRow GhostEulerRow { get; private set; } public static TwoColumnPanel.LabelRow GhostQuaternionRow { get; private set; } public static TwoColumnPanel.LabelRow GhostDistanceRow { get; private set; } + public static TwoColumnPanel.LabelRow GhostStabilityRow { get; private set; } public static void CreatePanel(Hud hud) { _placementGhostPanel = new TwoColumnPanel(hud.m_crosshair.transform); @@ -31,6 +33,10 @@ public static void CreatePanel(Hud hud) { GhostDistanceRow.LeftLabel.text = "Distance \u27A1"; GhostDistanceRow.RightLabel.color = new(1f, 0.79f, 0.15f); + GhostStabilityRow = _placementGhostPanel.AddLabelRow(); + GhostStabilityRow.LeftLabel.text = "Stability \u2616"; + GhostStabilityRow.RightLabel.color = new(1f, 0.79f, 0.15f); + _placementGhostPanel .SetPosition(PlacementGhostPanelPosition.Value) .SetAnchors(new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(0f, 0.5f)) @@ -51,14 +57,14 @@ public static void SetFontSize(int fontSize) { static PlacementGhostPanelRow _lastEnabledRows = PlacementGhostPanelRow.None; - public static void UpdateProperties(GameObject placementGhost, PlacementGhostPanelRow enabledRows) { + public static void UpdateProperties(GameObject placementGhost, PlacementGhostPanelRow enabledRows, Piece hoverPiece) { if (!_placementGhostPanel?.Panel) { return; } if (!placementGhost || enabledRows == PlacementGhostPanelRow.None - || !placementGhost.TryGetComponent(out Piece piece)) { + || !placementGhost.TryGetComponent(out Piece placementPiece)) { _placementGhostPanel.SetActive(false); return; } @@ -66,7 +72,7 @@ public static void UpdateProperties(GameObject placementGhost, PlacementGhostPan _placementGhostPanel.SetActive(true); SetupGhostRows(enabledRows); - UpdateGhostRows(placementGhost, piece, enabledRows); + UpdateGhostRows(placementGhost, placementPiece, enabledRows, hoverPiece); } static void SetupGhostRows(PlacementGhostPanelRow enabledRows) { @@ -78,13 +84,14 @@ static void SetupGhostRows(PlacementGhostPanelRow enabledRows) { GhostEulerRow.SetActive(enabledRows.HasFlag(PlacementGhostPanelRow.Euler)); GhostQuaternionRow.SetActive(enabledRows.HasFlag(PlacementGhostPanelRow.Quaternion)); GhostDistanceRow.SetActive(enabledRows.HasFlag(PlacementGhostPanelRow.Distance)); + GhostStabilityRow.SetActive(enabledRows.HasFlag(PlacementGhostPanelRow.Stability)); _lastEnabledRows = enabledRows; } - static void UpdateGhostRows(GameObject placementGhost, Piece piece, PlacementGhostPanelRow enabledRows) { + static void UpdateGhostRows(GameObject placementGhost, Piece placementPiece, PlacementGhostPanelRow enabledRows, Piece hoverPiece) { if (enabledRows.HasFlag(PlacementGhostPanelRow.Name)) { - UpdateGhostNameRow(piece, enabledRows.HasFlag(PlacementGhostPanelRow.PieceName)); + UpdateGhostNameRow(placementPiece, enabledRows.HasFlag(PlacementGhostPanelRow.PieceName)); } if (enabledRows.HasFlag(PlacementGhostPanelRow.Euler)) { @@ -98,6 +105,10 @@ static void UpdateGhostRows(GameObject placementGhost, Piece piece, PlacementGho if (enabledRows.HasFlag(PlacementGhostPanelRow.Distance)) { UpdateGhostDistanceRow(placementGhost, enabledRows.HasFlag(PlacementGhostPanelRow.Position)); } + + if (enabledRows.HasFlag(PlacementGhostPanelRow.Stability)) { + UpdateGhostStabilityRow(placementGhost, placementPiece, hoverPiece); + } } static void UpdateGhostNameRow(Piece piece, bool showPrefabName) { @@ -129,4 +140,82 @@ static void UpdateGhostDistanceRow(GameObject placementGhost, bool showPosition) GhostDistanceRow.RightLabel.text = distance.ToString("N2"); } } + + static void UpdateGhostStabilityRow(GameObject placementGhost, Piece placementPiece, Piece hoverPiece) { + // If we aren't hovering over anything or can't get their WearNTears, no need to show the stability row + if (!hoverPiece + || !hoverPiece.m_nview + || !hoverPiece.m_nview.IsValid() + || !hoverPiece.TryGetComponent(out WearNTear hoverWearNTear) + || !placementGhost.TryGetComponent(out WearNTear placementWearNTear)) { + GhostStabilityRow.SetActive(false); + return; + } + else { + GhostStabilityRow.SetActive(true); + } + + // Find out how much support the parent has left + float parentSupport = hoverWearNTear.GetSupport(); + + // Find out how much support this piece needs and consumes + placementWearNTear.GetMaterialProperties(out float maxSupport, out float minSupport, out float horizontalLoss, out float verticalLoss); + + // Compute roughly how many meters of this object can be snapped to the parent, both vertically and horizontally. + int metersCanPlaceHoriz = 0, metersCanPlaceVert = 0; + if (parentSupport > minSupport) { + // It would be ideal to list the number of objects we can snap horizontally and vertically, but the game's + // stability calculation factors in the width/height of objects and we aren't caculating the size of either piece yet. + // This simple calculation is relatively accurate when both objects are 1 meter wide/tall but badly off for larger pieces, + // especially when snapping a wide piece to a tall piece. But if we call the result "meters" it's pretty close! + // An accurate calculation would require calculating the dimensions of each piece, the angle of intersection, + // their centers of mass, and whether any other pieces also provide support (like placing the capstone in an arch). + // + // The 2m and 4m core wood parts are good test cases. You can stack 6 poles, 24m total, matching our numbers. + // This code thinks you can snap 13m worth of horizontal beams, though, but in the game the 3rd 4m beam shatters. + // But you can put six 2m beams in game, though! + // + // Once we have measurements of piece sizes, the accurate calculation is the ending block of the main loop + // in WearNTear.UpdateSupport() starting from the call to FindSupportPoint, using ACos on the pieces sizes. + + // How much support remains after adding a piece in each direction + float supportPercentLeftAfterEachPieceHoriz = 1f - horizontalLoss; + float supportPercentLeftAfterEachPieceVert = 1f - verticalLoss; + + // Bad approximation for the starting support - subtracts one iteration + float supportAvailableFromParent = Mathf.Min(maxSupport, parentSupport); + float supportAvailableHoriz = supportAvailableFromParent * supportPercentLeftAfterEachPieceHoriz; + float supportAvailableVert = supportAvailableFromParent * supportPercentLeftAfterEachPieceVert; + + // Calculate how many iterations we can repeat adding pieces till they collapse. + metersCanPlaceHoriz = Mathf.FloorToInt(Mathf.Log(minSupport / supportAvailableHoriz) / Mathf.Log(supportPercentLeftAfterEachPieceHoriz)); + metersCanPlaceVert = Mathf.FloorToInt(Mathf.Log(minSupport / supportAvailableVert) / Mathf.Log(supportPercentLeftAfterEachPieceVert)); + } + + metersCanPlaceHoriz = Mathf.Max(metersCanPlaceHoriz, 0); + metersCanPlaceVert = Mathf.Max(metersCanPlaceVert, 0); + + const string ColorPlain = "#FAFAFA"; + const string ColorError = "#F05250"; + const string ColorOk = "#9CCC63"; + StringBuilder sb = new StringBuilder(160); + + sb.AppendFormat("{1}: about ", ColorPlain, placementWearNTear.m_materialType.ToString()); + + sb.AppendFormat("{1}{2}", + metersCanPlaceHoriz > 1 ? ColorOk : ColorError, + metersCanPlaceHoriz, + metersCanPlaceHoriz > 0 ? "m" : ""); + + sb.AppendFormat(" horiz, ", ColorPlain); + + sb.AppendFormat("{1}{2}", + metersCanPlaceVert > 1 ? ColorOk : ColorError, + metersCanPlaceVert, + metersCanPlaceVert > 0 ? "m" : ""); + + sb.AppendFormat(" vert", ColorPlain); + + GhostStabilityRow.RightLabel.text = sb.ToString(); + } } diff --git a/EulersRuler/EulersRuler.cs b/EulersRuler/EulersRuler.cs index ed1fd2b0..ae37afa1 100644 --- a/EulersRuler/EulersRuler.cs +++ b/EulersRuler/EulersRuler.cs @@ -12,7 +12,7 @@ public sealed class EulersRuler : BaseUnityPlugin { public const string PluginGUID = "redseiko.valheim.eulersruler"; public const string PluginName = "EulersRuler"; - public const string PluginVersion = "1.9.0"; + public const string PluginVersion = "1.9.1"; void Awake() { BindConfig(Config); diff --git a/EulersRuler/manifest.json b/EulersRuler/manifest.json index 306e9dd0..dbc06e81 100644 --- a/EulersRuler/manifest.json +++ b/EulersRuler/manifest.json @@ -1,6 +1,6 @@ { "name": "EulersRuler", - "version_number": "1.9.0", + "version_number": "1.9.1", "website_url": "https://github.com/redseiko/ComfyMods/tree/main/EulersRuler", "author": "ComfyMods", "description": "Show building piece health, stability and rotation on your HUD!",