Docked/Clean Rafts and Speed Limiter Volumes (#1083)

## Minor features

- Added `raftDocks` to `Props` module. An easier way to spawn in docks
for rafts.
- Added `dockPath` to `RaftInfo`. This is a path to the dock the raft
will start attached to.
- Added `pristine` boolean to `RaftInfo`. Makes the raft use the
dreamworld model.
- Added `speedLimiterVolumes` to `Volumes` module. This is the same
thing the Stranger uses to slow you down.

## Bug fixes

- NH-made rafts no longer skip a node when riding on the Dam's raft
carrier.
This commit is contained in:
Noah Pilarski 2025-04-19 00:12:19 -04:00 committed by GitHub
commit 1f5a873e11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 628 additions and 117 deletions

View File

@ -0,0 +1,180 @@
using NewHorizons.Components.Props;
using NewHorizons.External.Modules.Props.EchoesOfTheEye;
using NewHorizons.Handlers;
using NewHorizons.Utility;
using NewHorizons.Utility.OWML;
using System.Collections.Generic;
using UnityEngine;
namespace NewHorizons.Builder.Props.EchoesOfTheEye
{
public static class RaftBuilder
{
private static GameObject _prefab;
private static GameObject _cleanPrefab;
internal static void InitPrefab()
{
if (_prefab == null)
{
_prefab = Object.FindObjectOfType<RaftController>()?.gameObject?.InstantiateInactive()?.Rename("Raft_Body_Prefab")?.DontDestroyOnLoad();
if (_prefab == null)
{
NHLogger.LogWarning($"Tried to make a raft but couldn't. Do you have the DLC installed?");
return;
}
else
{
_prefab.AddComponent<DestroyOnDLC>()._destroyOnDLCNotOwned = true;
var raftController = _prefab.GetComponent<RaftController>();
if (raftController._sector != null)
{
// Since awake already ran we have to unhook these events
raftController._sector.OnOccupantEnterSector -= raftController.OnOccupantEnterSector;
raftController._sector.OnOccupantExitSector -= raftController.OnOccupantExitSector;
raftController._sector = null;
}
raftController._riverFluid = null;
foreach (var lightSensor in _prefab.GetComponentsInChildren<SingleLightSensor>())
{
if (lightSensor._sector != null)
{
lightSensor._sector.OnSectorOccupantsUpdated -= lightSensor.OnSectorOccupantsUpdated;
lightSensor._sector = null;
}
lightSensor._detectDreamLanterns = true;
lightSensor._lanternFocusThreshold = 0.9f;
lightSensor._illuminatingDreamLanternList = new List<DreamLanternController>();
lightSensor._lightSourceMask |= LightSourceType.DREAM_LANTERN;
}
// TODO: Change to one mesh
var twRaftRoot = new GameObject("Effects_IP_SIM_Raft");
twRaftRoot.transform.SetParent(_prefab.transform, false);
var twRaft = SearchUtilities.Find("DreamWorld_Body/Sector_DreamWorld/Interactibles_Dreamworld/DreamRaft_Body/Effects_IP_SIM_Raft_1Way")
.Instantiate(Vector3.zero, Quaternion.identity, twRaftRoot.transform).Rename("Effects_IP_SIM_Raft_1Way");
twRaft.Instantiate(Vector3.zero, Quaternion.Euler(0, 180, 0), twRaftRoot.transform).Rename(twRaft.name);
twRaft.Instantiate(Vector3.zero, Quaternion.Euler(0, 90, 0), twRaftRoot.transform).Rename(twRaft.name);
twRaft.Instantiate(Vector3.zero, Quaternion.Euler(0, -90, 0), twRaftRoot.transform).Rename(twRaft.name);
}
}
if (_cleanPrefab == null && _prefab != null)
{
_cleanPrefab = _prefab?.InstantiateInactive()?.Rename("Raft_Body_Prefab_Clean")?.DontDestroyOnLoad();
if (_cleanPrefab == null)
{
NHLogger.LogWarning($"Tried to make a raft but couldn't. Do you have the DLC installed?");
return;
}
else
{
var raftController = _cleanPrefab.GetComponent<RaftController>();
var rwRaft = _cleanPrefab.FindChild("Structure_IP_Raft");
var rwRaftAnimator = rwRaft.GetComponent<Animator>();
var rac = rwRaftAnimator.runtimeAnimatorController;
Object.DestroyImmediate(rwRaft);
var dwRaft = SearchUtilities.Find("DreamWorld_Body/Sector_DreamWorld/Interactibles_Dreamworld/DreamRaft_Body/Structure_IP_Raft")
.Instantiate(Vector3.zero, Quaternion.identity, _cleanPrefab.transform).Rename("Structure_IP_DreamRaft");
dwRaft.transform.SetSiblingIndex(3);
foreach (var child in dwRaft.GetAllChildren())
{
child.SetActive(true);
}
var dwRaftAnimator = dwRaft.AddComponent<Animator>();
dwRaftAnimator.runtimeAnimatorController = rac;
raftController._railingAnimator = dwRaftAnimator;
var dwLightSensorForward = SearchUtilities.Find("DreamWorld_Body/Sector_DreamWorld/Interactibles_Dreamworld/DreamRaft_Body/LightSensor_Forward");
var dwLightSensorOrigMaterial = dwLightSensorForward.GetComponent<LightSensorEffects>()._origMaterial;
var dwLightSensor = dwLightSensorForward.FindChild("Structure_IP_Raft_Sensor");
ChangeSensor(_cleanPrefab.FindChild("LightSensorRoot/LightSensor_Forward"), dwLightSensorOrigMaterial, dwLightSensor);
ChangeSensor(_cleanPrefab.FindChild("LightSensorRoot/LightSensor_Right"), dwLightSensorOrigMaterial, dwLightSensor);
ChangeSensor(_cleanPrefab.FindChild("LightSensorRoot/LightSensor_Rear"), dwLightSensorOrigMaterial, dwLightSensor, true);
ChangeSensor(_cleanPrefab.FindChild("LightSensorRoot/LightSensor_Left"), dwLightSensorOrigMaterial, dwLightSensor);
}
}
}
private static void ChangeSensor(GameObject lightSensor, Material origMaterial, GameObject newLightSensor, bool reverse = false)
{
var singleLightSensor = lightSensor.GetComponent<SingleLightSensor>();
var lightSensorEffects = lightSensor.GetComponent<LightSensorEffects>();
lightSensorEffects._lightSensor = singleLightSensor;
Object.DestroyImmediate(lightSensor.FindChild("Structure_IP_Raft_Sensor"));
lightSensorEffects._origMaterial = origMaterial;
var copiedLightSensor = newLightSensor
.Instantiate(reverse ? new Vector3(0, -1.5f, -0.1303f) : new Vector3(0, -1.5f, -0.0297f),
reverse ? Quaternion.identity : Quaternion.Euler(0, 180, 0),
lightSensor.transform).Rename("Structure_IP_DreamRaft_Sensor");
var bulb = copiedLightSensor.FindChild("Props_IP_Raft_Lamp_geoBulb");
lightSensorEffects._renderer = bulb.GetComponent<MeshRenderer>();
lightSensorEffects._lightRenderer = bulb.GetComponent<OWRenderer>();
}
public static GameObject Make(GameObject planetGO, Sector sector, RaftInfo info, OWRigidbody planetBody)
{
InitPrefab();
if (_prefab == null || _cleanPrefab == null || sector == null) return null;
GameObject raftObject = GeneralPropBuilder.MakeFromPrefab(info.pristine ? _cleanPrefab : _prefab, "Raft_Body", planetGO, sector, info);
StreamingHandler.SetUpStreaming(raftObject, sector);
var raftController = raftObject.GetComponent<RaftController>();
raftController._sector = sector;
raftController._acceleration = info.acceleration;
sector.OnOccupantEnterSector += raftController.OnOccupantEnterSector;
sector.OnOccupantExitSector += raftController.OnOccupantExitSector;
// Detectors
var fluidDetector = raftObject.transform.Find("Detector_Raft").GetComponent<RaftFluidDetector>();
var waterVolume = planetGO.GetComponentInChildren<RadialFluidVolume>();
fluidDetector._alignmentFluid = waterVolume;
fluidDetector._buoyancy.checkAgainstWaves = true;
// Rafts were unable to trigger docks because these were disabled for some reason
fluidDetector.GetComponent<BoxShape>().enabled = true;
fluidDetector.GetComponent<OWCollider>().enabled = true;
// Light sensors
foreach (var lightSensor in raftObject.GetComponentsInChildren<SingleLightSensor>())
{
lightSensor._sector = sector;
sector.OnSectorOccupantsUpdated += lightSensor.OnSectorOccupantsUpdated;
}
var nhRaftController = raftObject.AddComponent<NHRaftController>();
var achievementObject = new GameObject("AchievementVolume");
achievementObject.transform.SetParent(raftObject.transform, false);
var shape = achievementObject.AddComponent<SphereShape>();
shape.radius = 3;
shape.SetCollisionMode(Shape.CollisionMode.Volume);
achievementObject.AddComponent<OWTriggerVolume>()._shape = shape;
achievementObject.AddComponent<OtherMods.AchievementsPlus.NH.RaftingAchievement>();
raftObject.SetActive(true);
if (planetGO != null && !string.IsNullOrEmpty(info.dockPath))
{
var dockTransform = planetGO.transform.Find(info.dockPath);
if (dockTransform != null && dockTransform.TryGetComponent(out RaftDock raftDock))
{
raftController.SkipSuspendOnStart();
raftDock._startRaft = raftController;
raftDock._raft = raftController;
}
else
{
NHLogger.LogError($"Cannot find raft dock object at path: {planetGO.name}/{info.dockPath}");
}
}
return raftObject;
}
}
}

View File

@ -0,0 +1,55 @@
using NewHorizons.Components.Props;
using NewHorizons.External.Modules.Props;
using NewHorizons.External.Modules.Props.EchoesOfTheEye;
using NewHorizons.Handlers;
using NewHorizons.Utility;
using NewHorizons.Utility.OWML;
using OWML.Common;
using UnityEngine;
namespace NewHorizons.Builder.Props.EchoesOfTheEye
{
public static class RaftDockBuilder
{
private static GameObject _prefab;
internal static void InitPrefab()
{
if (_prefab == null)
{
_prefab = SearchUtilities.Find("RingWorld_Body/Sector_RingInterior/Sector_Zone1/Structures_Zone1/RaftHouse_ArrivalPatio_Zone1/Interactables_RaftHouse_ArrivalPatio_Zone1/Prefab_IP_RaftDock").InstantiateInactive().Rename("Prefab_RaftDock").DontDestroyOnLoad();
if (_prefab == null)
{
NHLogger.LogWarning($"Tried to make a raft dock but couldn't. Do you have the DLC installed?");
return;
}
else
{
_prefab.AddComponent<DestroyOnDLC>()._destroyOnDLCNotOwned = true;
var raftDock = _prefab.GetComponent<RaftDock>();
raftDock._startRaft = null;
raftDock._floodSensor = null;
foreach (var floodToggle in _prefab.GetComponents<FloodToggle>())
{
Component.DestroyImmediate(floodToggle);
}
Object.DestroyImmediate(_prefab.FindChild("FloodSensor"));
Object.DestroyImmediate(_prefab.FindChild("FloodSensor_RaftHouseArrivalPatio_NoDelay"));
}
}
}
public static GameObject Make(GameObject planetGO, Sector sector, RaftDockInfo info, IModBehaviour mod)
{
InitPrefab();
if (_prefab == null || sector == null) return null;
var dockObject = DetailBuilder.Make(planetGO, sector, mod, _prefab, new DetailInfo(info));
//var raftDock = dockObject.GetComponent<RaftDock>();
return dockObject;
}
}
}

View File

@ -114,6 +114,7 @@ namespace NewHorizons.Builder.Props
MakeGeneralProps(go, config.Props.grappleTotems, (totem) => GrappleTotemBuilder.Make(go, sector, totem, mod));
MakeGeneralProps(go, config.Props.dreamCampfires, (campfire) => DreamCampfireBuilder.Make(go, sector, campfire, mod), (campfire) => campfire.id);
MakeGeneralProps(go, config.Props.dreamArrivalPoints, (point) => DreamArrivalPointBuilder.Make(go, sector, point, mod), (point) => point.id);
MakeGeneralProps(go, config.Props.raftDocks, (dock) => RaftDockBuilder.Make(go, sector, dock, mod));
MakeGeneralProps(go, config.Props.rafts, (raft) => RaftBuilder.Make(go, sector, raft, planetBody));
}
MakeGeneralProps(go, config.Props.tornados, (tornado) => TornadoBuilder.Make(go, sector, tornado, config.Atmosphere?.clouds != null));

