From 63fbc8afeb45133569938dd21bc192b694401b90 Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Sat, 18 Jan 2025 10:09:15 -0600 Subject: [PATCH] Eye sequence props first pass --- .../Builder/Props/EyeOfTheUniverseBuilder.cs | 187 ++++++++++++++++++ .../EyeOfTheUniverse/EyeMusicController.cs | 97 +++++++++ .../EyeOfTheUniverse/InstrumentZone.cs | 9 + .../QuantumInstrumentTrigger.cs | 30 +++ NewHorizons/External/Configs/PlanetConfig.cs | 5 + .../Modules/EyeOfTheUniverseModule.cs | 24 +++ .../Props/EyeOfTheUniverse/EyeTravelerInfo.cs | 45 +++++ .../EyeOfTheUniverse/InstrumentZoneInfo.cs | 13 ++ .../EyeOfTheUniverse/QuantumInstrumentInfo.cs | 34 ++++ NewHorizons/Handlers/EyeSceneHandler.cs | 167 ++++++++++++++++ NewHorizons/Handlers/PlanetCreationHandler.cs | 5 + NewHorizons/Main.cs | 2 + .../CosmicInflationControllerPatches.cs | 23 +++ .../QuantumCampsiteControllerPatches.cs | 85 ++++++++ .../TravelerEyeControllerPatches.cs | 17 ++ 15 files changed, 743 insertions(+) create mode 100644 NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs create mode 100644 NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs create mode 100644 NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs create mode 100644 NewHorizons/Components/EyeOfTheUniverse/QuantumInstrumentTrigger.cs create mode 100644 NewHorizons/External/Modules/EyeOfTheUniverseModule.cs create mode 100644 NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs create mode 100644 NewHorizons/External/Modules/Props/EyeOfTheUniverse/InstrumentZoneInfo.cs create mode 100644 NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs create mode 100644 NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs create mode 100644 NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs create mode 100644 NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs diff --git a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs new file mode 100644 index 00000000..93efe304 --- /dev/null +++ b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs @@ -0,0 +1,187 @@ +using NewHorizons.Builder.Props.Audio; +using NewHorizons.Components.EyeOfTheUniverse; +using NewHorizons.External; +using NewHorizons.External.Modules; +using NewHorizons.External.Modules.Props.Audio; +using NewHorizons.External.Modules.Props.EyeOfTheUniverse; +using NewHorizons.Handlers; +using NewHorizons.Utility; +using NewHorizons.Utility.OuterWilds; +using NewHorizons.Utility.OWML; +using UnityEngine; + +namespace NewHorizons.Builder.Props +{ + public static class EyeOfTheUniverseBuilder + { + public static TravelerEyeController MakeEyeTraveler(GameObject planetGO, Sector sector, EyeTravelerInfo info, NewHorizonsBody nhBody) + { + var go = DetailBuilder.Make(planetGO, sector, nhBody.Mod, info); + + var travelerController = go.GetAddComponent(); + if (!string.IsNullOrEmpty(info.startPlayingCondition)) + { + travelerController._startPlayingCondition = info.startPlayingCondition; + } + else if (string.IsNullOrEmpty(travelerController._startPlayingCondition)) + { + NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have a Start Playing condition set"); + } + if (travelerController._animator == null) + { + travelerController._animator = go.GetComponentInChildren(); + } + if (info.dialogue != null) + { + var (dialogueTree, remoteTrigger) = DialogueBuilder.Make(planetGO, sector, info.dialogue, nhBody.Mod); + if (travelerController._dialogueTree != null) + { + travelerController._dialogueTree.OnStartConversation -= travelerController.OnStartConversation; + travelerController._dialogueTree.OnEndConversation -= travelerController.OnEndConversation; + } + travelerController._dialogueTree = dialogueTree; + travelerController._dialogueTree.OnStartConversation += travelerController.OnStartConversation; + travelerController._dialogueTree.OnEndConversation += travelerController.OnEndConversation; + } + else if (travelerController._dialogueTree == null) + { + NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any dialogue set"); + } + + OWAudioSource loopAudioSource = null; + + if (!string.IsNullOrEmpty(info.loopAudio)) + { + var signalInfo = new SignalInfo() + { + audio = info.loopAudio, + detectionRadius = 0, + identificationRadius = 10f, + frequency = string.IsNullOrEmpty(info.frequency) ? "Traveler" : info.frequency, + parentPath = go.transform.GetPath(), + isRelativeToParent = true, + position = Vector3.up * 0.5f, + }; + var signalGO = SignalBuilder.Make(planetGO, sector, signalInfo, nhBody.Mod); + var signal = signalGO.GetComponent(); + travelerController._signal = signal; + loopAudioSource = signal.GetOWAudioSource(); + } + else if (travelerController._signal == null) + { + NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any loop audio set"); + } + + OWAudioSource finaleAudioSource = null; + + if (!string.IsNullOrEmpty(info.finaleAudio)) + { + var finaleAudioInfo = new AudioSourceInfo() + { + audio = info.finaleAudio, + track = External.SerializableEnums.NHAudioMixerTrackName.Music, + }; + finaleAudioSource = GeneralAudioBuilder.Make(planetGO, sector, finaleAudioInfo, nhBody.Mod); + finaleAudioSource.SetTrack(finaleAudioInfo.track.ConvertToOW()); + finaleAudioSource.loop = false; + finaleAudioSource.spatialBlend = 0f; + } + + var travelerData = EyeSceneHandler.GetOrCreateEyeTravelerData(info.id); + travelerData.info = info; + travelerData.controller = travelerController; + travelerData.loopAudioSource = loopAudioSource; + travelerData.finaleAudioSource = finaleAudioSource; + + return travelerController; + } + + public static QuantumInstrument MakeQuantumInstrument(GameObject planetGO, Sector sector, QuantumInstrumentInfo info, NewHorizonsBody nhBody) + { + var go = DetailBuilder.Make(planetGO, sector, nhBody.Mod, info); + go.layer = Layer.Interactible; + if (info.interactRadius > 0f) + { + var collider = go.AddComponent(); + collider.radius = info.interactRadius; + collider.isTrigger = true; + go.GetAddComponent(); + } + + go.GetAddComponent(); + var quantumInstrument = go.GetAddComponent(); + quantumInstrument._gatherWithScope = info.gatherWithScope; + + var trigger = go.AddComponent(); + trigger.gatherCondition = info.gatherCondition; + + var travelerData = EyeSceneHandler.GetOrCreateEyeTravelerData(info.id); + travelerData.quantumInstruments.Add(quantumInstrument); + + if (travelerData.info != null) + { + if (!string.IsNullOrEmpty(travelerData.info.loopAudio)) + { + var signalInfo = new SignalInfo() + { + audio = travelerData.info.loopAudio, + detectionRadius = 0, + identificationRadius = 0, + frequency = string.IsNullOrEmpty(travelerData.info.frequency) ? "Traveler" : travelerData.info.frequency, + parentPath = go.transform.GetPath(), + isRelativeToParent = true, + position = Vector3.zero, + }; + var signalGO = SignalBuilder.Make(planetGO, sector, signalInfo, nhBody.Mod); + } + else + { + NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any loop audio set"); + } + } + else + { + NHLogger.LogError($"Quantum instrument with ID \"{info.id}\" has no matching eye traveler"); + } + + return quantumInstrument; + } + + public static InstrumentZone MakeInstrumentZone(GameObject planetGO, Sector sector, InstrumentZoneInfo info, NewHorizonsBody nhBody) + { + var go = DetailBuilder.Make(planetGO, sector, nhBody.Mod, info); + + var instrumentZone = go.AddComponent(); + + var travelerData = EyeSceneHandler.GetOrCreateEyeTravelerData(info.id); + travelerData.instrumentZones.Add(instrumentZone); + + return instrumentZone; + } + + public static void Make(GameObject go, Sector sector, EyeOfTheUniverseModule module, NewHorizonsBody nhBody) + { + if (module.eyeTravelers != null) + { + foreach (var info in module.eyeTravelers) + { + MakeEyeTraveler(go, sector, info, nhBody); + } + } + if (module.instrumentZones != null) + { + foreach (var info in module.instrumentZones) + { + MakeInstrumentZone(go, sector, info, nhBody); + } + } + if (module.quantumInstruments != null) + { + foreach (var info in module.quantumInstruments) + { + MakeQuantumInstrument(go, sector, info, nhBody); + } + } + } + } +} diff --git a/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs b/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs new file mode 100644 index 00000000..3a93cbda --- /dev/null +++ b/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs @@ -0,0 +1,97 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace NewHorizons.Components.EyeOfTheUniverse +{ + public class EyeMusicController : MonoBehaviour + { + private List _loopSources = new(); + private List _finaleSources = new(); + private bool _transitionToFinale; + private bool _isPlaying; + + public void RegisterLoopSource(OWAudioSource src) + { + src.loop = false; + src.SetLocalVolume(1f); + _loopSources.Add(src); + } + + public void RegisterFinaleSource(OWAudioSource src) + { + src.loop = false; + src.SetLocalVolume(1f); + _finaleSources.Add(src); + } + + public void StartPlaying() + { + if (_isPlaying) return; + _isPlaying = true; + StartCoroutine(DoLoop()); + } + + public void TransitionToFinale() + { + _transitionToFinale = true; + } + + private IEnumerator DoLoop() + { + // Initial delay to ensure audio system has time to schedule all loops for the same tick + double timeBufferWindow = 0.5; + + // Track when the next audio (loop or finale) should play, in audio system time + double nextAudioEventTime = AudioSettings.dspTime + timeBufferWindow; + + // Determine timing using the first loop audio clip (should be Riebeck's banjo loop) + var referenceLoopClip = _loopSources.First().clip; + double loopDuration = referenceLoopClip.samples / (double)referenceLoopClip.frequency; + double segmentDuration = loopDuration / 4.0; + + while (!_transitionToFinale) + { + // Play loops in sync + var loopStartTime = nextAudioEventTime; + foreach (var loopSrc in _loopSources) + { + loopSrc._audioSource.PlayScheduled(loopStartTime); + } + + nextAudioEventTime += loopDuration; + + // Handle loop segments (the current musical measure will always finish playing before transitioning to the finale) + for (int i = 0; i < 4; i++) + { + // Interrupting the upcoming segment for the finale + if (_transitionToFinale) + { + // End the loop at the start time of the upcoming segment + var loopStopTime = loopStartTime + segmentDuration * (i + 1); + + // Cancel scheduled upcoming loop + foreach (var loopSrc in _loopSources) + { + loopSrc._audioSource.SetScheduledEndTime(loopStopTime); + } + + // Schedule finale for as soon as the loop ends + nextAudioEventTime = loopStopTime; + break; + } + + // Wait until shortly before the next segment (`nextAudioEventTime` will be ahead of current time by `timeBufferWindow`) + yield return new WaitForSecondsRealtime((float)segmentDuration); + } + } + + // Play finale in sync + foreach (var finaleSrc in _finaleSources) + { + finaleSrc._audioSource.PlayScheduled(nextAudioEventTime); + } + } + } +} diff --git a/NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs b/NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs new file mode 100644 index 00000000..39b0d529 --- /dev/null +++ b/NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs @@ -0,0 +1,9 @@ +using UnityEngine; + +namespace NewHorizons.Components.EyeOfTheUniverse +{ + public class InstrumentZone : MonoBehaviour + { + + } +} diff --git a/NewHorizons/Components/EyeOfTheUniverse/QuantumInstrumentTrigger.cs b/NewHorizons/Components/EyeOfTheUniverse/QuantumInstrumentTrigger.cs new file mode 100644 index 00000000..c927c36c --- /dev/null +++ b/NewHorizons/Components/EyeOfTheUniverse/QuantumInstrumentTrigger.cs @@ -0,0 +1,30 @@ +using UnityEngine; + +namespace NewHorizons.Components.EyeOfTheUniverse +{ + public class QuantumInstrumentTrigger : MonoBehaviour + { + public string gatherCondition; + + private QuantumInstrument _quantumInstrument; + + private void Awake() + { + _quantumInstrument = GetComponent(); + _quantumInstrument.OnFinishGather += OnFinishGather; + } + + private void OnDestroy() + { + _quantumInstrument.OnFinishGather -= OnFinishGather; + } + + private void OnFinishGather() + { + if (!string.IsNullOrEmpty(gatherCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(gatherCondition, true); + } + } + } +} diff --git a/NewHorizons/External/Configs/PlanetConfig.cs b/NewHorizons/External/Configs/PlanetConfig.cs index c5ef092c..6184ed36 100644 --- a/NewHorizons/External/Configs/PlanetConfig.cs +++ b/NewHorizons/External/Configs/PlanetConfig.cs @@ -102,6 +102,11 @@ namespace NewHorizons.External.Configs /// public DreamModule Dream; + /// + /// Add features exclusive to the Eye of the Universe scene + /// + public EyeOfTheUniverseModule EyeOfTheUniverse; + /// /// Make this body into a focal point (barycenter) /// diff --git a/NewHorizons/External/Modules/EyeOfTheUniverseModule.cs b/NewHorizons/External/Modules/EyeOfTheUniverseModule.cs new file mode 100644 index 00000000..58e878c1 --- /dev/null +++ b/NewHorizons/External/Modules/EyeOfTheUniverseModule.cs @@ -0,0 +1,24 @@ +using NewHorizons.External.Modules.Props.EyeOfTheUniverse; +using Newtonsoft.Json; + +namespace NewHorizons.External.Modules +{ + [JsonObject] + public class EyeOfTheUniverseModule + { + /// + /// Add custom travelers to the campfire sequence + /// + public EyeTravelerInfo[] eyeTravelers; + + /// + /// Add instrument zones which contain puzzles to gather a quantum instrument. You can parent other props to these with `parentPath` + /// + public InstrumentZoneInfo[] instrumentZones; + + /// + /// Add quantum instruments which cause their associated eye traveler to appear and instrument zones to disappear + /// + public QuantumInstrumentInfo[] quantumInstruments; + } +} diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs new file mode 100644 index 00000000..8f4ee0c2 --- /dev/null +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs @@ -0,0 +1,45 @@ +using NewHorizons.External.Modules.Props.Dialogue; +using Newtonsoft.Json; + +namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse +{ + [JsonObject] + public class EyeTravelerInfo : DetailInfo + { + /// + /// A unique ID to associate this traveler with their corresponding quantum instruments and instrument zones. Must be unique for each traveler. + /// + public string id; + + /// + /// The dialogue condition that will trigger the traveler to start playing their instrument. Must be unique for each traveler. + /// + public string startPlayingCondition; + + /// + /// If specified, this dialogue condition must be set for the traveler to participate in the campfire song. Otherwise, the song will be able to start without them. + /// + public string participatingCondition; + + /// + /// The audio to use for the traveler while playing around the campfire (and also for their paired quantum instrument). It should be 16 measures at 92 BPM (approximately 42 seconds long). Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// + public string loopAudio; + + /// + /// The audio to use for the traveler during the finale of the campfire song. It should be 8 measures of the main loop at 92 BPM followed by 2 measures of fade-out (approximately 26 seconds long in total). Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// + public string finaleAudio; + + /// + /// The frequency ID of the signal emitted by the traveler. The built-in game values are `Default`, `Traveler`, `Quantum`, `EscapePod`, + /// `Statue`, `WarpCore`, `HideAndSeek`, and `Radio`. Defaults to `Traveler`. You can also put a custom value. + /// + public string frequency; + + /// + /// The dialogue to use for this traveler. Omit this or set it to null if your traveler already has valid dialogue. + /// + public DialogueInfo dialogue; + } +} diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/InstrumentZoneInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/InstrumentZoneInfo.cs new file mode 100644 index 00000000..047babb9 --- /dev/null +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/InstrumentZoneInfo.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse +{ + [JsonObject] + public class InstrumentZoneInfo : DetailInfo + { + /// + /// The unique ID of the Eye Traveler associated with this instrument zone. + /// + public string id; + } +} diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs new file mode 100644 index 00000000..4e03728f --- /dev/null +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; +using System.ComponentModel; + +namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse +{ + [JsonObject] + public class QuantumInstrumentInfo : DetailInfo + { + /// + /// The unique ID of the Eye Traveler associated with this quantum instrument. + /// + public string id; + + /// + /// A dialogue condition to set when gathering this quantum instrument. Use it in conjunction with `activationCondition` or `deactivationCondition` on other details. + /// + public string gatherCondition; + + /// + /// Allows gathering this quantum instrument using the zoomed-in signalscope, like Chert's bongos. + /// + public bool gatherWithScope; + + /// + /// The radius of the added sphere collider that will be used for interaction. + /// + [DefaultValue(0.5f)] public float interactRadius = 0.5f; + + /// + /// The furthest distance where the player can interact with this quantum instrument. + /// + [DefaultValue(2f)] public float interactRange = 2f; + } +} diff --git a/NewHorizons/Handlers/EyeSceneHandler.cs b/NewHorizons/Handlers/EyeSceneHandler.cs index a0c925a9..16fd0194 100644 --- a/NewHorizons/Handlers/EyeSceneHandler.cs +++ b/NewHorizons/Handlers/EyeSceneHandler.cs @@ -1,14 +1,57 @@ using NewHorizons.Builder.General; using NewHorizons.Components.EyeOfTheUniverse; using NewHorizons.Components.Stars; +using NewHorizons.External.Modules.Props.EyeOfTheUniverse; using NewHorizons.External.SerializableData; using NewHorizons.Utility; +using NewHorizons.Utility.OWML; +using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace NewHorizons.Handlers { public static class EyeSceneHandler { + private static Dictionary _eyeTravelers = new(); + private static EyeMusicController _eyeMusicController; + + public static void Init() + { + _eyeTravelers.Clear(); + _eyeMusicController = null; + } + + public static EyeMusicController GetMusicController() + { + return _eyeMusicController; + } + + public static EyeTravelerData GetOrCreateEyeTravelerData(string id) + { + if (_eyeTravelers.TryGetValue(id, out EyeTravelerData traveler)) + { + return traveler; + } + traveler = new EyeTravelerData() + { + id = id, + info = null, + controller = null, + loopAudioSource = null, + finaleAudioSource = null, + instrumentZones = new(), + quantumInstruments = new(), + }; + _eyeTravelers[traveler.id] = traveler; + return traveler; + } + + public static List GetCustomEyeTravelers() + { + return _eyeTravelers.Values.ToList(); + } + public static void OnSceneLoad() { // Create astro objects for eye and vessel because they didn't have them somehow. @@ -134,5 +177,129 @@ namespace NewHorizons.Handlers SunLightEffectsController.AddStar(starController); SunLightEffectsController.AddStarLight(sunLight); } + + public static void SetUpEyeCampfireSequence() + { + _eyeMusicController = new GameObject("EyeMusicController").AddComponent(); + + var quantumCampsiteController = Object.FindObjectOfType(); + var cosmicInflationController = Object.FindObjectOfType(); + + _eyeMusicController.RegisterFinaleSource(cosmicInflationController._travelerFinaleSource); + + foreach (var controller in quantumCampsiteController._travelerControllers) + { + _eyeMusicController.RegisterLoopSource(controller._signal.GetOWAudioSource()); + } + + foreach (var eyeTraveler in _eyeTravelers.Values) + { + if (eyeTraveler.controller != null) + { + ArrayHelpers.Append(ref quantumCampsiteController._travelerControllers, eyeTraveler.controller); + eyeTraveler.controller.OnStartPlaying += quantumCampsiteController.OnTravelerStartPlaying; + + ArrayHelpers.Append(ref cosmicInflationController._travelers, eyeTraveler.controller); + eyeTraveler.controller.OnStartPlaying += cosmicInflationController.OnTravelerStartPlaying; + + ArrayHelpers.Append(ref cosmicInflationController._inflationObjects, eyeTraveler.controller.transform); + } + else + { + NHLogger.LogError($"Missing Eye Traveler for ID \"{eyeTraveler.id}\""); + } + + if (eyeTraveler.loopAudioSource != null) + { + _eyeMusicController.RegisterLoopSource(eyeTraveler.loopAudioSource); + } + if (eyeTraveler.finaleAudioSource != null) + { + _eyeMusicController.RegisterFinaleSource(eyeTraveler.finaleAudioSource); + } + + foreach (var quantumInstrument in eyeTraveler.quantumInstruments) + { + ArrayHelpers.Append(ref quantumInstrument._activateObjects, eyeTraveler.controller.gameObject); + ArrayHelpers.Append(ref quantumInstrument._deactivateObjects, eyeTraveler.instrumentZones.Select(z => z.gameObject)); + + var ancestorInstrumentZone = quantumInstrument.GetComponentInParent(); + if (ancestorInstrumentZone == null) + { + // Quantum instrument is not a child of an instrument zone, so treat it like its own zone + ArrayHelpers.Append(ref quantumCampsiteController._instrumentZones, quantumInstrument.gameObject); + } + } + + foreach (var instrumentZone in eyeTraveler.instrumentZones) + { + instrumentZone.gameObject.SetActive(false); + ArrayHelpers.Append(ref quantumCampsiteController._instrumentZones, instrumentZone.gameObject); + } + } + } + + public static void UpdateTravelerPositions() + { + //if (!GetCustomEyeTravelers().Any()) return; + + var quantumCampsiteController = Object.FindObjectOfType(); + + var travelers = new List() + { + quantumCampsiteController._travelerControllers[0].transform, // Riebeck + quantumCampsiteController._travelerControllers[2].transform, // Chert + quantumCampsiteController._travelerControllers[6].transform, // Esker + quantumCampsiteController._travelerControllers[1].transform, // Felspar + quantumCampsiteController._travelerControllers[3].transform, // Gabbro + }; + + if (quantumCampsiteController._hasMetSolanum) + { + travelers.Add(quantumCampsiteController._travelerControllers[4].transform); // Solanum + } + if (quantumCampsiteController._hasMetPrisoner) + { + travelers.Add(quantumCampsiteController._travelerControllers[5].transform); // Prisoner + } + + // Custom travelers (starting at index 7) + for (int i = 7; i < quantumCampsiteController._travelerControllers.Length; i++) + { + travelers.Add(quantumCampsiteController._travelerControllers[i].transform); + } + + var radius = 2f + 0.2f * travelers.Count; + var angle = Mathf.PI * 2f / travelers.Count; + var index = 0; + + foreach (var traveler in travelers) + { + // Esker isn't at height 0 so we have to do all this + var initialY = traveler.transform.position.y; + var newPos = quantumCampsiteController.transform.TransformPoint(new Vector3( + Mathf.Cos(angle * index) * radius, + 0f, + -Mathf.Sin(angle * index) * radius + )); + newPos.y = initialY; + traveler.transform.position = newPos; + var lookTarget = quantumCampsiteController.transform.position; + lookTarget.y = newPos.y; + traveler.transform.LookAt(lookTarget, traveler.transform.up); + index++; + } + } + + public class EyeTravelerData + { + public string id; + public EyeTravelerInfo info; + public TravelerEyeController controller; + public OWAudioSource loopAudioSource; + public OWAudioSource finaleAudioSource; + public List quantumInstruments = new(); + public List instrumentZones = new(); + } } } diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index fc8b8ae7..27b67234 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -691,6 +691,11 @@ namespace NewHorizons.Handlers atmosphere = AtmosphereBuilder.Make(go, sector, body.Config.Atmosphere, surfaceSize).GetComponentInChildren(); } + if (body.Config.EyeOfTheUniverse != null) + { + EyeOfTheUniverseBuilder.Make(go, sector, body.Config.EyeOfTheUniverse, body); + } + if (body.Config.ParticleFields != null) { EffectsBuilder.Make(go, sector, body.Config); diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index c964744f..64f9a47e 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -436,7 +436,9 @@ namespace NewHorizons if (isEyeOfTheUniverse) { _playerAwake = true; + EyeSceneHandler.Init(); EyeSceneHandler.OnSceneLoad(); + EyeSceneHandler.SetUpEyeCampfireSequence(); } if (isSolarSystem || isEyeOfTheUniverse) diff --git a/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs new file mode 100644 index 00000000..e6e4ce99 --- /dev/null +++ b/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs @@ -0,0 +1,23 @@ +using HarmonyLib; +using NewHorizons.Handlers; +using NewHorizons.Utility.OWML; + +namespace NewHorizons.Patches.EyeScenePatches +{ + [HarmonyPatch(typeof(CosmicInflationController))] + public static class CosmicInflationControllerPatches + { + [HarmonyPostfix] + [HarmonyPatch(nameof(CosmicInflationController.UpdateFormation))] + public static void CosmicInflationController_UpdateFormation(CosmicInflationController __instance) + { + if (__instance._waitForCrossfade) + { + NHLogger.Log($"Hijacking finale cross-fade, NH will handle it"); + __instance._waitForCrossfade = false; + __instance._waitForMusicEnd = true; + EyeSceneHandler.GetMusicController().TransitionToFinale(); + } + } + } +} diff --git a/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs new file mode 100644 index 00000000..93c1b410 --- /dev/null +++ b/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs @@ -0,0 +1,85 @@ +using HarmonyLib; +using NewHorizons.Handlers; +using NewHorizons.Utility.OWML; +using System.Linq; + +namespace NewHorizons.Patches.EyeScenePatches +{ + [HarmonyPatch(typeof(QuantumCampsiteController))] + public static class QuantumCampsiteControllerPatches + { + [HarmonyPostfix] + [HarmonyPatch(nameof(QuantumCampsiteController.Start))] + public static void QuantumCampsiteController_Start() + { + EyeSceneHandler.UpdateTravelerPositions(); + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(QuantumCampsiteController.ActivateRemainingInstrumentZones))] + public static void QuantumCampsiteController_ActivateRemainingInstrumentZones(QuantumCampsiteController __instance) + { + // We modify this array when registering a custom instrument zone but the vanilla method only activates the first 6 + for (int i = 6; i < __instance._instrumentZones.Length; i++) + { + __instance._instrumentZones[i].SetActive(true); + } + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(QuantumCampsiteController.AreAllTravelersGathered))] + public static bool QuantumCampsiteController_AreAllTravelersGathered(QuantumCampsiteController __instance, ref bool __result) + { + bool gatheredAllHearthianTravelers = __instance._travelerControllers.Take(4).All(t => t.gameObject.activeInHierarchy); + if (!gatheredAllHearthianTravelers) + { + NHLogger.LogVerbose(""); + __result = false; + return false; + } + bool needsSolanum = __instance._hasMetSolanum; + bool gatheredSolanum = __instance._travelerControllers[QuantumCampsiteController.SOLANUM_INDEX].gameObject.activeInHierarchy; + if (needsSolanum && !gatheredSolanum) + { + __result = false; + return false; + } + bool needsPrisoner = __instance._hasMetPrisoner && !__instance._hasErasedPrisoner; + bool gatheredPrisoner = __instance._travelerControllers[QuantumCampsiteController.PRISONER_INDEX].gameObject.activeInHierarchy; + if (needsPrisoner && !gatheredPrisoner) + { + __result = false; + return false; + } + foreach (var traveler in EyeSceneHandler.GetCustomEyeTravelers()) + { + bool needsTraveler = true; + if (!string.IsNullOrEmpty(traveler.info.participatingCondition)) + { + needsTraveler = DialogueConditionManager.SharedInstance.GetConditionState(traveler.info.participatingCondition); + } + bool gatheredTraveler = traveler.controller.gameObject.activeInHierarchy; + if (needsTraveler && !gatheredTraveler) + { + __result = false; + return false; + } + } + __result = true; + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(QuantumCampsiteController.OnTravelerStartPlaying))] + public static void OnTravelerStartPlaying(QuantumCampsiteController __instance) + { + if (!__instance._hasJamSessionStarted) + { + NHLogger.Log($"NH is handling Eye sequence music"); + // Jam session is starting, start our custom music handler + EyeSceneHandler.GetMusicController().StartPlaying(); + } + // Letting the original method run in case mods have patched TravelerEyeController.OnStartCosmicJamSession() + } + } +} diff --git a/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs new file mode 100644 index 00000000..8548c244 --- /dev/null +++ b/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs @@ -0,0 +1,17 @@ +using HarmonyLib; + +namespace NewHorizons.Patches.EyeScenePatches +{ + [HarmonyPatch(typeof(TravelerEyeController))] + public static class TravelerEyeControllerPatches + { + [HarmonyPrefix] + [HarmonyPatch(nameof(TravelerEyeController.OnStartCosmicJamSession))] + public static bool TravelerEyeController_OnStartCosmicJamSession(TravelerEyeController __instance) + { + // Not starting the loop audio here; EyeMusicController will handle that + __instance._signal.GetOWAudioSource().SetLocalVolume(0f); + return false; + } + } +}