Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions EulersRuler/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 3 additions & 1 deletion EulersRuler/Config/PluginConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum HoverPiecePanelRow {
Euler = 8,
Quaternion = 16,
PieceName = 32,
Resists = 64,
}

[Flags]
Expand All @@ -29,6 +30,7 @@ public enum PlacementGhostPanelRow {
PieceName = 32,
Distance = 64,
Position = 128,
Stability = 256,
}

public static ConfigEntry<bool> IsModEnabled { get; private set; }
Expand Down Expand Up @@ -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 =
Expand Down
2 changes: 1 addition & 1 deletion EulersRuler/Core/PanelUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
49 changes: 45 additions & 4 deletions EulersRuler/Core/UI/HoverPiecePanel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace EulersRuler;

using System.Text;
using TMPro;

using UnityEngine;
Expand All @@ -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 =
Expand All @@ -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);
Expand All @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
"<color=#{0}>{1:N0}</color> /<color={2}>{3}</color> (<color=#{0}>{4:0%}</color>)",
ColorUtility.ToHtmlStringRGB(PanelUtils.StabilityPercentGradient.Evaluate(supportPrecent)),
"<color=#{0}>{1:N0}</color> /<color={2}>{3}</color> (<color=#{0}>{4:0%}</color>, <color={5}>{6}</color>)",
ColorUtility.ToHtmlStringRGB(PanelUtils.StabilityPercentGradient.Evaluate(supportPercent)),
support,
"#FAFAFA",
maxSupport,
supportPrecent);
supportPercent,
"#FAFAFA",
materialType);
}
}

Expand All @@ -139,4 +148,36 @@ static void UpdateHoverPieceQuaternionRow(WearNTear wearNTear, bool isRowEnabled
_pieceQuaternionTextValue.text = $"<color=#D7CCC8>{wearNTear.transform.rotation:N2}</color>";
}
}

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("<color={0}>{1}</color>", "#4040f0", "no rain damage");

if (wearNTear.m_ashDamageImmune) {
if (sb.Length > 0) sb.Append("<color=#FAFAFA>, </color>");
sb.AppendFormat("<color={0}>{1}</color>", "#707070", "no ash damage");
}
else if (wearNTear.m_ashDamageResist) {
if (sb.Length > 0) sb.Append("<color=#FAFAFA>, </color>");
sb.AppendFormat("<color={0}>{1}</color>", "#a0a0a0", "lower ash damage");
}

if (!wearNTear.m_burnable) {
if (sb.Length > 0) sb.Append("<color=#FAFAFA>, </color>");
sb.AppendFormat("<color={0}>{1}</color>", "#f07070", "no fire damage");
}

if (sb.Length == 0)
sb.AppendFormat("<color={0}>{1}</color>", "#FAFAFA", "no resistances");

_pieceResistsTextValue.text = sb.ToString();
}
}
}
99 changes: 94 additions & 5 deletions EulersRuler/Core/UI/PlacementGhostPanel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace EulersRuler;

using System.Text;
using UnityEngine;

using static PluginConfig;
Expand All @@ -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);
Expand All @@ -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))
Expand All @@ -51,22 +57,22 @@ 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;
}

_placementGhostPanel.SetActive(true);

SetupGhostRows(enabledRows);
UpdateGhostRows(placementGhost, piece, enabledRows);
UpdateGhostRows(placementGhost, placementPiece, enabledRows, hoverPiece);
}

static void SetupGhostRows(PlacementGhostPanelRow enabledRows) {
Expand All @@ -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)) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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("<color={0}>{1}: about </color>", ColorPlain, placementWearNTear.m_materialType.ToString());

sb.AppendFormat("<color={0}>{1}{2}</color>",
metersCanPlaceHoriz > 1 ? ColorOk : ColorError,
metersCanPlaceHoriz,
metersCanPlaceHoriz > 0 ? "m" : "");

sb.AppendFormat("<color={0}> horiz, </color>", ColorPlain);

sb.AppendFormat("<color={0}>{1}{2}</color>",
metersCanPlaceVert > 1 ? ColorOk : ColorError,
metersCanPlaceVert,
metersCanPlaceVert > 0 ? "m" : "");

sb.AppendFormat("<color={0}> vert</color>", ColorPlain);

GhostStabilityRow.RightLabel.text = sb.ToString();
}
}
2 changes: 1 addition & 1 deletion EulersRuler/EulersRuler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion EulersRuler/manifest.json
Original file line number Diff line number Diff line change
@@ -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!",
Expand Down