View File

@ -1,97 +0,0 @@
using NewHorizons.Components.Props;
using NewHorizons.External.Modules.Props.EchoesOfTheEye;
using NewHorizons.Handlers;
using NewHorizons.Utility;
using NewHorizons.Utility.OWML;
using UnityEngine;
namespace NewHorizons.Builder.Props
{
public static class RaftBuilder
{
private static GameObject _prefab;
internal static void InitPrefab()
{
if (_prefab == null)
{
_prefab = Object.FindObjectOfType<RaftController>()?.gameObject?.InstantiateInactive()?.Rename("Raft_Body_Prefab")?.DontDestroyOnLoad();
if (_prefab == null)
{
NHLogger.LogWarning($"Tried to make a raft but couldn't. Do you have the DLC installed?");
return;
}
else
{
_prefab.AddComponent<DestroyOnDLC>()._destroyOnDLCNotOwned = true;
var raftController = _prefab.GetComponent<RaftController>();
if (raftController._sector != null)
{
// Since awake already ran we have to unhook these events
raftController._sector.OnOccupantEnterSector -= raftController.OnOccupantEnterSector;
raftController._sector.OnOccupantExitSector -= raftController.OnOccupantExitSector;
raftController._sector = null;
}
raftController._riverFluid = null;
foreach (var lightSensor in _prefab.GetComponentsInChildren<SingleLightSensor>())
{
if (lightSensor._sector != null)
{
lightSensor._sector.OnSectorOccupantsUpdated -= lightSensor.OnSectorOccupantsUpdated;
lightSensor._sector = null;
}
}
}
}
}
public static GameObject Make(GameObject planetGO, Sector sector, RaftInfo info, OWRigidbody planetBody)
{
InitPrefab();
if (_prefab == null || sector == null) return null;
GameObject raftObject = GeneralPropBuilder.MakeFromPrefab(_prefab, "Raft_Body", planetGO, sector, info);
StreamingHandler.SetUpStreaming(raftObject, sector);
var raftController = raftObject.GetComponent<RaftController>();
raftController._sector = sector;
raftController._acceleration = info.acceleration;
sector.OnOccupantEnterSector += raftController.OnOccupantEnterSector;
sector.OnOccupantExitSector += raftController.OnOccupantExitSector;
// Detectors
var fluidDetector = raftObject.transform.Find("Detector_Raft").GetComponent<RaftFluidDetector>();
var waterVolume = planetGO.GetComponentInChildren<RadialFluidVolume>();
fluidDetector._alignmentFluid = waterVolume;
fluidDetector._buoyancy.checkAgainstWaves = true;
// Rafts were unable to trigger docks because these were disabled for some reason
fluidDetector.GetComponent<BoxShape>().enabled = true;
fluidDetector.GetComponent<OWCollider>().enabled = true;
// Light sensors
foreach (var lightSensor in raftObject.GetComponentsInChildren<SingleLightSensor>())
{
lightSensor._sector = sector;
sector.OnSectorOccupantsUpdated += lightSensor.OnSectorOccupantsUpdated;
}
var nhRaftController = raftObject.AddComponent<NHRaftController>();
var achievementObject = new GameObject("AchievementVolume");
achievementObject.transform.SetParent(raftObject.transform, false);
var shape = achievementObject.AddComponent<SphereShape>();
shape.radius = 3;
shape.SetCollisionMode(Shape.CollisionMode.Volume);
achievementObject.AddComponent<OWTriggerVolume>()._shape = shape;
achievementObject.AddComponent<OtherMods.AchievementsPlus.NH.RaftingAchievement>();
raftObject.SetActive(true);
return raftObject;
}
}
}

