Merge branch 'dev' into too-many-signals

This commit is contained in:
xen-42 2025-08-04 21:12:11 -04:00
commit 57d945d6ee
20 changed files with 747 additions and 15 deletions

View File

@ -1,3 +1,4 @@
using NewHorizons.Components.EOTE;
using NewHorizons.External.Modules.Props;
using NewHorizons.External.Modules.Props.EchoesOfTheEye;
using NewHorizons.Handlers;
@ -70,6 +71,12 @@ namespace NewHorizons.Builder.Props.EchoesOfTheEye
dreamCandle._startLit = info.startLit;
dreamCandle.SetLit(info.startLit, false, true);
if (info.condition != null)
{
var conditionController = dreamCandle.gameObject.AddComponent<DreamLightConditionController>();
conditionController.SetFromInfo(info.condition);
}
return candleObj;
}
}

View File

@ -1,3 +1,4 @@
using NewHorizons.Components.EOTE;
using NewHorizons.External.Modules.Props;
using NewHorizons.External.Modules.Props.EchoesOfTheEye;
using NewHorizons.Handlers;
@ -116,6 +117,13 @@ namespace NewHorizons.Builder.Props.EchoesOfTheEye
projector._lit = info.startLit;
projector._startLit = info.startLit;
projector._extinguishOnly = info.extinguishOnly;
if (info.condition != null)
{
var conditionController = projector.gameObject.AddComponent<DreamLightConditionController>();
conditionController.SetFromInfo(info.condition);
}
/*
Delay.FireOnNextUpdate(() =>
{

View File

@ -16,13 +16,6 @@ namespace NewHorizons.Builder.Props
internal static void Init()
{
if (_itemTypes != null)
{
foreach (var value in _itemTypes.Values)
{
EnumUtils.Remove<ItemType>(value);
}
}
_itemTypes = new Dictionary<string, ItemType>();
}
@ -141,11 +134,7 @@ namespace NewHorizons.Builder.Props
{
go.layer = Layer.Interactible;
var itemType = EnumUtils.TryParse(info.itemType, true, out ItemType result) ? result : ItemType.Invalid;
if (itemType == ItemType.Invalid && !string.IsNullOrEmpty(info.itemType))
{
itemType = EnumUtilities.Create<ItemType>(info.itemType);
}
var itemType = GetOrCreateItemType(info.itemType);
var socket = go.GetAddComponent<NHItemSocket>();
socket._sector = sector;
@ -205,7 +194,7 @@ namespace NewHorizons.Builder.Props
}
else if (!string.IsNullOrEmpty(name))
{
itemType = EnumUtils.Create<ItemType>(name);
itemType = EnumUtilities.Create<ItemType>(name);
_itemTypes.Add(name, itemType);
}
return itemType;

View File

@ -80,6 +80,18 @@ namespace NewHorizons.Builder.Props
};
var scatterPrefab = DetailBuilder.Make(go, sector, mod, prefab, detailInfo);
bool reasonableHeightConstraints = true;
if (!propInfo.preventOverlap && (heightMapTexture != null) && (propInfo.minHeight != null || propInfo.maxHeight != null)) // If caution is relevant
{
var maxHeight = (propInfo.maxHeight != null ? Math.Min(propInfo.maxHeight, heightMap.maxHeight) : heightMap.maxHeight);
var minHeight = (propInfo.minHeight != null ? Math.Max(propInfo.minHeight, heightMap.minHeight) : heightMap.minHeight);
if ((maxHeight - minHeight) / (heightMap.maxHeight - heightMap.minHeight) < 0.001) // If height roll has less than 0.1% chance of being valid
{
NHLogger.LogError($"Ignoring minHeight/maxHeight for scatter of [{scatterPrefab.name}] to prevent infinite rerolls from too much constraint on height.");
reasonableHeightConstraints = false; // Ignore propInfo.min/maxHeight to prevent infinite rerolls
}
// That way, even if often not valid, it still won't loop much more than propInfo.count * 1000 per prop
}
for (int i = 0; i < propInfo.count; i++)
{
Vector3 point;
@ -113,7 +125,7 @@ namespace NewHorizons.Builder.Props
float relativeHeight = heightMapTexture.GetPixel((int)sampleX, (int)sampleY).r;
height = (relativeHeight * (heightMap.maxHeight - heightMap.minHeight) + heightMap.minHeight);
if ((propInfo.minHeight != null && height < propInfo.minHeight) || (propInfo.maxHeight != null && height > propInfo.maxHeight))
if (reasonableHeightConstraints && ((propInfo.minHeight != null && height < propInfo.minHeight) || (propInfo.maxHeight != null && height > propInfo.maxHeight)))
{
// Try this point again
i--;

View File

@ -0,0 +1,25 @@
using NewHorizons.Components.Volumes;
using NewHorizons.External.Modules.Volumes.VolumeInfos;
using UnityEngine;
namespace NewHorizons.Builder.Volumes
{
internal static class ConditionTriggerVolumeBuilder
{
public static ConditionTriggerVolume Make(GameObject planetGO, Sector sector, ConditionTriggerVolumeInfo info)
{
var volume = VolumeBuilder.Make<ConditionTriggerVolume>(planetGO, sector, info);
volume.Condition = info.condition;
volume.Persistent = info.persistent;
volume.Reversible = info.reversible;
volume.Player = info.player;
volume.Probe = info.probe;
volume.Ship = info.ship;
volume.gameObject.SetActive(true);
return volume;
}
}
}

View File

@ -0,0 +1,87 @@
using NewHorizons.Components.Volumes;
using NewHorizons.External.Modules.Volumes.VolumeInfos;
using NewHorizons.Handlers;
using NewHorizons.Utility;
using NewHorizons.Utility.Files;
using NewHorizons.Utility.OuterWilds;
using NewHorizons.Utility.OWML;
using OWML.Common;
using UnityEngine;
namespace NewHorizons.Builder.Volumes
{
internal static class InteractionVolumeBuilder
{
public static InteractReceiver Make(GameObject planetGO, Sector sector, InteractionVolumeInfo info, IModBehaviour mod)
{
// Interaction volumes must use colliders because the first-person interaction system uses raycasting
if (info.shape != null)
{
info.shape.useShape = false;
}
var receiver = VolumeBuilder.Make<InteractReceiver>(planetGO, sector, info);
receiver.gameObject.layer = Layer.Interactible;
receiver._interactRange = info.range;
receiver._checkViewAngle = info.maxViewAngle.HasValue;
receiver._maxViewAngle = info.maxViewAngle ?? 180f;
receiver._usableInShip = info.usableInShip;
var volume = receiver.gameObject.AddComponent<NHInteractionVolume>();
volume.Reusable = info.reusable;
volume.Condition = info.condition;
volume.Persistent = info.persistent;
if (!string.IsNullOrEmpty(info.audio))
{
var audioSource = receiver.gameObject.AddComponent<AudioSource>();
// This could be more configurable but this should cover the most common use cases without bloating the info object
var owAudioSource = receiver.gameObject.AddComponent<OWAudioSource>();
owAudioSource._audioSource = audioSource;
owAudioSource.playOnAwake = false;
owAudioSource.loop = false;
owAudioSource.SetMaxVolume(1f);
owAudioSource.SetClipSelectionType(OWAudioSource.ClipSelectionOnPlay.RANDOM);
owAudioSource.SetTrack(OWAudioMixer.TrackName.Environment);
AudioUtilities.SetAudioClip(owAudioSource, info.audio, mod);
}
if (!string.IsNullOrEmpty(info.pathToAnimator))
{
var animObj = planetGO.transform.Find(info.pathToAnimator);
if (animObj == null)
{
NHLogger.LogError($"Couldn't find child of {planetGO.transform.GetPath()} at {info.pathToAnimator}");
}
else
{
var animator = animObj.GetComponent<Animator>();
if (animator == null)
{
NHLogger.LogError($"Couldn't find Animator on {animObj.name} at {info.pathToAnimator}");
}
else
{
volume.TargetAnimator = animator;
volume.AnimationTrigger = info.animationTrigger;
}
}
}
receiver.gameObject.SetActive(true);
var text = TranslationHandler.GetTranslation(info.prompt, TranslationHandler.TextType.UI);
Delay.FireOnNextUpdate(() =>
{
// This NREs if set immediately
receiver.ChangePrompt(text);
});
return receiver;
}
}
}

View File

@ -35,6 +35,13 @@ namespace NewHorizons.Builder.Volumes
AudioVolumeBuilder.Make(go, sector, audioVolume, mod);
}
}
if (config.Volumes.conditionTriggerVolumes != null)
{
foreach (var conditionTriggerVolume in config.Volumes.conditionTriggerVolumes)
{
ConditionTriggerVolumeBuilder.Make(go, sector, conditionTriggerVolume);
}
}
if (config.Volumes.dayNightAudioVolumes != null)
{
foreach (var dayNightAudioVolume in config.Volumes.dayNightAudioVolumes)
@ -63,6 +70,13 @@ namespace NewHorizons.Builder.Volumes
VolumeBuilder.MakeAndEnable<MapRestrictionVolume>(go, sector, mapRestrictionVolume);
}
}
if (config.Volumes.interactionVolumes != null)
{
foreach (var interactionVolume in config.Volumes.interactionVolumes)
{
InteractionVolumeBuilder.Make(go, sector, interactionVolume, mod);
}
}
if (config.Volumes.interferenceVolumes != null)
{
foreach (var interferenceVolume in config.Volumes.interferenceVolumes)

View File

@ -0,0 +1,85 @@
using NewHorizons.External.Modules.Props.EchoesOfTheEye;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace NewHorizons.Components.EOTE
{
public class DreamLightConditionController : MonoBehaviour
{
public string Condition { get; set; }
public bool Persistent { get; set; }
public bool Reversible { get; set; }
public bool OnExtinguish { get; set; }
DreamObjectProjector _projector;
DreamCandle _dreamCandle;
public void SetFromInfo(DreamLightConditionInfo info)
{
Condition = info.condition;
Persistent = info.persistent;
Reversible = info.reversible;
OnExtinguish = info.onExtinguish;
}
protected void Awake()
{
_projector = GetComponent<DreamObjectProjector>();
_projector.OnProjectorLit.AddListener(OnProjectorLit);
_projector.OnProjectorExtinguished.AddListener(OnProjectorExtinguished);
_dreamCandle = GetComponent<DreamCandle>();
if (_dreamCandle != null)
{
_dreamCandle.OnLitStateChanged.AddListener(OnCandleLitStateChanged);
}
}
protected void OnDestroy()
{
if (_projector != null)
{
_projector.OnProjectorLit.RemoveListener(OnProjectorLit);
_projector.OnProjectorExtinguished.RemoveListener(OnProjectorExtinguished);
}
if (_dreamCandle != null)
{
_dreamCandle.OnLitStateChanged.RemoveListener(OnCandleLitStateChanged);
}
}
private void OnProjectorLit()
{
HandleCondition(!OnExtinguish);
}
private void OnProjectorExtinguished()
{
HandleCondition(OnExtinguish);
}
private void OnCandleLitStateChanged()
{
HandleCondition(OnExtinguish ? !_dreamCandle._lit : _dreamCandle._lit);
}
private void HandleCondition(bool shouldSet)
{
if (shouldSet || Reversible)
{
if (Persistent)
{
PlayerData.SetPersistentCondition(Condition, shouldSet);
}
else
{
DialogueConditionManager.SharedInstance.SetConditionState(Condition, shouldSet);
}
}
}
}
}

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace NewHorizons.Components.Volumes
{
public class ConditionTriggerVolume : BaseVolume
{
public string Condition { get; set; }
public bool Persistent { get; set; }
public bool Reversible { get; set; }
public bool Player { get; set; } = true;
public bool Probe { get; set; }
public bool Ship { get; set; }
public override void OnTriggerVolumeEntry(GameObject hitObj)
{
if (TestHitObject(hitObj))
{
if (Persistent)
{
PlayerData.SetPersistentCondition(Condition, true);
}
else
{
DialogueConditionManager.SharedInstance.SetConditionState(Condition, true);
}
}
}
public override void OnTriggerVolumeExit(GameObject hitObj)
{
if (Reversible && TestHitObject(hitObj))
{
if (Persistent)
{
PlayerData.SetPersistentCondition(Condition, false);
}
else
{
DialogueConditionManager.SharedInstance.SetConditionState(Condition, false);
}
}
}
bool TestHitObject(GameObject hitObj)
{
if (Player && hitObj.CompareTag("PlayerDetector"))
{
return true;
}
if (Probe && hitObj.CompareTag("ProbeDetector"))
{
return true;
}
if (Ship && hitObj.CompareTag("ShipDetector"))
{
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace NewHorizons.Components.Volumes
{
public class NHInteractionVolume : MonoBehaviour
{
public bool Reusable { get; set; }
public string Condition { get; set; }
public bool Persistent { get; set; }
public Animator TargetAnimator { get; set; }
public string AnimationTrigger { get; set; }
InteractReceiver _interactReceiver;
OWAudioSource _audioSource;
protected void Awake()
{
_interactReceiver = GetComponent<InteractReceiver>();
_audioSource = GetComponent<OWAudioSource>();
_interactReceiver.OnPressInteract += OnInteract;
}
protected void OnDestroy()
{
_interactReceiver.OnPressInteract -= OnInteract;
}
protected void OnInteract()
{
if (!string.IsNullOrEmpty(Condition))
{
if (Persistent)
{
PlayerData.SetPersistentCondition(Condition, true);
}
else
{
DialogueConditionManager.SharedInstance.SetConditionState(Condition, true);
}
}
if (_audioSource != null)
{
_audioSource.Play();
}
if (TargetAnimator)
{
TargetAnimator.SetTrigger(AnimationTrigger);
}
if (Reusable)
{
_interactReceiver.ResetInteraction();
_interactReceiver.EnableInteraction();
}
else
{
_interactReceiver.DisableInteraction();
}
}
}
}

View File

@ -20,5 +20,10 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye
/// Whether the candle should start lit or extinguished.
/// </summary>
public bool startLit;
/// <summary>
/// A condition to set when the candle is lit.
/// </summary>
public DreamLightConditionInfo condition;
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NewHorizons.External.Modules.Props.EchoesOfTheEye
{
public class DreamLightConditionInfo
{
/// <summary>
/// The name of the dialogue condition or persistent condition to set when the light is lit.
/// </summary>
public string condition;
/// <summary>
/// If true, the condition will persist across all future loops until unset.
/// </summary>
public bool persistent;
/// <summary>
/// Whether to unset the condition when the light is extinguished again.
/// </summary>
public bool reversible;
/// <summary>
/// Whether to set the condition when the light is extinguished instead. If `reversible` is true, the condition will be unset when the light is lit again.
/// </summary>
public bool onExtinguish;
}
}

View File

@ -45,5 +45,10 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye
/// If set, projected objects will be set to fully active or fully disabled instantly instead of smoothly fading lights/renderers/colliders. Use this if the normal behavior is insufficient for the objects you're using.
/// </summary>
public bool toggleProjectedObjectsActive;
/// <summary>
/// A condition to set when the totem is lit.
/// </summary>
public DreamLightConditionInfo condition;
}
}

View File

@ -0,0 +1,39 @@
using Newtonsoft.Json;
using System.ComponentModel;
namespace NewHorizons.External.Modules.Volumes.VolumeInfos
{
[JsonObject]
public class ConditionTriggerVolumeInfo : VolumeInfo
{
/// <summary>
/// The name of the dialogue condition or persistent condition to set when entering the volume.
/// </summary>
public string condition;
/// <summary>
/// If true, the condition will persist across all future loops until unset.
/// </summary>
public bool persistent;
/// <summary>
/// Whether to unset the condition when existing the volume.
/// </summary>
public bool reversible;
/// <summary>
/// Whether to set the condition when the player enters this volume. Defaults to true.
/// </summary>
[DefaultValue(true)] public bool player = true;
/// <summary>
/// Whether to set the condition when the scout probe enters this volume.
/// </summary>
public bool probe;
/// <summary>
/// Whether to set the condition when the ship enters this volume.
/// </summary>
public bool ship;
}
}

View File

@ -0,0 +1,65 @@
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 InteractionVolumeInfo : VolumeInfo
{
/// <summary>
/// The prompt to display when the volume is interacted with.
/// </summary>
public string prompt;
/// <summary>
/// The range at which the volume can be interacted with.
/// </summary>
[DefaultValue(2f)] public float range = 2f;
/// <summary>
/// The max view angle (in degrees) the player can see the volume with to interact with it. This will effectively be a cone extending from the volume's center forwards (along the Z axis) based on the volume's rotation.
/// If not specified, no view angle restriction will be applied.
/// </summary>
public float? maxViewAngle;
/// <summary>
/// Whether the volume can be interacted with while in the ship.
/// </summary>
public bool usableInShip;
/// <summary>
/// Whether the volume can be interacted with multiple times.
/// </summary>
public bool reusable;
/// <summary>
/// The name of the dialogue condition or persistent condition to set when the volume is interacted with.
/// </summary>
public string condition;
/// <summary>
/// If true, the condition will persist across all future loops until unset.
/// </summary>
public bool persistent;
/// <summary>
/// A sound to play when the volume is interacted with. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.
/// </summary>
public string audio;
/// <summary>
/// A path to an animator component where an animation will be triggered when the volume is interacted with.
/// </summary>
public string pathToAnimator;
/// <summary>
/// The name of an animation trigger to set on the animator when the volume is interacted with.
/// </summary>
public string animationTrigger;
}
}

View File

@ -11,6 +11,11 @@ namespace NewHorizons.External.Modules.Volumes
/// </summary>
public AudioVolumeInfo[] audioVolumes;
/// <summary>
/// Add condition trigger volumes to this planet. Sets a condition when the player, scout, or ship enters this volume.
/// </summary>
public ConditionTriggerVolumeInfo[] conditionTriggerVolumes;
/// <summary>
/// Add day night audio volumes to this planet. These volumes play a different clip depending on the time of day.
/// </summary>
@ -38,6 +43,12 @@ namespace NewHorizons.External.Modules.Volumes
/// </summary>
public HazardVolumeInfo[] hazardVolumes;
/// <summary>
/// Add interaction volumes to this planet.
/// They can be interacted with by the player to trigger various effects.
/// </summary>
public InteractionVolumeInfo[] interactionVolumes;
/// <summary>
/// Add interference volumes to this planet.
/// Hides HUD markers of ship scout/probe and prevents scout photos if you are not inside the volume together with ship or scout probe.

View File

@ -42,6 +42,16 @@ namespace NewHorizons.Handlers
public static void Init(List<NewHorizonsBody> bodies)
{
// TH gets preloaded in title screen. custom systems dont need this
if (Main.Instance.CurrentStarSystem is not ("SolarSystem" or "EyeOfTheUniverse"))
{
foreach (var bundle in StreamingManager.s_activeBundles)
{
// save memory NOW instead of next frame when other stuff has loaded and taken memory
bundle.UnloadImmediate();
}
}
// Start by destroying all planets if need be
if (Main.SystemDict[Main.Instance.CurrentStarSystem].Config.destroyStockPlanets)
{

View File

@ -4571,6 +4571,10 @@
"startLit": {
"type": "boolean",
"description": "Whether the candle should start lit or extinguished."
},
"condition": {
"description": "A condition to set when the candle is lit.",
"$ref": "#/definitions/DreamLightConditionInfo"
}
}
},
@ -4600,6 +4604,28 @@
"pile"
]
},
"DreamLightConditionInfo": {
"type": "object",
"additionalProperties": false,
"properties": {
"condition": {
"type": "string",
"description": "The name of the dialogue condition or persistent condition to set when the light is lit."
},
"persistent": {
"type": "boolean",
"description": "If true, the condition will persist across all future loops until unset."
},
"reversible": {
"type": "boolean",
"description": "Whether to unset the condition when the light is extinguished again."
},
"onExtinguish": {
"type": "boolean",
"description": "Whether to set the condition when the light is extinguished instead. If `reversible` is true, the condition will be unset when the light is lit again."
}
}
},
"ProjectionTotemInfo": {
"type": "object",
"additionalProperties": false,
@ -4667,6 +4693,10 @@
"toggleProjectedObjectsActive": {
"type": "boolean",
"description": "If set, projected objects will be set to fully active or fully disabled instantly instead of smoothly fading lights/renderers/colliders. Use this if the normal behavior is insufficient for the objects you're using."
},
"condition": {
"description": "A condition to set when the totem is lit.",
"$ref": "#/definitions/DreamLightConditionInfo"
}
}
},
@ -5365,6 +5395,13 @@
"$ref": "#/definitions/AudioVolumeInfo"
}
},
"conditionTriggerVolumes": {
"type": "array",
"description": "Add condition trigger volumes to this planet. Sets a condition when the player, scout, or ship enters this volume.",
"items": {
"$ref": "#/definitions/ConditionTriggerVolumeInfo"
}
},
"dayNightAudioVolumes": {
"type": "array",
"description": "Add day night audio volumes to this planet. These volumes play a different clip depending on the time of day.",
@ -5397,6 +5434,13 @@
"$ref": "#/definitions/HazardVolumeInfo"
}
},
"interactionVolumes": {
"type": "array",
"description": "Add interaction volumes to this planet.\nThey can be interacted with by the player to trigger various effects.",
"items": {
"$ref": "#/definitions/InteractionVolumeInfo"
}
},
"interferenceVolumes": {
"type": "array",
"description": "Add interference volumes to this planet.\nHides HUD markers of ship scout/probe and prevents scout photos if you are not inside the volume together with ship or scout probe.",
@ -5724,6 +5768,74 @@
"manual"
]
},
"ConditionTriggerVolumeInfo": {
"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"
},
"condition": {
"type": "string",
"description": "The name of the dialogue condition or persistent condition to set when entering the volume."
},
"persistent": {
"type": "boolean",
"description": "If true, the condition will persist across all future loops until unset."
},
"reversible": {
"type": "boolean",
"description": "Whether to unset the condition when existing the volume."
},
"player": {
"type": "boolean",
"description": "Whether to set the condition when the player enters this volume. Defaults to true.",
"default": true
},
"probe": {
"type": "boolean",
"description": "Whether to set the condition when the scout probe enters this volume."
},
"ship": {
"type": "boolean",
"description": "Whether to set the condition when the ship enters this volume."
}
}
},
"DayNightAudioVolumeInfo": {
"type": "object",
"additionalProperties": false,
@ -6528,6 +6640,95 @@
}
}
},
"InteractionVolumeInfo": {
"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"
},
"prompt": {
"type": "string",
"description": "The prompt to display when the volume is interacted with."
},
"range": {
"type": "number",
"description": "The range at which the volume can be interacted with.",
"format": "float",
"default": 2.0
},
"maxViewAngle": {
"type": [
"null",
"number"
],
"description": "The max view angle (in degrees) the player can see the volume with to interact with it. This will effectively be a cone extending from the volume's center forwards (along the Z axis) based on the volume's rotation.\nIf not specified, no view angle restriction will be applied.",
"format": "float"
},
"usableInShip": {
"type": "boolean",
"description": "Whether the volume can be interacted with while in the ship."
},
"reusable": {
"type": "boolean",
"description": "Whether the volume can be interacted with multiple times."
},
"condition": {
"type": "string",
"description": "The name of the dialogue condition or persistent condition to set when the volume is interacted with."
},
"persistent": {
"type": "boolean",
"description": "If true, the condition will persist across all future loops until unset."
},
"audio": {
"type": "string",
"description": "A sound to play when the volume is interacted with. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list."
},
"pathToAnimator": {
"type": "string",
"description": "A path to an animator component where an animation will be triggered when the volume is interacted with. "
},
"animationTrigger": {
"type": "string",
"description": "The name of an animation trigger to set on the animator when the volume is interacted with."
}
}
},
"VolumeInfo": {
"type": "object",
"additionalProperties": false,

View File

@ -23,4 +23,6 @@ To unlock ship logs after reading each text block, add a `<ShipLogConditions>` n
In your planet config, you must define where the Nomai text is positioned. See [the translator text json schema](/schemas/body-schema/defs/translatortextinfo/) for more info.
You can input a `seed` for a wall of text which will randomly generate the position of each arc. To test out different combinations, just keep incrementing the number and then hit "Reload Configs" from the pause menu with debug mode on. This seed ensures the same positioning each time the mod is played. Alternatively, you can use `arcInfo` to set the position and rotation of all text arcs, as well as determining their types (adult, teenager, child, or Stranger). The various age stages make the text look messier, while Stranger allows you to make a translatable version of the DLC text.
You can input a `seed` for a wall of text which will randomly generate the position of each arc. To test out different combinations, just keep incrementing the number and then hit "Reload Configs" from the pause menu with debug mode on. This seed ensures the same positioning each time the mod is played. Alternatively, you can use `arcInfo` to set the position and rotation of all text arcs, as well as determining their types (adult, teenager, child, or Stranger). The various age stages make the text look messier, while Stranger allows you to make a translatable version of the DLC text.
If you decide to arrange text manually in your mod, the [Unity Explorer](https://outerwildsmods.com/mods/unityexplorer/) and [Nomai Text Printer](https://github.com/coderCleric/NomaiTextPrinter) mods can make that much more convenient.

View File

@ -56,6 +56,7 @@ These mods are useful when developing your addon
- [Save Editor](https://outerwildsmods.com/mods/saveeditor) - Useful when creating a custom [ship log](/ship-log), can be used to reveal all custom facts so you can see them in the ship's computer.
- [Time Saver](https://outerwildsmods.com/mods/timesaver/) - Lets you skip some repeated cutscenes and get into the game faster.
- [The Examples Mod](https://github.com/Outer-Wilds-New-Horizons/nh-examples) - A mod that contains examples of how to use New Horizons features.
- [Nomai Text Printer](https://github.com/coderCleric/NomaiTextPrinter) - Useful for saving text changes (such as moving arcs with Unity Explorer) to your json files.
## Helpful Tools