Repair Volumes (#1085)

## Minor features

- Added `repairVolumes` which act like the repairable nodes on the
"satellite" in the Zero-G cave. Resolves #1084

[Examples
PR](https://github.com/Outer-Wilds-New-Horizons/nh-examples/pull/59)

Also in this PR I've fixed two unrelated minor issues that were causing
warnings to be flagged on every build.
This commit is contained in:
xen-42 2025-05-12 17:33:08 -04:00 committed by GitHub
commit ce2b247ebf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 362 additions and 5 deletions

View File

@ -459,11 +459,6 @@ namespace NewHorizons.Builder.ShipLog
private static MapModeObject ConstructPrimaryNode(List<NewHorizonsBody> bodies)
{
float DistanceFromPrimary(NewHorizonsBody body)
{
return Mathf.Max(body.Config.Orbit.semiMajorAxis, body.Config.Orbit.staticPosition?.Length() ?? 0f);
}
foreach (NewHorizonsBody body in bodies.Where(b => b.Config.Base.centerOfSolarSystem))
{
bodies.Sort((b, o) => b.Config.Orbit.semiMajorAxis.CompareTo(o.Config.Orbit.semiMajorAxis));

View File

@ -0,0 +1,52 @@
using NewHorizons.Builder.Props;
using NewHorizons.Components.Volumes;
using NewHorizons.External.Modules.Props;
using NewHorizons.External.Modules.Volumes.VolumeInfos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace NewHorizons.Builder.Volumes
{
public static class RepairVolumeBuilder
{
public static NHRepairReceiver Make(GameObject planetGO, Sector sector, RepairVolumeInfo info)
{
// Repair receivers aren't technically volumes (no OWTriggerVolume) so we don't use the VolumeBuilder
var go = GeneralPropBuilder.MakeNew("RepairVolume", planetGO, sector, info);
if (info.shape != null)
{
ShapeBuilder.AddCollider(go, info.shape);
}
else
{
var shapeInfo = new ShapeInfo()
{
type = ShapeType.Sphere,
useShape = false,
hasCollision = true,
radius = info.radius,
};
ShapeBuilder.AddCollider(go, shapeInfo);
}
var repairReceiver = go.AddComponent<NHRepairReceiver>();
repairReceiver.displayName = info.name ?? info.rename ?? go.name;
repairReceiver.repairFraction = info.repairFraction;
repairReceiver.repairTime = info.repairTime;
repairReceiver.repairDistance = info.repairDistance;
repairReceiver.damagedCondition = info.damagedCondition;
repairReceiver.repairedCondition = info.repairedCondition;
repairReceiver.revealFact = info.revealFact;
go.SetActive(true);
return repairReceiver;
}
}
}

View File

@ -222,6 +222,13 @@ namespace NewHorizons.Builder.Volumes
VolumeBuilder.MakeAndEnable<ReferenceFrameBlockerVolume>(go, sector, referenceFrameBlockerVolume);
}
}
if (config.Volumes.repairVolumes != null)
{
foreach (var repairVolume in config.Volumes.repairVolumes)
{
RepairVolumeBuilder.Make(go, sector, repairVolume);
}
}
if (config.Volumes.speedTrapVolumes != null)
{
foreach (var speedTrapVolume in config.Volumes.speedTrapVolumes)

View File

@ -0,0 +1,103 @@
using NewHorizons.Handlers;
using OWML.Utils;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
namespace NewHorizons.Components.Volumes
{
// RepairReceiver isn't set up for proper subclassing but a subclass is necessary for the first-person manipulator to detect it.
public class NHRepairReceiver : RepairReceiver
{
public static Type RepairReceiverType = EnumUtils.Create<Type>("NewHorizons");
public class RepairEvent : UnityEvent<NHRepairReceiver> { }
public RepairEvent OnRepaired = new();
public RepairEvent OnDamaged = new();
public string displayName;
public float repairTime;
public string damagedCondition;
public string repairedCondition;
public string revealFact;
float _repairFraction = 0f;
UITextType _uiTextType = UITextType.None;
public float repairFraction
{
get => _repairFraction;
set
{
var prevValue = _repairFraction;
_repairFraction = Mathf.Clamp01(value);
if (prevValue < 1f && _repairFraction >= 1f)
{
Repair();
}
else if (prevValue >= 1f && _repairFraction < 1f)
{
Damage();
}
}
}
public new virtual bool IsRepairable() => IsDamaged();
public new virtual bool IsDamaged() => _repairFraction < 1f;
public new virtual float GetRepairFraction() => _repairFraction;
protected new void Awake()
{
base.Awake();
_type = RepairReceiverType;
if (IsDamaged()) Damage();
else Repair();
}
public new virtual void RepairTick()
{
if (!IsRepairable()) return;
repairFraction += Time.deltaTime / repairTime;
}
public new virtual UITextType GetRepairableName()
{
if (_uiTextType != UITextType.None) return _uiTextType;
var value = TranslationHandler.GetTranslation(displayName, TranslationHandler.TextType.UI);
_uiTextType = (UITextType)TranslationHandler.AddUI(value, false);
return _uiTextType;
}
void Damage()
{
if (!string.IsNullOrEmpty(damagedCondition))
{
DialogueConditionManager.SharedInstance.SetConditionState(damagedCondition, true);
}
if (!string.IsNullOrEmpty(repairedCondition))
{
DialogueConditionManager.SharedInstance.SetConditionState(repairedCondition, false);
}
OnDamaged.Invoke(this);
}
void Repair()
{
if (!string.IsNullOrEmpty(damagedCondition))
{
DialogueConditionManager.SharedInstance.SetConditionState(damagedCondition, false);
}
if (!string.IsNullOrEmpty(repairedCondition))
{
DialogueConditionManager.SharedInstance.SetConditionState(repairedCondition, true);
}
if (!string.IsNullOrEmpty(revealFact))
{
Locator.GetShipLogManager().RevealFact(revealFact);
}
OnRepaired.Invoke(this);
}
}
}

View File

@ -0,0 +1,49 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NewHorizons.External.Modules.Volumes.VolumeInfos
{
[JsonObject]
public class RepairVolumeInfo : VolumeInfo
{
/// <summary>
/// The name displayed in the UI when the player is repairing this object. If not set, the name of the object will be used.
/// </summary>
public string name;
/// <summary>
/// How much of the object is initially repaired. 0 = not repaired, 1 = fully repaired.
/// </summary>
[DefaultValue(0f)] public float repairFraction = 0f;
/// <summary>
/// The time it takes to repair the object. Defaults to 3 seconds.
/// </summary>
[DefaultValue(3f)] public float repairTime = 3f;
/// <summary>
/// The distance from the object that the player can be to repair it. Defaults to 3 meters.
/// </summary>
[DefaultValue(3f)] public float repairDistance = 3f;
/// <summary>
/// A dialogue condition that will be set while the object is damaged. It will be unset when the object is repaired.
/// </summary>
public string damagedCondition;
/// <summary>
/// A dialogue condition that will be set when the object is repaired. It will be unset if the object is damaged again.
/// </summary>
public string repairedCondition;
/// <summary>
/// A ship log fact that will be revealed when the object is repaired.
/// </summary>
public string revealFact;
}
}

View File

@ -85,6 +85,11 @@ namespace NewHorizons.External.Modules.Volumes
/// </summary>
public VolumeInfo[] referenceFrameBlockerVolumes;
/// <summary>
/// Add repair volumes to this planet.
/// </summary>
public RepairVolumeInfo[] repairVolumes;
/// <summary>
/// Add triggers that reveal parts of the ship log on this planet.
/// </summary>

View File

@ -0,0 +1,62 @@
using HarmonyLib;
using NewHorizons.Components.Volumes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NewHorizons.Patches.VolumePatches
{
[HarmonyPatch(typeof(RepairReceiver))]
public static class RepairReceiverPatches
{
// We can't actually override these methods so we patch the base class methods to invoke the subclass methods dynamically
[HarmonyPostfix, HarmonyPatch(nameof(RepairReceiver.IsRepairable))]
public static void IsRepairable(RepairReceiver __instance, ref bool __result)
{
if (__instance is NHRepairReceiver r)
{
__result = r.IsRepairable();
}
}
[HarmonyPostfix, HarmonyPatch(nameof(RepairReceiver.RepairTick))]
public static void RepairTick(RepairReceiver __instance)
{
if (__instance is NHRepairReceiver r)
{
r.RepairTick();
}
}
[HarmonyPostfix, HarmonyPatch(nameof(RepairReceiver.IsDamaged))]
public static void IsDamaged(RepairReceiver __instance, ref bool __result)
{
if (__instance is NHRepairReceiver r)
{
__result = r.IsDamaged();
}
}
[HarmonyPostfix, HarmonyPatch(nameof(RepairReceiver.GetRepairableName))]
public static void GetRepairableName(RepairReceiver __instance, ref UITextType __result)
{
if (__instance is NHRepairReceiver r)
{
__result = r.GetRepairableName();
}
}
[HarmonyPostfix, HarmonyPatch(nameof(RepairReceiver.GetRepairFraction))]
public static void GetRepairFraction(RepairReceiver __instance, ref float __result)
{
if (__instance is NHRepairReceiver r)
{
__result = r.GetRepairFraction();
}
}
}
}

View File

@ -5450,6 +5450,13 @@
"$ref": "#/definitions/VolumeInfo"
}
},
"repairVolumes": {
"type": "array",
"description": "Add repair volumes to this planet.",
"items": {
"$ref": "#/definitions/RepairVolumeInfo"
}
},
"revealVolumes": {
"type": "array",
"description": "Add triggers that reveal parts of the ship log on this planet.",
@ -6722,6 +6729,83 @@
}
}
},
"RepairVolumeInfo": {
"type": "object",
"additionalProperties": false,
"properties": {
"radius": {
"type": "number",
"description": "The radius of this volume, if a shape is not specified.",
"format": "float",
"default": 1.0
},
"shape": {
"description": "The shape of this volume. Defaults to a sphere with a radius of `radius` if not specified.",
"$ref": "#/definitions/ShapeInfo"
},
"rotation": {
"description": "Rotation of the object",
"$ref": "#/definitions/MVector3"
},
"alignRadial": {
"type": [
"boolean",
"null"
],
"description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else."
},
"position": {
"description": "Position of the object",
"$ref": "#/definitions/MVector3"
},
"isRelativeToParent": {
"type": "boolean",
"description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object."
},
"parentPath": {
"type": "string",
"description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)."
},
"rename": {
"type": "string",
"description": "An optional rename of this object"
},
"name": {
"type": "string",
"description": "The name displayed in the UI when the player is repairing this object. If not set, the name of the object will be used."
},
"repairFraction": {
"type": "number",
"description": "How much of the object is initially repaired. 0 = not repaired, 1 = fully repaired.",
"format": "float",
"default": 0.0
},
"repairTime": {
"type": "number",
"description": "The time it takes to repair the object. Defaults to 3 seconds.",
"format": "float",
"default": 3.0
},
"repairDistance": {
"type": "number",
"description": "The distance from the object that the player can be to repair it. Defaults to 3 meters.",
"format": "float",
"default": 3.0
},
"damagedCondition": {
"type": "string",
"description": "A dialogue condition that will be set while the object is damaged. It will be unset when the object is repaired."
},
"repairedCondition": {
"type": "string",
"description": "A dialogue condition that will be set when the object is repaired. It will be unset if the object is damaged again."
},
"revealFact": {
"type": "string",
"description": "A ship log fact that will be revealed when the object is repaired."
}
}
},
"RevealVolumeInfo": {
"type": "object",
"additionalProperties": false,