View File

@ -471,7 +471,7 @@ namespace NewHorizons.Builder.Props.TranslatorText
if (info.arcInfo != null && info.arcInfo.Count() != dict.Values.Count())
{
NHLogger.LogError($"Can't make NomaiWallText, arcInfo length [{info.arcInfo.Count()}] doesn't equal number of TextBlocks [{dict.Values.Count()}] in the xml");
NHLogger.LogError($"Can't make NomaiWallText [{info.xmlFile}], arcInfo length [{info.arcInfo.Count()}] doesn't equal number of TextBlocks [{dict.Values.Count()}] in the xml");
return;
}

View File

@ -0,0 +1,20 @@
using NewHorizons.Components.Volumes;
using NewHorizons.External.Modules.Volumes.VolumeInfos;
using UnityEngine;
namespace NewHorizons.Builder.Volumes
{
public static class SpeedLimiterVolumeBuilder
{
public static SpeedLimiterVolume Make(GameObject planetGO, Sector sector, SpeedLimiterVolumeInfo info)
{
var volume = VolumeBuilder.Make<SpeedLimiterVolume>(planetGO, sector, info);
volume.maxSpeed = info.maxSpeed;
volume.stoppingDistance = info.stoppingDistance;
volume.maxEntryAngle = info.maxEntryAngle;
return volume;
}
}
}

