diff --git a/NewHorizons/Builder/Body/SingularityBuilder.cs b/NewHorizons/Builder/Body/SingularityBuilder.cs index eb8863c8..f7d303ef 100644 --- a/NewHorizons/Builder/Body/SingularityBuilder.cs +++ b/NewHorizons/Builder/Body/SingularityBuilder.cs @@ -150,6 +150,7 @@ namespace NewHorizons.Builder.Body streamingVolume.streamingGroup = streamingGroup; streamingVolume.transform.parent = blackHoleVolume.transform; streamingVolume.transform.localPosition = Vector3.zero; + streamingVolume.gameObject.SetActive(true); } } catch (Exception e) diff --git a/NewHorizons/Builder/Props/ShapeBuilder.cs b/NewHorizons/Builder/Props/ShapeBuilder.cs new file mode 100644 index 00000000..1496e6e0 --- /dev/null +++ b/NewHorizons/Builder/Props/ShapeBuilder.cs @@ -0,0 +1,170 @@ +using NewHorizons.Components; +using NewHorizons.External.Modules.Props; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Builder.Props +{ + public static class ShapeBuilder + { + public static OWTriggerVolume AddTriggerVolume(GameObject go, ShapeInfo info, float defaultRadius) + { + var owTriggerVolume = go.AddComponent(); + + if (info != null) + { + var shapeOrCol = AddShapeOrCollider(go, info); + if (shapeOrCol is Shape shape) + owTriggerVolume._shape = shape; + else if (shapeOrCol is Collider col) + owTriggerVolume._owCollider = col.GetComponent(); + } + else + { + var col = go.AddComponent(); + col.radius = defaultRadius; + col.isTrigger = true; + var owCollider = go.GetAddComponent(); + + owTriggerVolume._owCollider = owCollider; + } + + return owTriggerVolume; + } + + public static Component AddShapeOrCollider(GameObject go, ShapeInfo info) + { + if (info.useShape.HasValue) + { + // Explicitly add either a shape or collider if specified + if (info.useShape.Value) + return AddShape(go, info); + else + return AddCollider(go, info); + } + else + { + // Prefer colliders over shapes if no preference is specified + if (info.type is ShapeType.Sphere or ShapeType.Box or ShapeType.Capsule) + return AddCollider(go, info); + else + return AddShape(go, info); + } + } + + public static Shape AddShape(GameObject go, ShapeInfo info) + { + if (info.hasCollision) + { + throw new NotSupportedException($"Shapes do not support collision; set {nameof(info.hasCollision)} to false or use a supported collider type (sphere, box, or capsule)."); + } + if (info.useShape.HasValue && !info.useShape.Value) + { + throw new NotSupportedException($"{info.useShape} was explicitly set to false but a shape is required here."); + } + switch (info.type) + { + case ShapeType.Sphere: + var sphereShape = go.AddComponent(); + sphereShape._radius = info.radius; + sphereShape._center = info.offset ?? Vector3.zero; + return sphereShape; + case ShapeType.Box: + var boxShape = go.AddComponent(); + boxShape._size = info.size ?? Vector3.one; + boxShape._center = info.offset ?? Vector3.zero; + return boxShape; + case ShapeType.Capsule: + var capsuleShape = go.AddComponent(); + capsuleShape._radius = info.radius; + capsuleShape._direction = (int)info.direction; + capsuleShape._height = info.height; + capsuleShape._center = info.offset ?? Vector3.zero; + return capsuleShape; + case ShapeType.Cylinder: + var cylinderShape = go.AddComponent(); + cylinderShape._radius = info.radius; + cylinderShape._height = info.height; + cylinderShape._center = info.offset ?? Vector3.zero; + cylinderShape._pointChecksOnly = true; + return cylinderShape; + case ShapeType.Cone: + var coneShape = go.AddComponent(); + coneShape._topRadius = info.innerRadius; + coneShape._bottomRadius = info.outerRadius; + coneShape._direction = (int)info.direction; + coneShape._height = info.height; + coneShape._center = info.offset ?? Vector3.zero; + coneShape._pointChecksOnly = true; + return coneShape; + case ShapeType.Hemisphere: + var hemisphereShape = go.AddComponent(); + hemisphereShape._radius = info.radius; + hemisphereShape._direction = (int)info.direction; + hemisphereShape._cap = info.cap; + hemisphereShape._center = info.offset ?? Vector3.zero; + hemisphereShape._pointChecksOnly = true; + return hemisphereShape; + case ShapeType.Hemicapsule: + var hemicapsuleShape = go.AddComponent(); + hemicapsuleShape._radius = info.radius; + hemicapsuleShape._direction = (int)info.direction; + hemicapsuleShape._height = info.height; + hemicapsuleShape._cap = info.cap; + hemicapsuleShape._center = info.offset ?? Vector3.zero; + hemicapsuleShape._pointChecksOnly = true; + return hemicapsuleShape; + case ShapeType.Ring: + var ringShape = go.AddComponent(); + ringShape.innerRadius = info.innerRadius; + ringShape.outerRadius = info.outerRadius; + ringShape.height = info.height; + ringShape.center = info.offset ?? Vector3.zero; + ringShape._pointChecksOnly = true; + return ringShape; + default: + throw new ArgumentOutOfRangeException(nameof(info.type), info.type, $"Unsupported shape type"); + } + } + + public static Collider AddCollider(GameObject go, ShapeInfo info) + { + if (info.useShape.HasValue && info.useShape.Value) + { + throw new NotSupportedException($"{info.useShape} was explicitly set to true but a non-shape collider is required here."); + } + switch (info.type) + { + case ShapeType.Sphere: + var sphereCollider = go.AddComponent(); + sphereCollider.radius = info.radius; + sphereCollider.center = info.offset ?? Vector3.zero; + sphereCollider.isTrigger = !info.hasCollision; + go.GetAddComponent(); + return sphereCollider; + case ShapeType.Box: + var boxCollider = go.AddComponent(); + boxCollider.size = info.size ?? Vector3.one; + boxCollider.center = info.offset ?? Vector3.zero; + boxCollider.isTrigger = !info.hasCollision; + go.GetAddComponent(); + return boxCollider; + case ShapeType.Capsule: + var capsuleCollider = go.AddComponent(); + capsuleCollider.radius = info.radius; + capsuleCollider.direction = (int)info.direction; + capsuleCollider.height = info.height; + capsuleCollider.center = info.offset ?? Vector3.zero; + capsuleCollider.isTrigger = !info.hasCollision; + go.GetAddComponent(); + return capsuleCollider; + default: + throw new ArgumentOutOfRangeException(nameof(info.type), info.type, $"Unsupported collider type"); + } + } + } +} diff --git a/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs b/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs index 6b961a79..0c209154 100644 --- a/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs @@ -25,7 +25,8 @@ namespace NewHorizons.Builder.Volumes owAudioSource.SetTrack(info.track.ConvertToOW()); AudioUtilities.SetAudioClip(owAudioSource, info.audio, mod); - var audioVolume = go.AddComponent(); + var audioVolume = PriorityVolumeBuilder.MakeExisting(go, planetGO, sector, info); + audioVolume._layer = info.layer; audioVolume.SetPriority(info.priority); audioVolume._fadeSeconds = info.fadeSeconds; @@ -33,11 +34,7 @@ namespace NewHorizons.Builder.Volumes audioVolume._randomizePlayhead = info.randomizePlayhead; audioVolume._pauseOnFadeOut = info.pauseOnFadeOut; - var shape = go.AddComponent(); - shape.radius = info.radius; - - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._shape = shape; + var owTriggerVolume = go.GetComponent(); audioVolume._triggerVolumeOverride = owTriggerVolume; go.SetActive(true); diff --git a/NewHorizons/Builder/Volumes/ChangeStarSystemVolumeBuilder.cs b/NewHorizons/Builder/Volumes/ChangeStarSystemVolumeBuilder.cs index 5557da98..54eddc3d 100644 --- a/NewHorizons/Builder/Volumes/ChangeStarSystemVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/ChangeStarSystemVolumeBuilder.cs @@ -13,6 +13,8 @@ namespace NewHorizons.Builder.Volumes volume.TargetSolarSystem = info.targetStarSystem; volume.TargetSpawnID = info.spawnPointID; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs b/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs index 4f1c0ecb..cd55f9da 100644 --- a/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs @@ -16,6 +16,8 @@ namespace NewHorizons.Builder.Volumes volume.deathType = info.deathType == null ? null : EnumUtils.Parse(info.deathType.ToString(), DeathType.Default); volume.mod = mod; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/DayNightAudioVolumeBuilder.cs b/NewHorizons/Builder/Volumes/DayNightAudioVolumeBuilder.cs index b2ed1ce1..875bba75 100644 --- a/NewHorizons/Builder/Volumes/DayNightAudioVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/DayNightAudioVolumeBuilder.cs @@ -15,7 +15,8 @@ namespace NewHorizons.Builder.Volumes var go = GeneralPropBuilder.MakeNew("DayNightAudioVolume", planetGO, sector, info); go.layer = Layer.AdvancedEffectVolume; - var audioVolume = go.AddComponent(); + var audioVolume = PriorityVolumeBuilder.MakeExisting(go, planetGO, sector, info); + audioVolume.sunName = info.sun; audioVolume.dayWindow = info.dayWindow; audioVolume.dayAudio = info.dayAudio; @@ -24,13 +25,7 @@ namespace NewHorizons.Builder.Volumes audioVolume.volume = info.volume; audioVolume.SetTrack(info.track.ConvertToOW()); - var shape = go.AddComponent(); - shape.radius = info.radius; - - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._shape = shape; - - go.SetActive(true); + audioVolume.gameObject.SetActive(true); return audioVolume; } diff --git a/NewHorizons/Builder/Volumes/DestructionVolumeBuilder.cs b/NewHorizons/Builder/Volumes/DestructionVolumeBuilder.cs index ad93ad7f..d633d27c 100644 --- a/NewHorizons/Builder/Volumes/DestructionVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/DestructionVolumeBuilder.cs @@ -12,6 +12,8 @@ namespace NewHorizons.Builder.Volumes volume._deathType = EnumUtils.Parse(info.deathType.ToString(), DeathType.Default); + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/FluidVolumeBuilder.cs b/NewHorizons/Builder/Volumes/FluidVolumeBuilder.cs index e2839bd3..70b6d399 100644 --- a/NewHorizons/Builder/Volumes/FluidVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/FluidVolumeBuilder.cs @@ -35,6 +35,8 @@ namespace NewHorizons.Builder.Volumes volume._allowShipAutoroll = info.allowShipAutoroll; volume._disableOnStart = info.disableOnStart; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/ForceVolumeBuilder.cs b/NewHorizons/Builder/Volumes/ForceVolumeBuilder.cs new file mode 100644 index 00000000..ebc5c360 --- /dev/null +++ b/NewHorizons/Builder/Volumes/ForceVolumeBuilder.cs @@ -0,0 +1,111 @@ +using NewHorizons.Builder.Props; +using NewHorizons.External; +using NewHorizons.External.Modules; +using NewHorizons.External.Modules.Volumes.VolumeInfos; +using NewHorizons.Utility.OuterWilds; +using OWML.Common; +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 ForceVolumeBuilder + { + public static CylindricalForceVolume Make(GameObject planetGO, Sector sector, CylindricalForceVolumeInfo info) + { + var forceVolume = Make(planetGO, sector, info); + + forceVolume._acceleration = info.force; + forceVolume._localAxis = info.normal ?? Vector3.up; + forceVolume._playGravityCrystalAudio = info.playGravityCrystalAudio; + + forceVolume.gameObject.SetActive(true); + + return forceVolume; + } + + public static DirectionalForceVolume Make(GameObject planetGO, Sector sector, DirectionalForceVolumeInfo info) + { + var forceVolume = Make(planetGO, sector, info); + + forceVolume._fieldDirection = info.normal ?? Vector3.up; + forceVolume._fieldMagnitude = info.force; + forceVolume._affectsAlignment = info.affectsAlignment; + forceVolume._offsetCentripetalForce = info.offsetCentripetalForce; + forceVolume._playGravityCrystalAudio = info.playGravityCrystalAudio; + + forceVolume.gameObject.SetActive(true); + + return forceVolume; + } + + public static GravityVolume Make(GameObject planetGO, Sector sector, GravityVolumeInfo info) + { + var forceVolume = Make(planetGO, sector, info); + + forceVolume._isPlanetGravityVolume = false; + forceVolume._setMass = false; + forceVolume._surfaceAcceleration = info.force; + forceVolume._upperSurfaceRadius = info.upperRadius; + forceVolume._lowerSurfaceRadius = info.lowerRadius; + forceVolume._cutoffAcceleration = info.minForce; + forceVolume._cutoffRadius = info.minRadius; + forceVolume._alignmentRadius = info.alignmentRadius ?? info.upperRadius * 1.5f; + forceVolume._falloffType = info.fallOff switch + { + GravityFallOff.Linear => GravityVolume.FalloffType.linear, + GravityFallOff.InverseSquared => GravityVolume.FalloffType.inverseSquared, + _ => throw new NotImplementedException(), + }; + + forceVolume.gameObject.SetActive(true); + + return forceVolume; + } + + public static PolarForceVolume Make(GameObject planetGO, Sector sector, PolarForceVolumeInfo info) + { + var forceVolume = Make(planetGO, sector, info); + + forceVolume._acceleration = info.force; + forceVolume._localAxis = info.normal ?? Vector3.up; + forceVolume._fieldMode = info.tangential ? PolarForceVolume.ForceMode.Tangential : PolarForceVolume.ForceMode.Polar; + + forceVolume.gameObject.SetActive(true); + + return forceVolume; + } + + public static RadialForceVolume Make(GameObject planetGO, Sector sector, RadialForceVolumeInfo info) + { + var forceVolume = Make(planetGO, sector, info); + + forceVolume._acceleration = info.force; + forceVolume._falloff = info.fallOff switch + { + RadialForceVolumeInfo.FallOff.Constant => RadialForceVolume.Falloff.Constant, + RadialForceVolumeInfo.FallOff.Linear => RadialForceVolume.Falloff.Linear, + RadialForceVolumeInfo.FallOff.InverseSquared => RadialForceVolume.Falloff.InvSqr, + _ => throw new NotImplementedException(), + }; + + forceVolume.gameObject.SetActive(true); + + return forceVolume; + } + + public static TVolume Make(GameObject planetGO, Sector sector, ForceVolumeInfo info) where TVolume : ForceVolume + { + var forceVolume = PriorityVolumeBuilder.Make(planetGO, sector, info); + + forceVolume._alignmentPriority = info.alignmentPriority; + forceVolume._inheritable = info.inheritable; + + return forceVolume; + } + } +} diff --git a/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs b/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs index f26f0c2e..1d5fe013 100644 --- a/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs @@ -3,6 +3,7 @@ using NewHorizons.External.Modules.Volumes.VolumeInfos; using NewHorizons.Utility.OuterWilds; using OWML.Common; using OWML.Utils; +using System; using System.Linq; using UnityEngine; @@ -13,35 +14,28 @@ namespace NewHorizons.Builder.Volumes public static HazardVolume Make(GameObject planetGO, Sector sector, OWRigidbody owrb, HazardVolumeInfo info, IModBehaviour mod) { var go = GeneralPropBuilder.MakeNew("HazardVolume", planetGO, sector, info); - go.layer = Layer.BasicEffectVolume; - - var shape = go.AddComponent(); - shape.radius = info.radius; - - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._shape = shape; - - var volume = AddHazardVolume(go, sector, owrb, info.type, info.firstContactDamageType, info.firstContactDamage, info.damagePerSecond); + + var volume = MakeExisting(go, planetGO, sector, owrb, info); go.SetActive(true); return volume; } - public static HazardVolume AddHazardVolume(GameObject go, Sector sector, OWRigidbody owrb, HazardVolumeInfo.HazardType? type, HazardVolumeInfo.InstantDamageType? firstContactDamageType, float firstContactDamage, float damagePerSecond) + public static HazardVolume MakeExisting(GameObject go, GameObject planetGO, Sector sector, OWRigidbody owrb, HazardVolumeInfo info) { HazardVolume hazardVolume = null; - if (type == HazardVolumeInfo.HazardType.RIVERHEAT) + if (info.type == HazardVolumeInfo.HazardType.RIVERHEAT) { - hazardVolume = go.AddComponent(); + hazardVolume = VolumeBuilder.MakeExisting(go, planetGO, sector, info); } - else if (type == HazardVolumeInfo.HazardType.HEAT) + else if (info.type == HazardVolumeInfo.HazardType.HEAT) { - hazardVolume = go.AddComponent(); + hazardVolume = VolumeBuilder.MakeExisting(go, planetGO, sector, info); } - else if (type == HazardVolumeInfo.HazardType.DARKMATTER) + else if (info.type == HazardVolumeInfo.HazardType.DARKMATTER) { - hazardVolume = go.AddComponent(); + hazardVolume = VolumeBuilder.MakeExisting(go, planetGO, sector, info); var visorFrostEffectVolume = go.AddComponent(); visorFrostEffectVolume._frostRate = 0.5f; visorFrostEffectVolume._maxFrost = 0.91f; @@ -67,28 +61,38 @@ namespace NewHorizons.Builder.Volumes submerge._fluidDetector = detector; } } - else if (type == HazardVolumeInfo.HazardType.ELECTRICITY) + else if (info.type == HazardVolumeInfo.HazardType.ELECTRICITY) { - var electricityVolume = go.AddComponent(); + var electricityVolume = VolumeBuilder.MakeExisting(go, planetGO, sector, info); electricityVolume._shockAudioPool = new OWAudioSource[0]; hazardVolume = electricityVolume; } else { var simpleHazardVolume = go.AddComponent(); - simpleHazardVolume._type = EnumUtils.Parse(type.ToString(), HazardVolume.HazardType.GENERAL); + simpleHazardVolume._type = EnumUtils.Parse(info.type.ToString(), HazardVolume.HazardType.GENERAL); hazardVolume = simpleHazardVolume; } hazardVolume._attachedBody = owrb; - hazardVolume._damagePerSecond = type == null ? 0f : damagePerSecond; + hazardVolume._damagePerSecond = info.type == HazardVolumeInfo.HazardType.NONE ? 0f : info.damagePerSecond; - if (firstContactDamageType != null) - { - hazardVolume._firstContactDamageType = EnumUtils.Parse(firstContactDamageType.ToString(), InstantDamageType.Impact); - hazardVolume._firstContactDamage = firstContactDamage; - } + hazardVolume._firstContactDamageType = EnumUtils.Parse(info.firstContactDamageType.ToString(), InstantDamageType.Impact); + hazardVolume._firstContactDamage = info.firstContactDamage; return hazardVolume; } + + public static HazardVolume AddHazardVolume(GameObject go, Sector sector, OWRigidbody owrb, HazardVolumeInfo.HazardType? type, HazardVolumeInfo.InstantDamageType? firstContactDamageType, float firstContactDamage, float damagePerSecond) + { + var planetGO = sector.transform.root.gameObject; + return MakeExisting(go, planetGO, sector, owrb, new HazardVolumeInfo + { + radius = 0f, // Volume builder should skip creating an extra trigger volume and collider if radius is 0 + type = type ?? HazardVolumeInfo.HazardType.NONE, + firstContactDamageType = firstContactDamageType ?? HazardVolumeInfo.InstantDamageType.Impact, + firstContactDamage = firstContactDamage, + damagePerSecond = damagePerSecond + }); + } } } diff --git a/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs b/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs index 18f51e69..bf4bbe3f 100644 --- a/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs @@ -11,21 +11,16 @@ namespace NewHorizons.Builder.Volumes { public static NHNotificationVolume Make(GameObject planetGO, Sector sector, NotificationVolumeInfo info, IModBehaviour mod) { - var go = GeneralPropBuilder.MakeNew("NotificationVolume", planetGO, sector, info); - go.layer = Layer.BasicEffectVolume; + var notificationVolume = VolumeBuilder.Make(planetGO, sector, info); - var shape = go.AddComponent(); - shape.radius = info.radius; + // Preserving name for backwards compatibility + notificationVolume.gameObject.name = string.IsNullOrEmpty(info.rename) ? "NotificationVolume" : info.rename; - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._shape = shape; - - var notificationVolume = go.AddComponent(); notificationVolume.SetTarget(info.target); if (info.entryNotification != null) notificationVolume.SetEntryNotification(info.entryNotification.displayMessage, info.entryNotification.duration); if (info.exitNotification != null) notificationVolume.SetExitNotification(info.exitNotification.displayMessage, info.exitNotification.duration); - go.SetActive(true); + notificationVolume.gameObject.SetActive(true); return notificationVolume; } diff --git a/NewHorizons/Builder/Volumes/OxygenVolumeBuilder.cs b/NewHorizons/Builder/Volumes/OxygenVolumeBuilder.cs index 3fa34dfe..bcd1aa77 100644 --- a/NewHorizons/Builder/Volumes/OxygenVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/OxygenVolumeBuilder.cs @@ -12,6 +12,8 @@ namespace NewHorizons.Builder.Volumes volume._treeVolume = info.treeVolume; volume._playRefillAudio = info.playRefillAudio; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/PriorityVolumeBuilder.cs b/NewHorizons/Builder/Volumes/PriorityVolumeBuilder.cs index f3cce567..fc76910f 100644 --- a/NewHorizons/Builder/Volumes/PriorityVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/PriorityVolumeBuilder.cs @@ -5,6 +5,17 @@ namespace NewHorizons.Builder.Volumes { public static class PriorityVolumeBuilder { + public static TVolume MakeExisting(GameObject go, GameObject planetGO, Sector sector, PriorityVolumeInfo info) where TVolume : PriorityVolume + { + var volume = VolumeBuilder.MakeExisting(go, planetGO, sector, info); + + volume._layer = info.layer; + volume.SetPriority(info.priority); + + return volume; + } + + public static TVolume Make(GameObject planetGO, Sector sector, PriorityVolumeInfo info) where TVolume : PriorityVolume { var volume = VolumeBuilder.Make(planetGO, sector, info); diff --git a/NewHorizons/Builder/Volumes/Rulesets/PlayerImpactRulesetBuilder.cs b/NewHorizons/Builder/Volumes/Rulesets/PlayerImpactRulesetBuilder.cs index f892ea07..988e9da8 100644 --- a/NewHorizons/Builder/Volumes/Rulesets/PlayerImpactRulesetBuilder.cs +++ b/NewHorizons/Builder/Volumes/Rulesets/PlayerImpactRulesetBuilder.cs @@ -12,6 +12,8 @@ namespace NewHorizons.Builder.Volumes.Rulesets volume.minImpactSpeed = info.minImpactSpeed; volume.maxImpactSpeed = info.maxImpactSpeed; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/Rulesets/ProbeRulesetBuilder.cs b/NewHorizons/Builder/Volumes/Rulesets/ProbeRulesetBuilder.cs index 98823ffa..b0768324 100644 --- a/NewHorizons/Builder/Volumes/Rulesets/ProbeRulesetBuilder.cs +++ b/NewHorizons/Builder/Volumes/Rulesets/ProbeRulesetBuilder.cs @@ -15,6 +15,8 @@ namespace NewHorizons.Builder.Volumes.Rulesets volume._lanternRange = info.lanternRange; volume._ignoreAnchor = info.ignoreAnchor; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/Rulesets/ThrustRulesetBuilder.cs b/NewHorizons/Builder/Volumes/Rulesets/ThrustRulesetBuilder.cs index 8a49808d..5e158ecd 100644 --- a/NewHorizons/Builder/Volumes/Rulesets/ThrustRulesetBuilder.cs +++ b/NewHorizons/Builder/Volumes/Rulesets/ThrustRulesetBuilder.cs @@ -13,6 +13,8 @@ namespace NewHorizons.Builder.Volumes.Rulesets volume._nerfJetpackBooster = info.nerfJetpackBooster; volume._nerfDuration = info.nerfDuration; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/SpeedLimiterVolumeBuilder.cs b/NewHorizons/Builder/Volumes/SpeedLimiterVolumeBuilder.cs index d28b0a24..0bd94b87 100644 --- a/NewHorizons/Builder/Volumes/SpeedLimiterVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/SpeedLimiterVolumeBuilder.cs @@ -14,6 +14,8 @@ namespace NewHorizons.Builder.Volumes volume.stoppingDistance = info.stoppingDistance; volume.maxEntryAngle = info.maxEntryAngle; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/SpeedTrapVolumeBuilder.cs b/NewHorizons/Builder/Volumes/SpeedTrapVolumeBuilder.cs index fa1ed9bc..1b9319ed 100644 --- a/NewHorizons/Builder/Volumes/SpeedTrapVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/SpeedTrapVolumeBuilder.cs @@ -12,6 +12,8 @@ namespace NewHorizons.Builder.Volumes volume._speedLimit = info.speedLimit; volume._acceleration = info.acceleration; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/VanishVolumeBuilder.cs b/NewHorizons/Builder/Volumes/VanishVolumeBuilder.cs index 9bc2a514..4b312a60 100644 --- a/NewHorizons/Builder/Volumes/VanishVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/VanishVolumeBuilder.cs @@ -1,6 +1,4 @@ -using NewHorizons.Builder.Props; using NewHorizons.External.Modules.Volumes.VolumeInfos; -using NewHorizons.Utility.OuterWilds; using UnityEngine; namespace NewHorizons.Builder.Volumes @@ -9,27 +7,13 @@ namespace NewHorizons.Builder.Volumes { public static TVolume Make(GameObject planetGO, Sector sector, VanishVolumeInfo info) where TVolume : VanishVolume { - var go = GeneralPropBuilder.MakeNew(typeof(TVolume).Name, planetGO, sector, info); - go.layer = Layer.BasicEffectVolume; - - var collider = go.AddComponent(); - collider.isTrigger = true; - collider.radius = info.radius; - - var owCollider = go.AddComponent(); - owCollider._collider = collider; - - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._owCollider = owCollider; - - var volume = go.AddComponent(); + var volume = VolumeBuilder.Make(planetGO, sector, info); + var collider = volume.gameObject.GetComponent(); volume._collider = collider; volume._shrinkBodies = info.shrinkBodies; volume._onlyAffectsPlayerAndShip = info.onlyAffectsPlayerRelatedBodies; - go.SetActive(true); - return volume; } } diff --git a/NewHorizons/Builder/Volumes/VisorEffects/VisorFrostEffectVolumeBuilder.cs b/NewHorizons/Builder/Volumes/VisorEffects/VisorFrostEffectVolumeBuilder.cs index 793d72b4..c33886e2 100644 --- a/NewHorizons/Builder/Volumes/VisorEffects/VisorFrostEffectVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/VisorEffects/VisorFrostEffectVolumeBuilder.cs @@ -12,6 +12,8 @@ namespace NewHorizons.Builder.Volumes.VisorEffects volume._frostRate = info.frostRate; volume._maxFrost = info.maxFrost; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/VisorEffects/VisorRainEffectVolumeBuilder.cs b/NewHorizons/Builder/Volumes/VisorEffects/VisorRainEffectVolumeBuilder.cs index e07c95e6..6f7c9c01 100644 --- a/NewHorizons/Builder/Volumes/VisorEffects/VisorRainEffectVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/VisorEffects/VisorRainEffectVolumeBuilder.cs @@ -13,6 +13,8 @@ namespace NewHorizons.Builder.Volumes.VisorEffects volume._dropletRate = info.dropletRate; volume._streakRate = info.streakRate; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/VolumeBuilder.cs b/NewHorizons/Builder/Volumes/VolumeBuilder.cs index 0ee5d0b7..061ea559 100644 --- a/NewHorizons/Builder/Volumes/VolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/VolumeBuilder.cs @@ -7,21 +7,36 @@ namespace NewHorizons.Builder.Volumes { public static class VolumeBuilder { + public static TVolume MakeExisting(GameObject go, GameObject planetGO, Sector sector, VolumeInfo info) where TVolume : MonoBehaviour + { + // Respect existing layer if set to a valid volume layer + if (go.layer != Layer.AdvancedEffectVolume) + { + go.layer = Layer.BasicEffectVolume; + } + + // Skip creating a trigger volume if one already exists and has a shape set and we aren't overriding it + var trigger = go.GetComponent(); + if (trigger == null || (trigger._shape == null && trigger._owCollider == null) || info.shape != null || info.radius > 0f) + { + ShapeBuilder.AddTriggerVolume(go, info.shape, info.radius); + } + + var volume = go.AddComponent(); + + return volume; + } + public static TVolume Make(GameObject planetGO, Sector sector, VolumeInfo info) where TVolume : MonoBehaviour //Could be BaseVolume but I need to create vanilla volumes too. { var go = GeneralPropBuilder.MakeNew(typeof(TVolume).Name, planetGO, sector, info); - go.layer = Layer.BasicEffectVolume; - - var shape = go.AddComponent(); - shape.radius = info.radius; - - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._shape = shape; - - var volume = go.AddComponent(); - - go.SetActive(true); + return MakeExisting(go, planetGO, sector, info); + } + public static TVolume MakeAndEnable(GameObject planetGO, Sector sector, VolumeInfo info) where TVolume : MonoBehaviour + { + var volume = Make(planetGO, sector, info); + volume.gameObject.SetActive(true); return volume; } } diff --git a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs index 7f3e36bc..161f1d6a 100644 --- a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs +++ b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs @@ -60,28 +60,28 @@ namespace NewHorizons.Builder.Volumes { foreach (var mapRestrictionVolume in config.Volumes.mapRestrictionVolumes) { - VolumeBuilder.Make(go, sector, mapRestrictionVolume); + VolumeBuilder.MakeAndEnable(go, sector, mapRestrictionVolume); } } if (config.Volumes.interferenceVolumes != null) { foreach (var interferenceVolume in config.Volumes.interferenceVolumes) { - VolumeBuilder.Make(go, sector, interferenceVolume); + VolumeBuilder.MakeAndEnable(go, sector, interferenceVolume); } } if (config.Volumes.reverbVolumes != null) { foreach (var reverbVolume in config.Volumes.reverbVolumes) { - VolumeBuilder.Make(go, sector, reverbVolume); + VolumeBuilder.MakeAndEnable(go, sector, reverbVolume); } } if (config.Volumes.insulatingVolumes != null) { foreach (var insulatingVolume in config.Volumes.insulatingVolumes) { - VolumeBuilder.Make(go, sector, insulatingVolume); + VolumeBuilder.MakeAndEnable(go, sector, insulatingVolume); } } if (config.Volumes.zeroGravityVolumes != null) @@ -112,20 +112,58 @@ namespace NewHorizons.Builder.Volumes FluidVolumeBuilder.Make(go, sector, fluidVolume); } } + if (config.Volumes.forces != null) + { + if (config.Volumes.forces.cylindricalVolumes != null) + { + foreach (var cylindricalVolume in config.Volumes.forces.cylindricalVolumes) + { + ForceVolumeBuilder.Make(go, sector, cylindricalVolume); + } + } + if (config.Volumes.forces.directionalVolumes != null) + { + foreach (var directionalVolume in config.Volumes.forces.directionalVolumes) + { + ForceVolumeBuilder.Make(go, sector, directionalVolume); + } + } + if (config.Volumes.forces.gravityVolumes != null) + { + foreach (var gravityVolume in config.Volumes.forces.gravityVolumes) + { + ForceVolumeBuilder.Make(go, sector, gravityVolume); + } + } + if (config.Volumes.forces.polarVolumes != null) + { + foreach (var polarVolume in config.Volumes.forces.polarVolumes) + { + ForceVolumeBuilder.Make(go, sector, polarVolume); + } + } + if (config.Volumes.forces.radialVolumes != null) + { + foreach (var radialVolume in config.Volumes.forces.radialVolumes) + { + ForceVolumeBuilder.Make(go, sector, radialVolume); + } + } + } if (config.Volumes.probe != null) { if (config.Volumes.probe.destructionVolumes != null) { foreach (var destructionVolume in config.Volumes.probe.destructionVolumes) { - VolumeBuilder.Make(go, sector, destructionVolume); + VolumeBuilder.MakeAndEnable(go, sector, destructionVolume); } } if (config.Volumes.probe.safetyVolumes != null) { foreach (var safetyVolume in config.Volumes.probe.safetyVolumes) { - VolumeBuilder.Make(go, sector, safetyVolume); + VolumeBuilder.MakeAndEnable(go, sector, safetyVolume); } } } @@ -152,7 +190,7 @@ namespace NewHorizons.Builder.Volumes { foreach (var antiTravelMusicRuleset in config.Volumes.rulesets.antiTravelMusicRulesets) { - VolumeBuilder.Make(go, sector, antiTravelMusicRuleset); + VolumeBuilder.MakeAndEnable(go, sector, antiTravelMusicRuleset); } } if (config.Volumes.rulesets.playerImpactRulesets != null) @@ -181,7 +219,7 @@ namespace NewHorizons.Builder.Volumes { foreach (var referenceFrameBlockerVolume in config.Volumes.referenceFrameBlockerVolumes) { - VolumeBuilder.Make(go, sector, referenceFrameBlockerVolume); + VolumeBuilder.MakeAndEnable(go, sector, referenceFrameBlockerVolume); } } if (config.Volumes.speedTrapVolumes != null) @@ -202,7 +240,7 @@ namespace NewHorizons.Builder.Volumes { foreach (var lightSourceVolume in config.Volumes.lightSourceVolumes) { - VolumeBuilder.Make(go, sector, lightSourceVolume); + VolumeBuilder.MakeAndEnable(go, sector, lightSourceVolume); } } if (config.Volumes.solarSystemVolume != null) diff --git a/NewHorizons/Builder/Volumes/ZeroGVolumeBuilder.cs b/NewHorizons/Builder/Volumes/ZeroGVolumeBuilder.cs index afdde0b8..70ecc7e1 100644 --- a/NewHorizons/Builder/Volumes/ZeroGVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/ZeroGVolumeBuilder.cs @@ -11,6 +11,8 @@ namespace NewHorizons.Builder.Volumes volume._inheritable = true; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/External/Modules/Props/ShapeInfo.cs b/NewHorizons/External/Modules/Props/ShapeInfo.cs new file mode 100644 index 00000000..19b81ecb --- /dev/null +++ b/NewHorizons/External/Modules/Props/ShapeInfo.cs @@ -0,0 +1,92 @@ +using NewHorizons.External.SerializableData; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Props +{ + [JsonObject] + public class ShapeInfo + { + /// + /// The type of shape or collider to add. Sphere, box, and capsule colliders are more performant and support collision. Defaults to sphere. + /// + public ShapeType type = ShapeType.Sphere; + + /// + /// The radius of the shape or collider. Defaults to 0.5 meters. Only used by spheres, capsules, cylinders, hemispheres, hemicapsules, and rings. + /// + public float radius = 0.5f; + + /// + /// The height of the shape or collider. Defaults to 1 meter. Only used by capsules, cylinders, cones, hemicapsules, and rings. + /// + public float height = 1f; + + /// + /// The axis that the shape or collider is aligned with. Defaults to the Y axis (up). The flat bottom of the shape will be pointing towards the negative axis. Only used by capsules, cones, hemispheres, and hemicapsules. + /// + public ColliderAxis direction = ColliderAxis.Y; + + /// + /// The inner radius of the shape. Defaults to 0 meters. Only used by cones and rings. + /// + public float innerRadius = 0f; + + /// + /// The outer radius of the shape. Defaults to 0.5 meters. Only used by cones and rings. + /// + public float outerRadius = 0.5f; + + /// + /// Whether the shape has an end cap. Defaults to true. Only used by hemispheres and hemicapsules. + /// + public bool cap = true; + + /// + /// The size of the shape or collider. Defaults to (1,1,1). Only used by boxes. + /// + public MVector3 size; + + /// + /// The offset of the shape or collider from the object's origin. Defaults to (0,0,0). Supported by all collider and shape types. + /// + public MVector3 offset; + + /// + /// Whether the collider should have collision enabled. If false, the collider will be a trigger. Defaults to false. Only supported for spheres, boxes, and capsules. + /// + public bool hasCollision = false; + + /// + /// Whether to explicitly use a shape instead of a collider. Shapes do not support collision and are less performant, but support a wider set of shapes and are required by some components. Omit this unless you explicitly want to use a sphere, box, or capsule shape instead of a collider. + /// + public bool? useShape; + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum ShapeType + { + [EnumMember(Value = @"sphere")] Sphere, + [EnumMember(Value = @"box")] Box, + [EnumMember(Value = @"capsule")] Capsule, + [EnumMember(Value = @"cylinder")] Cylinder, + [EnumMember(Value = @"cone")] Cone, + [EnumMember(Value = @"hemisphere")] Hemisphere, + [EnumMember(Value = @"hemicapsule")] Hemicapsule, + [EnumMember(Value = @"ring")] Ring, + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum ColliderAxis + { + [EnumMember(Value = @"x")] X = 0, + [EnumMember(Value = @"y")] Y = 1, + [EnumMember(Value = @"z")] Z = 2, + } +} diff --git a/NewHorizons/External/Modules/Volumes/ForceModule.cs b/NewHorizons/External/Modules/Volumes/ForceModule.cs new file mode 100644 index 00000000..8250d3e1 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/ForceModule.cs @@ -0,0 +1,40 @@ +using NewHorizons.External.Modules.Volumes.VolumeInfos; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Volumes +{ + [JsonObject] + public class ForceModule + { + /// + /// Applies a constant force along the volume's XZ plane towards the volume's center. Affects alignment. + /// + public CylindricalForceVolumeInfo[] cylindricalVolumes; + + /// + /// Applies a constant force in the direction of the volume's Y axis. May affect alignment. + /// + public DirectionalForceVolumeInfo[] directionalVolumes; + + /// + /// Applies planet-like gravity towards the volume's center with falloff by distance. May affect alignment. + /// For actual planetary body gravity, use the properties in the Base module. + /// + public GravityVolumeInfo[] gravityVolumes; + + /// + /// Applies a constant force towards the volume's center. Affects alignment. + /// + public PolarForceVolumeInfo[] polarVolumes; + + /// + /// Applies a force towards the volume's center with falloff by distance. Affects alignment. + /// + public RadialForceVolumeInfo[] radialVolumes; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/CylindricalForceVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/CylindricalForceVolumeInfo.cs new file mode 100644 index 00000000..5926056b --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/CylindricalForceVolumeInfo.cs @@ -0,0 +1,24 @@ +using NewHorizons.External.SerializableData; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Volumes.VolumeInfos +{ + [JsonObject] + public class CylindricalForceVolumeInfo : ForceVolumeInfo + { + /// + /// The direction that the force applied by this volume will be perpendicular to. Defaults to up (0, 1, 0). + /// + public MVector3 normal; + + /// + /// Whether to play the gravity crystal audio when the player is in this volume. + /// + public bool playGravityCrystalAudio; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/DirectionalForceVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/DirectionalForceVolumeInfo.cs new file mode 100644 index 00000000..82af81d2 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/DirectionalForceVolumeInfo.cs @@ -0,0 +1,35 @@ +using NewHorizons.External.SerializableData; +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 DirectionalForceVolumeInfo : ForceVolumeInfo + { + /// + /// The direction of the force applied by this volume. Defaults to up (0, 1, 0). + /// + public MVector3 normal; + + /// + /// Whether this force volume affects alignment. Defaults to true. + /// + [DefaultValue(true)] public bool affectsAlignment = true; + + /// + /// Whether the force applied by this volume takes the centripetal force of the volume's parent body into account. Defaults to false. + /// + public bool offsetCentripetalForce; + + /// + /// Whether to play the gravity crystal audio when the player is in this volume. + /// + public bool playGravityCrystalAudio; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/ForceVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/ForceVolumeInfo.cs new file mode 100644 index 00000000..e56a3ca5 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/ForceVolumeInfo.cs @@ -0,0 +1,35 @@ +using NewHorizons.External.Modules.Props; +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 ForceVolumeInfo : PriorityVolumeInfo + { + /// + /// The force applied by this volume. Can be negative to reverse the direction. + /// + public float force; + + /// + /// The priority of this force volume for the purposes of alignment. + /// + /// Volumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal. + /// Ex: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity. + /// + /// Default value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. + /// + [DefaultValue(1)] public int alignmentPriority = 1; + + /// + /// Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ. + /// + public bool inheritable; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/GravityVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/GravityVolumeInfo.cs new file mode 100644 index 00000000..44b226fc --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/GravityVolumeInfo.cs @@ -0,0 +1,44 @@ +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 GravityVolumeInfo : ForceVolumeInfo + { + /// + /// The upper bounds of the volume's "surface". Above this radius, the force applied by this volume will have falloff applied. + /// + public float upperRadius; + + /// + /// The lower bounds of the volume's "surface". Above this radius and below the `upperRadius`, the force applied by this volume will be constant. Defaults to 0. + /// + [DefaultValue(0f)] public float lowerRadius; + + /// + /// The volume's force will decrease linearly from `force` to `minForce` as distance decreases from `lowerRadius` to `minRadius`. Defaults to 0. + /// + [DefaultValue(0f)] public float minRadius; + + /// + /// The minimum force applied by this volume between `lowerRadius` and `minRadius`. Defaults to 0. + /// + [DefaultValue(0f)] public float minForce; + + /// + /// How the force falls off with distance. Most planets use linear but the sun and some moons use inverseSquared. + /// + [DefaultValue("linear")] public GravityFallOff fallOff = GravityFallOff.Linear; + + /// + /// The radius where objects will be aligned to the volume's force. Defaults to 1.5x the `upperRadius`. Set to 0 to disable alignment. + /// + public float? alignmentRadius; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/PolarForceVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/PolarForceVolumeInfo.cs new file mode 100644 index 00000000..bdf1ee0d --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/PolarForceVolumeInfo.cs @@ -0,0 +1,23 @@ +using NewHorizons.External.SerializableData; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Volumes.VolumeInfos +{ + [JsonObject] + public class PolarForceVolumeInfo : ForceVolumeInfo + { + /// + /// Tangential mode only. The force applied by this volume will be perpendicular to this direction and the direction to the other body. Defaults to up (0, 1, 0). + /// + public MVector3 normal; + /// + /// Enables tangential mode. The force applied by this volume will be perpendicular to the normal and the direction to the other body. Defaults to false. + /// + public bool tangential; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/RadialForceVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/RadialForceVolumeInfo.cs new file mode 100644 index 00000000..eefe58e4 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/RadialForceVolumeInfo.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Volumes.VolumeInfos +{ + [JsonObject] + public class RadialForceVolumeInfo : ForceVolumeInfo + { + /// + /// How the force falls off with distance. Defaults to linear. + /// + [DefaultValue("linear")] public FallOff fallOff = FallOff.Linear; + + [JsonConverter(typeof(StringEnumConverter))] + public enum FallOff + { + [EnumMember(Value = @"constant")] Constant = 0, + + [EnumMember(Value = @"linear")] Linear = 1, + + [EnumMember(Value = @"inverseSquared")] + InverseSquared = 2 + } + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/RevealVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/RevealVolumeInfo.cs index 2f577918..49a917c7 100644 --- a/NewHorizons/External/Modules/Volumes/VolumeInfos/RevealVolumeInfo.cs +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/RevealVolumeInfo.cs @@ -29,7 +29,7 @@ namespace NewHorizons.External.Modules.Volumes.VolumeInfos } /// - /// The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only) + /// The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only). This will effectively be a cone extending from the volume's center forwards (along the Z axis) based on the volume's rotation. /// [DefaultValue(180f)] public float maxAngle = 180f; // Observe Only diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/VolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/VolumeInfo.cs index 7b106614..94f7a41b 100644 --- a/NewHorizons/External/Modules/Volumes/VolumeInfos/VolumeInfo.cs +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/VolumeInfo.cs @@ -1,14 +1,20 @@ +using NewHorizons.External.Modules.Props; using Newtonsoft.Json; using System.ComponentModel; namespace NewHorizons.External.Modules.Volumes.VolumeInfos { [JsonObject] - public class VolumeInfo : GeneralPointPropInfo + public class VolumeInfo : GeneralPropInfo { /// - /// The radius of this volume. + /// The radius of this volume, if a shape is not specified. /// [DefaultValue(1f)] public float radius = 1f; + + /// + /// The shape of this volume. Defaults to a sphere with a radius of `radius` if not specified. + /// + public ShapeInfo shape; } } diff --git a/NewHorizons/External/Modules/Volumes/VolumesModule.cs b/NewHorizons/External/Modules/Volumes/VolumesModule.cs index e9791a51..34795d15 100644 --- a/NewHorizons/External/Modules/Volumes/VolumesModule.cs +++ b/NewHorizons/External/Modules/Volumes/VolumesModule.cs @@ -27,6 +27,11 @@ namespace NewHorizons.External.Modules.Volumes /// public FluidVolumeInfo[] fluidVolumes; + /// + /// Add force volumes to this planet. + /// + public ForceModule forces; + /// /// Add hazard volumes to this planet. /// Causes damage to player when inside this volume. diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index 19669bff..27da29db 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -5386,6 +5386,10 @@ "$ref": "#/definitions/FluidVolumeInfo" } }, + "forces": { + "description": "Add force volumes to this planet.", + "$ref": "#/definitions/ForceModule" + }, "hazardVolumes": { "type": "array", "description": "Add hazard volumes to this planet.\nCauses damage to player when inside this volume.", @@ -5523,10 +5527,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5589,6 +5608,101 @@ } } }, + "ShapeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "description": "The type of shape or collider to add. Sphere, box, and capsule colliders are more performant and support collision. Defaults to sphere.", + "$ref": "#/definitions/ShapeType" + }, + "radius": { + "type": "number", + "description": "The radius of the shape or collider. Defaults to 0.5 meters. Only used by spheres, capsules, cylinders, hemispheres, hemicapsules, and rings.", + "format": "float" + }, + "height": { + "type": "number", + "description": "The height of the shape or collider. Defaults to 1 meter. Only used by capsules, cylinders, cones, hemicapsules, and rings.", + "format": "float" + }, + "direction": { + "description": "The axis that the shape or collider is aligned with. Defaults to the Y axis (up). The flat bottom of the shape will be pointing towards the negative axis. Only used by capsules, cones, hemispheres, and hemicapsules.", + "$ref": "#/definitions/ColliderAxis" + }, + "innerRadius": { + "type": "number", + "description": "The inner radius of the shape. Defaults to 0 meters. Only used by cones and rings.", + "format": "float" + }, + "outerRadius": { + "type": "number", + "description": "The outer radius of the shape. Defaults to 0.5 meters. Only used by cones and rings.", + "format": "float" + }, + "cap": { + "type": "boolean", + "description": "Whether the shape has an end cap. Defaults to true. Only used by hemispheres and hemicapsules." + }, + "size": { + "description": "The size of the shape or collider. Defaults to (1,1,1). Only used by boxes.", + "$ref": "#/definitions/MVector3" + }, + "offset": { + "description": "The offset of the shape or collider from the object's origin. Defaults to (0,0,0). Supported by all collider and shape types.", + "$ref": "#/definitions/MVector3" + }, + "hasCollision": { + "type": "boolean", + "description": "Whether the collider should have collision enabled. If false, the collider will be a trigger. Defaults to false. Only supported for spheres, boxes, and capsules." + }, + "useShape": { + "type": [ + "boolean", + "null" + ], + "description": "Whether to explicitly use a shape instead of a collider. Shapes do not support collision and are less performant, but support a wider set of shapes and are required by some components. Omit this unless you explicitly want to use a sphere, box, or capsule shape instead of a collider." + } + } + }, + "ShapeType": { + "type": "string", + "description": "", + "x-enumNames": [ + "Sphere", + "Box", + "Capsule", + "Cylinder", + "Cone", + "Hemisphere", + "Hemicapsule", + "Ring" + ], + "enum": [ + "sphere", + "box", + "capsule", + "cylinder", + "cone", + "hemisphere", + "hemicapsule", + "ring" + ] + }, + "ColliderAxis": { + "type": "string", + "description": "", + "x-enumNames": [ + "X", + "Y", + "Z" + ], + "enum": [ + "x", + "y", + "z" + ] + }, "NHClipSelectionType": { "type": "string", "description": "", @@ -5621,10 +5735,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5691,10 +5820,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5774,10 +5918,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5819,16 +5978,510 @@ } } }, + "ForceModule": { + "type": "object", + "additionalProperties": false, + "properties": { + "cylindricalVolumes": { + "type": "array", + "description": "Applies a constant force along the volume's XZ plane towards the volume's center. Affects alignment.", + "items": { + "$ref": "#/definitions/CylindricalForceVolumeInfo" + } + }, + "directionalVolumes": { + "type": "array", + "description": "Applies a constant force in the direction of the volume's Y axis. May affect alignment.", + "items": { + "$ref": "#/definitions/DirectionalForceVolumeInfo" + } + }, + "gravityVolumes": { + "type": "array", + "description": "Applies planet-like gravity towards the volume's center with falloff by distance. May affect alignment.\nFor actual planetary body gravity, use the properties in the Base module.", + "items": { + "$ref": "#/definitions/GravityVolumeInfo" + } + }, + "polarVolumes": { + "type": "array", + "description": "Applies a constant force towards the volume's center. Affects alignment.", + "items": { + "$ref": "#/definitions/PolarForceVolumeInfo" + } + }, + "radialVolumes": { + "type": "array", + "description": "Applies a force towards the volume's center with falloff by distance. Affects alignment.", + "items": { + "$ref": "#/definitions/RadialForceVolumeInfo" + } + } + } + }, + "CylindricalForceVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "force": { + "type": "number", + "description": "The force applied by this volume. Can be negative to reverse the direction.", + "format": "float" + }, + "alignmentPriority": { + "type": "integer", + "description": "The priority of this force volume for the purposes of alignment.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "inheritable": { + "type": "boolean", + "description": "Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ." + }, + "layer": { + "type": "integer", + "description": "The layer of this volume.\n\nLayers separate the priority system. The priority of volumes in one layer will not affect or override volumes in another. The highest priority volume in each layer will stack like normal.\nThe exception is layer 0. A higher-priority volume in layer 0 will override lower-priority volumes in ALL other layers. A lower-priority volume in layer 0 will stack with other layers like normal.\n \nEx: A player could be affected by the sun on layer 9 priority 0 and planet gravity on layer 3 priority 2. They would experience the gravity of both volumes since they are on different layers.\nIf there was a zero-g volume on layer 0 priority 1, since it is on layer 0 it will override the gravity from the sun (priority 0 which is less than 1) but they will still feel the \ngravity of the planet (priority 2 is greater than 1). The zero-g volume will also still be applied because it is on a different layer.\n \nDefault value here is 0 which means this volume's priority will be evaluated against all other priority volumes regardless of their layer.", + "format": "int32", + "default": 0 + }, + "priority": { + "type": "integer", + "description": "The priority of this volume.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "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" + }, + "normal": { + "description": "The direction that the force applied by this volume will be perpendicular to. Defaults to up (0, 1, 0).", + "$ref": "#/definitions/MVector3" + }, + "playGravityCrystalAudio": { + "type": "boolean", + "description": "Whether to play the gravity crystal audio when the player is in this volume." + } + } + }, + "DirectionalForceVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "force": { + "type": "number", + "description": "The force applied by this volume. Can be negative to reverse the direction.", + "format": "float" + }, + "alignmentPriority": { + "type": "integer", + "description": "The priority of this force volume for the purposes of alignment.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "inheritable": { + "type": "boolean", + "description": "Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ." + }, + "layer": { + "type": "integer", + "description": "The layer of this volume.\n\nLayers separate the priority system. The priority of volumes in one layer will not affect or override volumes in another. The highest priority volume in each layer will stack like normal.\nThe exception is layer 0. A higher-priority volume in layer 0 will override lower-priority volumes in ALL other layers. A lower-priority volume in layer 0 will stack with other layers like normal.\n \nEx: A player could be affected by the sun on layer 9 priority 0 and planet gravity on layer 3 priority 2. They would experience the gravity of both volumes since they are on different layers.\nIf there was a zero-g volume on layer 0 priority 1, since it is on layer 0 it will override the gravity from the sun (priority 0 which is less than 1) but they will still feel the \ngravity of the planet (priority 2 is greater than 1). The zero-g volume will also still be applied because it is on a different layer.\n \nDefault value here is 0 which means this volume's priority will be evaluated against all other priority volumes regardless of their layer.", + "format": "int32", + "default": 0 + }, + "priority": { + "type": "integer", + "description": "The priority of this volume.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "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" + }, + "normal": { + "description": "The direction of the force applied by this volume. Defaults to up (0, 1, 0).", + "$ref": "#/definitions/MVector3" + }, + "affectsAlignment": { + "type": "boolean", + "description": "Whether this force volume affects alignment. Defaults to true.", + "default": true + }, + "offsetCentripetalForce": { + "type": "boolean", + "description": "Whether the force applied by this volume takes the centripetal force of the volume's parent body into account. Defaults to false." + }, + "playGravityCrystalAudio": { + "type": "boolean", + "description": "Whether to play the gravity crystal audio when the player is in this volume." + } + } + }, + "GravityVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "force": { + "type": "number", + "description": "The force applied by this volume. Can be negative to reverse the direction.", + "format": "float" + }, + "alignmentPriority": { + "type": "integer", + "description": "The priority of this force volume for the purposes of alignment.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "inheritable": { + "type": "boolean", + "description": "Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ." + }, + "layer": { + "type": "integer", + "description": "The layer of this volume.\n\nLayers separate the priority system. The priority of volumes in one layer will not affect or override volumes in another. The highest priority volume in each layer will stack like normal.\nThe exception is layer 0. A higher-priority volume in layer 0 will override lower-priority volumes in ALL other layers. A lower-priority volume in layer 0 will stack with other layers like normal.\n \nEx: A player could be affected by the sun on layer 9 priority 0 and planet gravity on layer 3 priority 2. They would experience the gravity of both volumes since they are on different layers.\nIf there was a zero-g volume on layer 0 priority 1, since it is on layer 0 it will override the gravity from the sun (priority 0 which is less than 1) but they will still feel the \ngravity of the planet (priority 2 is greater than 1). The zero-g volume will also still be applied because it is on a different layer.\n \nDefault value here is 0 which means this volume's priority will be evaluated against all other priority volumes regardless of their layer.", + "format": "int32", + "default": 0 + }, + "priority": { + "type": "integer", + "description": "The priority of this volume.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "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" + }, + "upperRadius": { + "type": "number", + "description": "The upper bounds of the volume's \"surface\". Above this radius, the force applied by this volume will have falloff applied.", + "format": "float" + }, + "lowerRadius": { + "type": "number", + "description": "The lower bounds of the volume's \"surface\". Above this radius and below the `upperRadius`, the force applied by this volume will be constant. Defaults to 0.", + "format": "float", + "default": 0.0 + }, + "minRadius": { + "type": "number", + "description": "The volume's force will decrease linearly from `force` to `minForce` as distance decreases from `lowerRadius` to `minRadius`. Defaults to 0.", + "format": "float", + "default": 0.0 + }, + "minForce": { + "type": "number", + "description": "The minimum force applied by this volume between `lowerRadius` and `minRadius`. Defaults to 0.", + "format": "float", + "default": 0.0 + }, + "fallOff": { + "description": "How the force falls off with distance. Most planets use linear but the sun and some moons use inverseSquared.", + "default": "linear", + "$ref": "#/definitions/GravityFallOff" + }, + "alignmentRadius": { + "type": [ + "null", + "number" + ], + "description": "The radius where objects will be aligned to the volume's force. Defaults to 1.5x the `upperRadius`. Set to 0 to disable alignment.", + "format": "float" + } + } + }, + "PolarForceVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "force": { + "type": "number", + "description": "The force applied by this volume. Can be negative to reverse the direction.", + "format": "float" + }, + "alignmentPriority": { + "type": "integer", + "description": "The priority of this force volume for the purposes of alignment.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "inheritable": { + "type": "boolean", + "description": "Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ." + }, + "layer": { + "type": "integer", + "description": "The layer of this volume.\n\nLayers separate the priority system. The priority of volumes in one layer will not affect or override volumes in another. The highest priority volume in each layer will stack like normal.\nThe exception is layer 0. A higher-priority volume in layer 0 will override lower-priority volumes in ALL other layers. A lower-priority volume in layer 0 will stack with other layers like normal.\n \nEx: A player could be affected by the sun on layer 9 priority 0 and planet gravity on layer 3 priority 2. They would experience the gravity of both volumes since they are on different layers.\nIf there was a zero-g volume on layer 0 priority 1, since it is on layer 0 it will override the gravity from the sun (priority 0 which is less than 1) but they will still feel the \ngravity of the planet (priority 2 is greater than 1). The zero-g volume will also still be applied because it is on a different layer.\n \nDefault value here is 0 which means this volume's priority will be evaluated against all other priority volumes regardless of their layer.", + "format": "int32", + "default": 0 + }, + "priority": { + "type": "integer", + "description": "The priority of this volume.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "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" + }, + "normal": { + "description": "Tangential mode only. The force applied by this volume will be perpendicular to this direction and the direction to the other body. Defaults to up (0, 1, 0).", + "$ref": "#/definitions/MVector3" + }, + "tangential": { + "type": "boolean", + "description": "Enables tangential mode. The force applied by this volume will be perpendicular to the normal and the direction to the other body. Defaults to false." + } + } + }, + "RadialForceVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "force": { + "type": "number", + "description": "The force applied by this volume. Can be negative to reverse the direction.", + "format": "float" + }, + "alignmentPriority": { + "type": "integer", + "description": "The priority of this force volume for the purposes of alignment.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "inheritable": { + "type": "boolean", + "description": "Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ." + }, + "layer": { + "type": "integer", + "description": "The layer of this volume.\n\nLayers separate the priority system. The priority of volumes in one layer will not affect or override volumes in another. The highest priority volume in each layer will stack like normal.\nThe exception is layer 0. A higher-priority volume in layer 0 will override lower-priority volumes in ALL other layers. A lower-priority volume in layer 0 will stack with other layers like normal.\n \nEx: A player could be affected by the sun on layer 9 priority 0 and planet gravity on layer 3 priority 2. They would experience the gravity of both volumes since they are on different layers.\nIf there was a zero-g volume on layer 0 priority 1, since it is on layer 0 it will override the gravity from the sun (priority 0 which is less than 1) but they will still feel the \ngravity of the planet (priority 2 is greater than 1). The zero-g volume will also still be applied because it is on a different layer.\n \nDefault value here is 0 which means this volume's priority will be evaluated against all other priority volumes regardless of their layer.", + "format": "int32", + "default": 0 + }, + "priority": { + "type": "integer", + "description": "The priority of this volume.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "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" + }, + "fallOff": { + "description": "How the force falls off with distance. Defaults to linear.", + "default": "linear", + "$ref": "#/definitions/FallOff" + } + } + }, + "FallOff": { + "type": "string", + "description": "", + "x-enumNames": [ + "Constant", + "Linear", + "InverseSquared" + ], + "enum": [ + "constant", + "linear", + "inverseSquared" + ] + }, "HazardVolumeInfo": { "type": "object", "additionalProperties": false, "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5872,6 +6525,17 @@ "type": "object", "additionalProperties": false, "properties": { + "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" @@ -5890,9 +6554,13 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" } } }, @@ -5902,10 +6570,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5973,10 +6656,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6030,10 +6728,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6052,7 +6765,7 @@ }, "maxAngle": { "type": "number", - "description": "The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only)", + "description": "The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only). This will effectively be a cone extending from the volume's center forwards (along the Z axis) based on the volume's rotation.", "format": "float", "default": 180.0 }, @@ -6153,10 +6866,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6193,10 +6921,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6243,10 +6986,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6287,10 +7045,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6327,10 +7100,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6405,10 +7193,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6459,10 +7262,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6499,10 +7317,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6539,10 +7372,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6576,10 +7424,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" diff --git a/docs/src/content/docs/guides/volumes.md b/docs/src/content/docs/guides/volumes.md new file mode 100644 index 00000000..192b2180 --- /dev/null +++ b/docs/src/content/docs/guides/volumes.md @@ -0,0 +1,72 @@ +--- +title: Volumes +description: Guide to making volumes in New Horizons +--- + +Volumes are invisible 3D "zones" or "triggers" that cause various effects when objects enter or leave them. For example, `oxygenVolumes` refill the player's oxygen when they enter (used for the various oxygen-generating trees in the game), `forces.directionalVolumes` push players and other physics objects in a specific direction (used by both Nomai artificial gravity surfaces and tractor beams), `revealVolumes` unlock ship log facts when the player enters or observes them (used everywhere in the game), and more. + +New Horizons makes adding volumes to your planets easy; just specify them like you would [for a prop](/guides/details/) but under `Volumes` instead of `Props`. For example, to add an oxygen volume at certain location: + +```json title="planets/My Cool Planet.json" +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", + "name" : "My Cool Planet", + "Volumes": { + "oxygenVolumes": [ + { + "position": {"x": 399.4909, "y": -1.562098, "z": 20.11444}, + "radius": 30, + "treeVolume": true, + "playRefillAudio": true + } + ] + } +} +``` + +Listing out every type of volume is outside the scope of this guide, but you can see every supported type of volume and the properties they need in [the VolumesModule schema](/schemas/body-schema/defs/volumesmodule/). + +## Volume Shapes + +By default, volumes are spherical, and you can specify the radius of that sphere with the `radius` property. If you want to use a different shape for your volume, such as a box or capsule, you can specify your volume's `shape` like so: + +```json title="planets/My Cool Planet.json" +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", + "name" : "My Cool Planet", + "Volumes": { + "forces": { + "directionalVolumes": [ + { + "rename": "ArtificialGravitySurface", + "force": 8, + "playGravityCrystalAudio": true, + "shape": { + "type": "box", + "size": { + "x": 15.0, + "y": 10.0, + "z": 5.0 + }, + "offset": { + "x": 0, + "y": 5.0, + "z": 0 + } + }, + "position": { "x": 0, "y": -110, "z": 0 }, + "rotation": { "x": 180, "y": 0, "z": 0 } + } + ] + } + } +} +``` + +The supported shape types are: `sphere`, `box`, `capsule`, `cylinder`, `cone`, `hemisphere`, `hemicapsule`, and `ring`. See [the ShapeInfo schema](/schemas/body-schema/defs/shapeinfo/) for the full list of properties available to define each shape. + +Note that `sphere`, `box`, and `capsule` shapes are more reliable and efficient than other shapes, so prefer using them whenever possible. + +### Debugging + +To visualize the shapes of your volumes in-game, use the [Collider Visualizer mod](https://outerwildsmods.com/mods/collidervisualizer/). It will display a wireframe of the shapes around you so you can see precisely where they are and reposition or resize them as needed.