diff --git a/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs b/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs index 38387bf8..5b648899 100644 --- a/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs +++ b/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs @@ -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(); + conditionController.SetFromInfo(info.condition); + } + return candleObj; } } diff --git a/NewHorizons/Builder/Props/EchoesOfTheEye/ProjectionTotemBuilder.cs b/NewHorizons/Builder/Props/EchoesOfTheEye/ProjectionTotemBuilder.cs index 69155147..6b70b567 100644 --- a/NewHorizons/Builder/Props/EchoesOfTheEye/ProjectionTotemBuilder.cs +++ b/NewHorizons/Builder/Props/EchoesOfTheEye/ProjectionTotemBuilder.cs @@ -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(); + conditionController.SetFromInfo(info.condition); + } + /* Delay.FireOnNextUpdate(() => { diff --git a/NewHorizons/Builder/Props/ItemBuilder.cs b/NewHorizons/Builder/Props/ItemBuilder.cs index 05620329..15b3bc27 100644 --- a/NewHorizons/Builder/Props/ItemBuilder.cs +++ b/NewHorizons/Builder/Props/ItemBuilder.cs @@ -16,13 +16,6 @@ namespace NewHorizons.Builder.Props internal static void Init() { - if (_itemTypes != null) - { - foreach (var value in _itemTypes.Values) - { - EnumUtils.Remove(value); - } - } _itemTypes = new Dictionary(); } @@ -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(info.itemType); - } + var itemType = GetOrCreateItemType(info.itemType); var socket = go.GetAddComponent(); socket._sector = sector; @@ -205,7 +194,7 @@ namespace NewHorizons.Builder.Props } else if (!string.IsNullOrEmpty(name)) { - itemType = EnumUtils.Create(name); + itemType = EnumUtilities.Create(name); _itemTypes.Add(name, itemType); } return itemType; diff --git a/NewHorizons/Builder/Props/ScatterBuilder.cs b/NewHorizons/Builder/Props/ScatterBuilder.cs index 862f0629..0df720d6 100644 --- a/NewHorizons/Builder/Props/ScatterBuilder.cs +++ b/NewHorizons/Builder/Props/ScatterBuilder.cs @@ -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--; diff --git a/NewHorizons/Builder/Volumes/ConditionTriggerVolumeBuilder.cs b/NewHorizons/Builder/Volumes/ConditionTriggerVolumeBuilder.cs new file mode 100644 index 00000000..2f9f270a --- /dev/null +++ b/NewHorizons/Builder/Volumes/ConditionTriggerVolumeBuilder.cs @@ -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(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; + } + } +} diff --git a/NewHorizons/Builder/Volumes/InteractionVolumeBuilder.cs b/NewHorizons/Builder/Volumes/InteractionVolumeBuilder.cs new file mode 100644 index 00000000..2f5c0122 --- /dev/null +++ b/NewHorizons/Builder/Volumes/InteractionVolumeBuilder.cs @@ -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(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(); + + volume.Reusable = info.reusable; + volume.Condition = info.condition; + volume.Persistent = info.persistent; + + if (!string.IsNullOrEmpty(info.audio)) + { + var audioSource = receiver.gameObject.AddComponent(); + + // 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._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(); + 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; + } + } +} diff --git a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs index 40303a0c..3acb65cf 100644 --- a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs +++ b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs @@ -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(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) diff --git a/NewHorizons/Components/EOTE/DreamLightConditionController.cs b/NewHorizons/Components/EOTE/DreamLightConditionController.cs new file mode 100644 index 00000000..7800f236 --- /dev/null +++ b/NewHorizons/Components/EOTE/DreamLightConditionController.cs @@ -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(); + _projector.OnProjectorLit.AddListener(OnProjectorLit); + _projector.OnProjectorExtinguished.AddListener(OnProjectorExtinguished); + + _dreamCandle = GetComponent(); + 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); + } + } + } + } +} diff --git a/NewHorizons/Components/Volumes/ConditionTriggerVolume.cs b/NewHorizons/Components/Volumes/ConditionTriggerVolume.cs new file mode 100644 index 00000000..047f8fb2 --- /dev/null +++ b/NewHorizons/Components/Volumes/ConditionTriggerVolume.cs @@ -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; + } + } +} diff --git a/NewHorizons/Components/Volumes/NHInteractionVolume.cs b/NewHorizons/Components/Volumes/NHInteractionVolume.cs new file mode 100644 index 00000000..4e258208 --- /dev/null +++ b/NewHorizons/Components/Volumes/NHInteractionVolume.cs @@ -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(); + _audioSource = GetComponent(); + + _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(); + } + } + } +} diff --git a/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs index 7f4efb24..95017236 100644 --- a/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs @@ -20,5 +20,10 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye /// Whether the candle should start lit or extinguished. /// public bool startLit; + + /// + /// A condition to set when the candle is lit. + /// + public DreamLightConditionInfo condition; } } diff --git a/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamLightConditionInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamLightConditionInfo.cs new file mode 100644 index 00000000..b1b00583 --- /dev/null +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamLightConditionInfo.cs @@ -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 + { + /// + /// The name of the dialogue condition or persistent condition to set when the light is lit. + /// + public string condition; + + /// + /// If true, the condition will persist across all future loops until unset. + /// + public bool persistent; + + /// + /// Whether to unset the condition when the light is extinguished again. + /// + public bool reversible; + + /// + /// 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. + /// + public bool onExtinguish; + } +} diff --git a/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionTotemInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionTotemInfo.cs index 0852b7e9..31953579 100644 --- a/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionTotemInfo.cs +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionTotemInfo.cs @@ -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. /// public bool toggleProjectedObjectsActive; + + /// + /// A condition to set when the totem is lit. + /// + public DreamLightConditionInfo condition; } } diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/ConditionTriggerVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/ConditionTriggerVolumeInfo.cs new file mode 100644 index 00000000..c364d151 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/ConditionTriggerVolumeInfo.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json; +using System.ComponentModel; + +namespace NewHorizons.External.Modules.Volumes.VolumeInfos +{ + [JsonObject] + public class ConditionTriggerVolumeInfo : VolumeInfo + { + /// + /// The name of the dialogue condition or persistent condition to set when entering the volume. + /// + public string condition; + + /// + /// If true, the condition will persist across all future loops until unset. + /// + public bool persistent; + + /// + /// Whether to unset the condition when existing the volume. + /// + public bool reversible; + + /// + /// Whether to set the condition when the player enters this volume. Defaults to true. + /// + [DefaultValue(true)] public bool player = true; + + /// + /// Whether to set the condition when the scout probe enters this volume. + /// + public bool probe; + + /// + /// Whether to set the condition when the ship enters this volume. + /// + public bool ship; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/InteractionVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/InteractionVolumeInfo.cs new file mode 100644 index 00000000..0a9e7744 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/InteractionVolumeInfo.cs @@ -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 + { + /// + /// The prompt to display when the volume is interacted with. + /// + public string prompt; + + /// + /// The range at which the volume can be interacted with. + /// + [DefaultValue(2f)] public float range = 2f; + + /// + /// 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. + /// + public float? maxViewAngle; + + /// + /// Whether the volume can be interacted with while in the ship. + /// + public bool usableInShip; + + /// + /// Whether the volume can be interacted with multiple times. + /// + public bool reusable; + + /// + /// The name of the dialogue condition or persistent condition to set when the volume is interacted with. + /// + public string condition; + + /// + /// If true, the condition will persist across all future loops until unset. + /// + public bool persistent; + + /// + /// 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. + /// + public string audio; + + /// + /// A path to an animator component where an animation will be triggered when the volume is interacted with. + /// + public string pathToAnimator; + + /// + /// The name of an animation trigger to set on the animator when the volume is interacted with. + /// + public string animationTrigger; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumesModule.cs b/NewHorizons/External/Modules/Volumes/VolumesModule.cs index 800e94bd..0320df0b 100644 --- a/NewHorizons/External/Modules/Volumes/VolumesModule.cs +++ b/NewHorizons/External/Modules/Volumes/VolumesModule.cs @@ -11,6 +11,11 @@ namespace NewHorizons.External.Modules.Volumes /// public AudioVolumeInfo[] audioVolumes; + /// + /// Add condition trigger volumes to this planet. Sets a condition when the player, scout, or ship enters this volume. + /// + public ConditionTriggerVolumeInfo[] conditionTriggerVolumes; + /// /// Add day night audio volumes to this planet. These volumes play a different clip depending on the time of day. /// @@ -38,6 +43,12 @@ namespace NewHorizons.External.Modules.Volumes /// public HazardVolumeInfo[] hazardVolumes; + /// + /// Add interaction volumes to this planet. + /// They can be interacted with by the player to trigger various effects. + /// + public InteractionVolumeInfo[] interactionVolumes; + /// /// 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. diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 1528151d..e1c4864e 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -42,6 +42,16 @@ namespace NewHorizons.Handlers public static void Init(List 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) { diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index b525e4fe..a15baa45 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -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, diff --git a/docs/src/content/docs/guides/nomai-text.md b/docs/src/content/docs/guides/nomai-text.md index 959aeeb4..df6fb82b 100644 --- a/docs/src/content/docs/guides/nomai-text.md +++ b/docs/src/content/docs/guides/nomai-text.md @@ -23,4 +23,6 @@ To unlock ship logs after reading each text block, add a `` 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/src/content/docs/start-here/helpful-resources.md b/docs/src/content/docs/start-here/helpful-resources.md index 0ca13791..4ba3f2c8 100644 --- a/docs/src/content/docs/start-here/helpful-resources.md +++ b/docs/src/content/docs/start-here/helpful-resources.md @@ -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