View File

@ -191,6 +191,13 @@ namespace NewHorizons.Builder.Volumes
SpeedTrapVolumeBuilder.Make(go, sector, speedTrapVolume);
}
}
if (config.Volumes.speedLimiterVolumes != null)
{
foreach (var speedLimiterVolume in config.Volumes.speedLimiterVolumes)
{
SpeedLimiterVolumeBuilder.Make(go, sector, speedLimiterVolume);
}
}
if (config.Volumes.lightSourceVolumes != null)
{
foreach (var lightSourceVolume in config.Volumes.lightSourceVolumes)

View File

@ -0,0 +1,164 @@
using System.Collections.Generic;
using System;
using UnityEngine;
namespace NewHorizons.Components.Volumes
{
public class SpeedLimiterVolume : BaseVolume
{
public float maxSpeed = 10f;
public float stoppingDistance = 100f;
public float maxEntryAngle = 60f;
private OWRigidbody _parentBody;
private List<TrackedBody> _trackedBodies = new List<TrackedBody>();
private bool _playerJustExitedDream;
public override void Awake()
{
_parentBody = GetComponentInParent<OWRigidbody>();
base.Awake();
GlobalMessenger.AddListener("ExitDreamWorld", OnExitDreamWorld);
}
public void Start()
{
enabled = false;
}
public override void OnDestroy()
{
base.OnDestroy();
GlobalMessenger.RemoveListener("ExitDreamWorld", OnExitDreamWorld);
}
public void FixedUpdate()
{
foreach (var trackedBody in _trackedBodies)
{
bool slowed = false;
Vector3 velocity = trackedBody.body.GetVelocity() - _parentBody.GetVelocity();
float magnitude = velocity.magnitude;
if (magnitude <= maxSpeed)
{
slowed = true;
}
else
{
bool needsSlowing = true;
float velocityReduction = trackedBody.deceleration * Time.deltaTime;
float requiredReduction = maxSpeed - magnitude;
if (requiredReduction > velocityReduction)
{
velocityReduction = requiredReduction;
slowed = true;
}
if (trackedBody.name == Detector.Name.Ship)
{
Autopilot component = Locator.GetShipTransform().GetComponent<Autopilot>();
if (component != null && component.IsFlyingToDestination())
{
needsSlowing = false;
}
}
if (needsSlowing)
{
Vector3 velocityChange = velocityReduction * velocity.normalized;
trackedBody.body.AddVelocityChange(velocityChange);
if (trackedBody.name == Detector.Name.Ship && PlayerState.IsInsideShip())
{
Locator.GetPlayerBody().AddVelocityChange(velocityChange);
}
}
}
if (slowed)
{
if (trackedBody.name == Detector.Name.Ship)
GlobalMessenger.FireEvent("ShipExitSpeedLimiter");
_trackedBodies.Remove(trackedBody);
if (_trackedBodies.Count == 0)
enabled = false;
}
}
}
private void OnExitDreamWorld()
{
_playerJustExitedDream = true;
}
public override void OnTriggerVolumeEntry(GameObject hitObj)
{
DynamicForceDetector component = hitObj.GetComponent<DynamicForceDetector>();
if (component == null || !component.CompareNameMask(Detector.Name.Player | Detector.Name.Probe | Detector.Name.Ship)) return;
if (component.GetName() == Detector.Name.Player && (PlayerState.IsInsideShip() || _playerJustExitedDream))
{
_playerJustExitedDream = false;
}
else
{
OWRigidbody attachedOWRigidbody = component.GetAttachedOWRigidbody();
Vector3 from = transform.position - attachedOWRigidbody.GetPosition();
Vector3 to = attachedOWRigidbody.GetVelocity() - _parentBody.GetVelocity();
float magnitude = to.magnitude;
if (magnitude > maxSpeed && Vector3.Angle(from, to) < maxEntryAngle)
{
float deceleration = (maxSpeed * maxSpeed - magnitude * magnitude) / (2f * stoppingDistance);
TrackedBody trackedBody = new TrackedBody(attachedOWRigidbody, component.GetName(), deceleration);
_trackedBodies.Add(trackedBody);
if (component.GetName() == Detector.Name.Ship)
GlobalMessenger.FireEvent("ShipEnterSpeedLimiter");
enabled = true;
}
}
}
public override void OnTriggerVolumeExit(GameObject hitObj)
{
DynamicForceDetector component = hitObj.GetComponent<DynamicForceDetector>();
if (component == null) return;
OWRigidbody body = component.GetAttachedOWRigidbody();
TrackedBody trackedBody = _trackedBodies.Find((TrackedBody i) => i.body == body);
if (trackedBody != null)
{
if (trackedBody.name == Detector.Name.Ship)
GlobalMessenger.FireEvent("ShipExitSpeedLimiter");
_trackedBodies.Remove(trackedBody);
if (_trackedBodies.Count == 0)
enabled = false;
}
}
[Serializable]
protected class TrackedBody
{
public OWRigidbody body;
public Detector.Name name;
public float deceleration;
public TrackedBody(OWRigidbody body, Detector.Name name, float deceleration)
{
this.body = body;
this.name = name;
this.deceleration = deceleration;
}
public override bool Equals(object obj)
{
if (obj is TrackedBody trackedBody)
{
return trackedBody.body == body && trackedBody.name == name;
}
return base.Equals(obj);
}
public override int GetHashCode() => body.GetHashCode();
}
}
}

View File

@ -58,6 +58,11 @@ namespace NewHorizons.External.Modules
/// </summary>
public RaftInfo[] rafts;
/// <summary>
/// Add raft docks to this planet (requires Echoes of the Eye DLC)
/// </summary>
public RaftDockInfo[] raftDocks;
/// <summary>
/// Scatter props around this planet's surface
/// </summary>

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
using System.ComponentModel;
namespace NewHorizons.External.Modules.Props.EchoesOfTheEye
{
[JsonObject]
public class RaftDockInfo : GeneralPropInfo
{
}
}

View File

@ -10,6 +10,16 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye
/// Acceleration of the raft. Default acceleration is 5.
/// </summary>
[DefaultValue(5f)] public float acceleration = 5f;
/// <summary>
/// Path to the dock this raft will start attached to.
/// </summary>
public string dockPath;
/// <summary>
/// Uses the raft model from the dreamworld
/// </summary>
public bool pristine;
}
}

View File

@ -0,0 +1,28 @@
using Newtonsoft.Json;
using System.ComponentModel;
using UnityEngine;
namespace NewHorizons.External.Modules.Volumes.VolumeInfos
{
[JsonObject]
public class SpeedLimiterVolumeInfo : VolumeInfo
{
/// <summary>
/// The speed the volume will slow you down to when you enter it.
/// </summary>
[DefaultValue(10f)]
public float maxSpeed = 10f;
/// <summary>
/// The distance from the outside of the volume that the limiter slows you down to max speed at.
/// </summary>
[DefaultValue(100f)]
public float stoppingDistance = 100f;
/// <summary>
/// The maximum angle (in degrees) between the direction the incoming object is moving relative to the volume's center and the line from the object toward the center of the volume, within which the speed limiter will activate.
/// </summary>
[DefaultValue(60f)]
public float maxEntryAngle = 60f;
}
}

View File

@ -101,6 +101,13 @@ namespace NewHorizons.External.Modules.Volumes
/// </summary>
public SpeedTrapVolumeInfo[] speedTrapVolumes;
/// <summary>
/// Add speed limiter volumes to this planet.
/// Slows down the player, ship, and probe when they enter this volume.
/// Used on the Stranger in DLC.
/// </summary>
public SpeedLimiterVolumeInfo[] speedLimiterVolumes;
/// <summary>
/// Add visor effect volumes to this planet.
/// </summary>

View File

@ -382,6 +382,7 @@ namespace NewHorizons
ProjectionBuilder.InitPrefabs();
CloakBuilder.InitPrefab();
RaftBuilder.InitPrefab();
RaftDockBuilder.InitPrefab();
DreamCampfireBuilder.InitPrefab();
DreamArrivalPointBuilder.InitPrefab();
}

View File

@ -75,30 +75,41 @@ namespace NewHorizons.Patches.EchoesOfTheEyePatches
return false;
}
[HarmonyPostfix]
[HarmonyPrefix]
[HarmonyPatch(nameof(RaftController.UpdateMoveToTarget))]
public static void UpdateMoveToTarget(RaftController __instance)
public static bool UpdateMoveToTarget(RaftController __instance)
{
OWRigidbody raftBody = __instance._raftBody;
OWRigidbody origParentBody = raftBody.GetOrigParentBody();
Transform transform = origParentBody.transform;
Vector3 position = transform.TransformPoint(__instance._targetLocalPosition);
Vector3 distance = position - raftBody.GetPosition();
float speed = Mathf.Min(__instance._targetSpeed, distance.magnitude / Time.deltaTime);
Vector3 pointVelocity = raftBody.GetOrigParentBody().GetPointVelocity(raftBody.GetPosition());
raftBody.SetVelocity(pointVelocity + (distance.normalized * speed));
float t = (__instance.currentDistanceLerp = Mathf.InverseLerp(__instance._startDistance, 0.001f, distance.magnitude));
var st = Mathf.SmoothStep(0f, 1f, t);
// If it has a riverFluid volume then its a regular stranger one
if (__instance._movingToTarget && __instance._riverFluid == null)
var isStranger = __instance._riverFluid != null;
// Base game threshold has this at 1f (after doing smoothstep on it)
// For whatever reason it never hits that for NH planets (probably since they're moving so much compared to the steady velocity of the Stranger)
// Might break for somebody with a wacky spinning planet in which case we can adjust this or add some kind of fallback (i.e., wait x seconds and then just say its there)
// Fixes #1005
if (isStranger ? (st >= 1f) : (t > 0.999f))
{
OWRigidbody raftBody = __instance._raftBody;
OWRigidbody origParentBody = __instance._raftBody.GetOrigParentBody();
Transform transform = origParentBody.transform;
Vector3 vector = transform.TransformPoint(__instance._targetLocalPosition);
// Base game threshold has this at 1f (after doing smoothstep on it)
// For whatever reason it never hits that for NH planets (probably since they're moving so much compared to the steady velocity of the Stranger)
// Might break for somebody with a wacky spinning planet in which case we can adjust this or add some kind of fallback (i.e., wait x seconds and then just say its there)
// Fixes #1005
if (__instance.currentDistanceLerp > 0.999f)
{
raftBody.SetPosition(vector);
raftBody.SetRotation(transform.rotation * __instance._targetLocalRotation);
__instance.StopMovingToTarget();
__instance.OnArriveAtTarget.Invoke();
}
raftBody.SetPosition(position);
raftBody.SetRotation(transform.rotation * __instance._targetLocalRotation);
__instance.StopMovingToTarget();
__instance.OnArriveAtTarget.Invoke();
}
else
{
Quaternion quaternion = Quaternion.Slerp(__instance._startLocalRotation, __instance._targetLocalRotation, st);
Quaternion toRotation = transform.rotation * quaternion;
Vector3 vector4 = OWPhysics.FromToAngularVelocity(raftBody.GetRotation(), toRotation);
raftBody.SetAngularVelocity(origParentBody.GetAngularVelocity() + vector4);
}
return false;
}
}
}

View File

@ -2231,6 +2231,13 @@
"$ref": "#/definitions/RaftInfo"
}
},
"raftDocks": {
"type": "array",
"description": "Add raft docks to this planet (requires Echoes of the Eye DLC)",
"items": {
"$ref": "#/definitions/RaftDockInfo"
}
},
"scatter": {
"type": "array",
"description": "Scatter props around this planet's surface",
@ -2813,6 +2820,47 @@
"description": "Acceleration of the raft. Default acceleration is 5.",
"format": "float",
"default": 5.0
},
"dockPath": {
"type": "string",
"description": "Path to the dock this raft will start attached to."
},
"pristine": {
"type": "boolean",
"description": "Uses the raft model from the dreamworld"
}
}
},
"RaftDockInfo": {
"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"
},
"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"
}
}
},
@ -5423,6 +5471,13 @@
"$ref": "#/definitions/SpeedTrapVolumeInfo"
}
},
"speedLimiterVolumes": {
"type": "array",
"description": "Add speed limiter volumes to this planet.\nSlows down the player, ship, and probe when they enter this volume.\nUsed on the Stranger in DLC.",
"items": {
"$ref": "#/definitions/SpeedLimiterVolumeInfo"
}
},
"visorEffects": {
"description": "Add visor effect volumes to this planet.",
"$ref": "#/definitions/VisorEffectModule"
@ -6266,6 +6321,52 @@
}
}
},
"SpeedLimiterVolumeInfo": {
"type": "object",
"additionalProperties": false,
"properties": {
"radius": {
"type": "number",
"description": "The radius of this volume.",
"format": "float",
"default": 1.0
},
"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"
},
"maxSpeed": {
"type": "number",
"description": "The speed the volume will slow you down to when you enter it.",
"format": "float",
"default": 10.0
},
"stoppingDistance": {
"type": "number",
"description": "The distance from the outside of the volume that the limiter slows you down to max speed at.",
"format": "float",
"default": 100.0
},
"maxEntryAngle": {
"type": "number",
"description": "The maximum angle (in degrees) between the direction the incoming object is moving relative to the volume's center and the line from the object toward the center of the volume, within which the speed limiter will activate.",
"format": "float",
"default": 60.0
}
}
},
"VisorEffectModule": {
"type": "object",
"additionalProperties": false,

View File

@ -274,6 +274,14 @@ namespace NewHorizons.Utility
return UnityEngine.Object.Instantiate(original);
}
public static GameObject Instantiate(this GameObject original, Vector3 localPosition, Quaternion localRotation, Transform parent)
{
var copy = UnityEngine.Object.Instantiate(original, parent, false);
copy.transform.localPosition = localPosition;
copy.transform.localRotation = localRotation;
return copy;
}
public static T DontDestroyOnLoad<T>(this T target) where T : UnityEngine.Object
{
UnityEngine.Object.DontDestroyOnLoad(target);