diff --git a/NewHorizons/Assets/DefaultMapModNoAtmoOutline.png b/NewHorizons/Assets/DefaultMapModNoAtmoOutline.png new file mode 100644 index 00000000..0e3954d3 Binary files /dev/null and b/NewHorizons/Assets/DefaultMapModNoAtmoOutline.png differ diff --git a/NewHorizons/Assets/DefaultMapModePlanetOutline.png b/NewHorizons/Assets/DefaultMapModePlanetOutline.png new file mode 100644 index 00000000..d2534b61 Binary files /dev/null and b/NewHorizons/Assets/DefaultMapModePlanetOutline.png differ diff --git a/NewHorizons/Assets/DefaultMapModeStarOutline.png b/NewHorizons/Assets/DefaultMapModeStarOutline.png new file mode 100644 index 00000000..b002a0b0 Binary files /dev/null and b/NewHorizons/Assets/DefaultMapModeStarOutline.png differ diff --git a/NewHorizons/Assets/textures/CloudEntry_GD_Island_d.png b/NewHorizons/Assets/textures/CloudEntry_GD_Island_d.png new file mode 100644 index 00000000..5ba6654b Binary files /dev/null and b/NewHorizons/Assets/textures/CloudEntry_GD_Island_d.png differ diff --git a/NewHorizons/Assets/textures/CloudExit__PlayerShip_d.png b/NewHorizons/Assets/textures/CloudExit__PlayerShip_d.png new file mode 100644 index 00000000..5e83973d Binary files /dev/null and b/NewHorizons/Assets/textures/CloudExit__PlayerShip_d.png differ diff --git a/NewHorizons/Assets/textures/Ice.png b/NewHorizons/Assets/textures/Ice.png new file mode 100644 index 00000000..c4d945db Binary files /dev/null and b/NewHorizons/Assets/textures/Ice.png differ diff --git a/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d.png b/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d.png index ece877d5..fc670c87 100644 Binary files a/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d.png and b/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d.png differ diff --git a/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d_greyscale.png b/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d_greyscale.png deleted file mode 100644 index e05049b1..00000000 Binary files a/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d_greyscale.png and /dev/null differ diff --git a/NewHorizons/Assets/textures/OceanExit_PlayerShip_d.png b/NewHorizons/Assets/textures/OceanExit_PlayerShip_d.png index d8e33ac1..7acfe355 100644 Binary files a/NewHorizons/Assets/textures/OceanExit_PlayerShip_d.png and b/NewHorizons/Assets/textures/OceanExit_PlayerShip_d.png differ diff --git a/NewHorizons/Assets/textures/OceanExit_PlayerShip_d_greyscale.png b/NewHorizons/Assets/textures/OceanExit_PlayerShip_d_greyscale.png deleted file mode 100644 index bba05cc4..00000000 Binary files a/NewHorizons/Assets/textures/OceanExit_PlayerShip_d_greyscale.png and /dev/null differ diff --git a/NewHorizons/Assets/textures/Quantum.png b/NewHorizons/Assets/textures/Quantum.png new file mode 100644 index 00000000..606b7870 Binary files /dev/null and b/NewHorizons/Assets/textures/Quantum.png differ diff --git a/NewHorizons/Assets/textures/Rocks.png b/NewHorizons/Assets/textures/Rocks.png new file mode 100644 index 00000000..1a2ccf91 Binary files /dev/null and b/NewHorizons/Assets/textures/Rocks.png differ diff --git a/NewHorizons/Builder/Atmosphere/FogBuilder.cs b/NewHorizons/Builder/Atmosphere/FogBuilder.cs index f47e3e29..8a694ac3 100644 --- a/NewHorizons/Builder/Atmosphere/FogBuilder.cs +++ b/NewHorizons/Builder/Atmosphere/FogBuilder.cs @@ -87,6 +87,9 @@ namespace NewHorizons.Builder.Atmosphere MR.material.SetFloat(Radius, atmo.fogSize); MR.material.SetFloat(Density, atmo.fogDensity); MR.material.SetFloat(DensityExponent, 1); + // We apply fogTint to the material and tint the fog ramp, which means the ramp and tint get multiplied together in the shader, so tint is applied twice + // However nobody has visually complained about this, so we don't want to change it until maybe somebody does + // Was previously documented by issue #747. MR.material.SetTexture(ColorRampTexture, colorRampTexture); fogGO.transform.position = planetGO.transform.position; diff --git a/NewHorizons/Builder/Body/AsteroidBeltBuilder.cs b/NewHorizons/Builder/Body/AsteroidBeltBuilder.cs index fdc893c7..263fb0ce 100644 --- a/NewHorizons/Builder/Body/AsteroidBeltBuilder.cs +++ b/NewHorizons/Builder/Body/AsteroidBeltBuilder.cs @@ -39,7 +39,8 @@ namespace NewHorizons.Builder.Body { surfaceGravity = belt.gravity, surfaceSize = size, - gravityFallOff = GravityFallOff.InverseSquared + gravityFallOff = GravityFallOff.InverseSquared, + hasFluidDetector = false }; config.Orbit = new OrbitModule() @@ -95,6 +96,7 @@ namespace NewHorizons.Builder.Body color = new MColor(126, 94, 73) }; } + config.AmbientLights = new[] { new AmbientLightModule() { outerRadius = size * 1.2f, intensity = 0.1f } }; var asteroid = new NewHorizonsBody(config, mod); PlanetCreationHandler.GenerateBody(asteroid); diff --git a/NewHorizons/Builder/Body/Geometry/Icosphere.cs b/NewHorizons/Builder/Body/Geometry/Icosphere.cs index 71233c18..a5da82e6 100644 --- a/NewHorizons/Builder/Body/Geometry/Icosphere.cs +++ b/NewHorizons/Builder/Body/Geometry/Icosphere.cs @@ -100,6 +100,11 @@ namespace NewHorizons.Builder.Body.Geometry mesh.normals = normals; mesh.uv = uvs; + mesh.RecalculateBounds(); + // Unity recalculate normals does not support smooth normals + //mesh.RecalculateNormals(); + mesh.RecalculateTangents(); + return mesh; } diff --git a/NewHorizons/Builder/Body/HeightMapBuilder.cs b/NewHorizons/Builder/Body/HeightMapBuilder.cs index 48e15193..f816ca80 100644 --- a/NewHorizons/Builder/Body/HeightMapBuilder.cs +++ b/NewHorizons/Builder/Body/HeightMapBuilder.cs @@ -11,7 +11,7 @@ namespace NewHorizons.Builder.Body { public static class HeightMapBuilder { - public static Shader PlanetShader; + private static Shader _planetShader; // I hate nested functions okay private static IModBehaviour _currentMod; @@ -62,7 +62,7 @@ namespace NewHorizons.Builder.Body cubeSphere.SetActive(false); cubeSphere.transform.SetParent(sector?.transform ?? planetGO.transform, false); - if (PlanetShader == null) PlanetShader = AssetBundleUtilities.NHAssetBundle.LoadAsset("Assets/Shaders/SphereTextureWrapperTriplanar.shader"); + if (_planetShader == null) _planetShader = AssetBundleUtilities.NHAssetBundle.LoadAsset("Assets/Shaders/SphereTextureWrapperTriplanar.shader"); var stretch = module.stretch != null ? (Vector3)module.stretch : Vector3.one; @@ -115,7 +115,7 @@ namespace NewHorizons.Builder.Body LODCubeSphere.AddComponent().mesh = CubeSphere.Build(resolution, heightMap, module.minHeight, module.maxHeight, stretch); var cubeSphereMR = LODCubeSphere.AddComponent(); - var material = new Material(PlanetShader); + var material = new Material(_planetShader); cubeSphereMR.material = material; material.name = textureMap.name; diff --git a/NewHorizons/Builder/Body/ProcGenBuilder.cs b/NewHorizons/Builder/Body/ProcGenBuilder.cs index e168c678..913ea72c 100644 --- a/NewHorizons/Builder/Body/ProcGenBuilder.cs +++ b/NewHorizons/Builder/Body/ProcGenBuilder.cs @@ -1,21 +1,50 @@ using NewHorizons.Builder.Body.Geometry; using NewHorizons.External.Modules; using NewHorizons.Utility; +using NewHorizons.Utility.Files; +using OWML.Common; +using System.Collections.Generic; using UnityEngine; + namespace NewHorizons.Builder.Body { public static class ProcGenBuilder { - private static Material quantumMaterial; - private static Material iceMaterial; + private static Material _material; + private static Shader _planetShader; - public static GameObject Make(GameObject planetGO, Sector sector, ProcGenModule module) + private static Dictionary _materialCache = new(); + + public static void ClearCache() { - if (quantumMaterial == null) quantumMaterial = SearchUtilities.FindResourceOfTypeAndName("Rock_QM_EyeRock_mat"); - if (iceMaterial == null) iceMaterial = SearchUtilities.FindResourceOfTypeAndName("Rock_BH_IceSpike_mat"); + foreach (var material in _materialCache.Values) + { + Object.Destroy(material); + } + _materialCache.Clear(); + } + private static Material MakeMaterial() + { + var material = new Material(_planetShader); - GameObject icosphere = new GameObject("Icosphere"); + var keyword = "BASE_TILE"; + var prefix = "_BaseTile"; + + material.SetFloat(prefix, 1); + material.EnableKeyword(keyword); + + material.SetTexture("_BlendMap", ImageUtilities.MakeSolidColorTexture(1, 1, Color.white)); + + return material; + } + + public static GameObject Make(IModBehaviour mod, GameObject planetGO, Sector sector, ProcGenModule module) + { + if (_planetShader == null) _planetShader = AssetBundleUtilities.NHAssetBundle.LoadAsset("Assets/Shaders/SphereTextureWrapperTriplanar.shader"); + if (_material == null) _material = MakeMaterial(); + + var icosphere = new GameObject("Icosphere"); icosphere.SetActive(false); icosphere.transform.parent = sector?.transform ?? planetGO.transform; icosphere.transform.rotation = Quaternion.Euler(90, 0, 0); @@ -26,8 +55,61 @@ namespace NewHorizons.Builder.Body icosphere.AddComponent().mesh = mesh; var cubeSphereMR = icosphere.AddComponent(); - cubeSphereMR.material = new Material(Shader.Find("Standard")); - cubeSphereMR.material.color = module.color != null ? module.color.ToColor() : Color.white; + + if (!_materialCache.TryGetValue(module, out var material)) + { + material = new Material(_material); + material.name = planetGO.name; + if (module.material == ProcGenModule.Material.Default) + { + if (!string.IsNullOrEmpty(module.texture)) + { + material.SetTexture($"_BaseTileAlbedo", ImageUtilities.GetTexture(mod, module.texture, wrap: true)); + } + else + { + material.mainTexture = ImageUtilities.MakeSolidColorTexture(1, 1, module.color?.ToColor() ?? Color.white); + } + if (!string.IsNullOrEmpty(module.smoothnessMap)) + { + material.SetTexture($"_BaseTileSmoothnessMap", ImageUtilities.GetTexture(mod, module.smoothnessMap, wrap: true)); + } + if (!string.IsNullOrEmpty(module.normalMap)) + { + material.SetFloat($"_BaseTileBumpStrength", module.normalStrength); + material.SetTexture($"_BaseTileBumpMap", ImageUtilities.GetTexture(mod, module.normalMap, wrap: true)); + } + } + else + { + switch (module.material) + { + case ProcGenModule.Material.Ice: + material.SetTexture($"_BaseTileAlbedo", ImageUtilities.GetTexture(Main.Instance, "Assets/textures/Ice.png", wrap: true)); + break; + case ProcGenModule.Material.Quantum: + material.SetTexture($"_BaseTileAlbedo", ImageUtilities.GetTexture(Main.Instance, "Assets/textures/Quantum.png", wrap: true)); + break; + case ProcGenModule.Material.Rock: + material.SetTexture($"_BaseTileAlbedo", ImageUtilities.GetTexture(Main.Instance, "Assets/textures/Rocks.png", wrap: true)); + break; + default: + break; + } + material.SetFloat($"_BaseTileScale", 5 / module.scale); + if (module.color != null) + { + material.color = module.color.ToColor(); + } + } + + material.SetFloat("_Smoothness", module.smoothness); + material.SetFloat("_Metallic", module.metallic); + + _materialCache[module] = material; + } + + cubeSphereMR.sharedMaterial = material; var cubeSphereMC = icosphere.AddComponent(); cubeSphereMC.sharedMesh = mesh; diff --git a/NewHorizons/Builder/Body/ProxyBuilder.cs b/NewHorizons/Builder/Body/ProxyBuilder.cs index f1567659..3520008d 100644 --- a/NewHorizons/Builder/Body/ProxyBuilder.cs +++ b/NewHorizons/Builder/Body/ProxyBuilder.cs @@ -162,7 +162,7 @@ namespace NewHorizons.Builder.Body GameObject procGen = null; if (body.Config.ProcGen != null) { - procGen = ProcGenBuilder.Make(proxy, null, body.Config.ProcGen); + procGen = ProcGenBuilder.Make(body.Mod, proxy, null, body.Config.ProcGen); if (realSize < body.Config.ProcGen.scale) realSize = body.Config.ProcGen.scale; } diff --git a/NewHorizons/Builder/Props/DetailBuilder.cs b/NewHorizons/Builder/Props/DetailBuilder.cs index 2de1ccbd..a395e080 100644 --- a/NewHorizons/Builder/Props/DetailBuilder.cs +++ b/NewHorizons/Builder/Props/DetailBuilder.cs @@ -56,6 +56,7 @@ namespace NewHorizons.Builder.Props private static void SceneManager_sceneUnloaded(Scene scene) { + // would be nice to only clear when system changes, but fixed prefabs rely on stuff in the scene foreach (var prefab in _fixedPrefabCache.Values) { UnityEngine.Object.Destroy(prefab.prefab); @@ -155,6 +156,17 @@ namespace NewHorizons.Builder.Props continue; } + /* We used to set SectorCullGroup._controllingProxy to null. Now we do not. + * This may break things on copied details because it prevents SetSector from doing anything, + * so that part of the detail might be culled by wrong sector. + * So if you copy something from e.g. Giants Deep it might turn off the detail if you arent in + * the sector of the thing you copied from (since it's still pointing to the original proxy, + * which has the original sector at giants deep there) + * + * Anyway nobody has complained about this for the year it's been like that so closing issue #831 until + * this affects somebody + */ + FixSectoredComponent(component, sector, existingSectors); } @@ -219,15 +231,10 @@ namespace NewHorizons.Builder.Props if (detail.removeChildren != null) { - var detailPath = prop.transform.GetPath(); - var transforms = prop.GetComponentsInChildren(true); foreach (var childPath in detail.removeChildren) { - // Multiple children can have the same path so we delete all that match - var path = $"{detailPath}/{childPath}"; - var flag = true; - foreach (var childObj in transforms.Where(x => x.GetPath() == path)) + foreach (var childObj in prop.transform.FindAll(childPath)) { flag = false; childObj.gameObject.SetActive(false); @@ -263,7 +270,7 @@ namespace NewHorizons.Builder.Props UnityEngine.Object.DestroyImmediate(prop); prop = newDetailGO; } - + if (isItem) { // Else when you put them down you can't pick them back up @@ -275,7 +282,7 @@ namespace NewHorizons.Builder.Props // For DLC related props // Make sure to do this before its set active - if (!string.IsNullOrEmpty(detail?.path) && + if (!string.IsNullOrEmpty(detail?.path) && (detail.path.ToLowerInvariant().StartsWith("ringworld") || detail.path.ToLowerInvariant().StartsWith("dreamworld"))) { prop.AddComponent()._destroyOnDLCNotOwned = true; @@ -294,7 +301,7 @@ namespace NewHorizons.Builder.Props if (!string.IsNullOrEmpty(detail.activationCondition)) { - ConditionalObjectActivation.SetUp(prop, detail.activationCondition, detail.blinkWhenActiveChanged, true); + ConditionalObjectActivation.SetUp(prop, detail.activationCondition, detail.blinkWhenActiveChanged, true); } if (!string.IsNullOrEmpty(detail.deactivationCondition)) { @@ -568,22 +575,22 @@ namespace NewHorizons.Builder.Props // Manually copied these values from a artifact lantern so that we don't have to find it (works in Eye) lantern._origLensFlareBrightness = 0f; - lantern._focuserPetalsBaseEulerAngles = new Vector3[] - { - new Vector3(0.7f, 270.0f, 357.5f), - new Vector3(288.7f, 270.1f, 357.4f), + lantern._focuserPetalsBaseEulerAngles = new Vector3[] + { + new Vector3(0.7f, 270.0f, 357.5f), + new Vector3(288.7f, 270.1f, 357.4f), new Vector3(323.3f, 90.0f, 177.5f), - new Vector3(35.3f, 90.0f, 177.5f), - new Vector3(72.7f, 270.1f, 357.5f) + new Vector3(35.3f, 90.0f, 177.5f), + new Vector3(72.7f, 270.1f, 357.5f) }; lantern._dirtyFlag_focus = true; - lantern._concealerRootsBaseScale = new Vector3[] + lantern._concealerRootsBaseScale = new Vector3[] { Vector3.one, Vector3.one, Vector3.one }; - lantern._concealerCoversStartPos = new Vector3[] + lantern._concealerCoversStartPos = new Vector3[] { new Vector3(0.0f, 0.0f, 0.0f), new Vector3(0.0f, -0.1f, 0.0f), @@ -594,7 +601,7 @@ namespace NewHorizons.Builder.Props }; lantern._dirtyFlag_concealment = true; lantern.UpdateVisuals(); - + Destroy(this); } } diff --git a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs index b8c84b62..f3db82e9 100644 --- a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs +++ b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs @@ -25,7 +25,7 @@ namespace NewHorizons.Builder.Props travelerData.requirementsMet = false; } - if (!string.IsNullOrEmpty(info.requiredPersistentCondition) && !DialogueConditionManager.SharedInstance.GetConditionState(info.requiredPersistentCondition)) + if (!string.IsNullOrEmpty(info.requiredPersistentCondition) && !PlayerData.GetPersistentCondition(info.requiredPersistentCondition)) { travelerData.requirementsMet = false; } diff --git a/NewHorizons/Builder/ShipLog/MapModeBuilder.cs b/NewHorizons/Builder/ShipLog/MapModeBuilder.cs index 9d66447f..de4116aa 100644 --- a/NewHorizons/Builder/ShipLog/MapModeBuilder.cs +++ b/NewHorizons/Builder/ShipLog/MapModeBuilder.cs @@ -1,14 +1,12 @@ using NewHorizons.Components.ShipLog; using NewHorizons.External; using NewHorizons.External.Modules; -using NewHorizons.External.Modules.VariableSize; using NewHorizons.Handlers; using NewHorizons.Utility; using NewHorizons.Utility.Files; using NewHorizons.Utility.OuterWilds; using NewHorizons.Utility.OWML; using OWML.ModHelper; -using System; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -28,7 +26,7 @@ namespace NewHorizons.Builder.ShipLog if (_astroObjectToMapModeInfo.TryGetValue(slao, out var mapModeInfo)) { return mapModeInfo; - } + } else { return null; @@ -149,7 +147,7 @@ namespace NewHorizons.Builder.ShipLog Rect rect = new Rect(0, 0, texture.width, texture.height); Vector2 pivot = new Vector2(texture.width / 2, texture.height / 2); - newImage.sprite = Sprite.Create(texture, rect, pivot); + newImage.sprite = Sprite.Create(texture, rect, pivot, 100, 0, SpriteMeshType.FullRect, Vector4.zero, false); return newImageGO; } @@ -190,15 +188,15 @@ namespace NewHorizons.Builder.ShipLog Texture2D image = null; Texture2D outline = null; - + string imagePath = body.Config.ShipLog?.mapMode?.revealedSprite; string outlinePath = body.Config.ShipLog?.mapMode?.outlineSprite; if (imagePath != null) image = ImageUtilities.GetTexture(body.Mod, imagePath); - if (image == null) image = AutoGenerateMapModePicture(body); + if (image == null) image = ImageUtilities.AutoGenerateMapModePicture(body); if (outlinePath != null) outline = ImageUtilities.GetTexture(body.Mod, outlinePath); - if (outline == null) outline = ImageUtilities.MakeOutline(image, Color.white, 10); + if (outline == null) outline = ImageUtilities.GetCachedOutlineOrCreate(body, image, imagePath); astroObject._imageObj = CreateImage(gameObject, image, body.Config.name + " Revealed", layer); astroObject._outlineObj = CreateImage(gameObject, outline, body.Config.name + " Outline", layer); @@ -246,10 +244,10 @@ namespace NewHorizons.Builder.ShipLog string outlinePath = info.outlineSprite; if (imagePath != null) image = ImageUtilities.GetTexture(body.Mod, imagePath); - else image = AutoGenerateMapModePicture(body); + else image = ImageUtilities.AutoGenerateMapModePicture(body); if (outlinePath != null) outline = ImageUtilities.GetTexture(body.Mod, outlinePath); - else outline = ImageUtilities.MakeOutline(image, Color.white, 10); + else outline = ImageUtilities.GetCachedOutlineOrCreate(body, image, imagePath); Image revealedImage = CreateImage(detailGameObject, image, "Detail Revealed", parent.gameObject.layer).GetComponent(); Image outlineImage = CreateImage(detailGameObject, outline, "Detail Outline", parent.gameObject.layer).GetComponent(); @@ -591,7 +589,7 @@ namespace NewHorizons.Builder.ShipLog GameObject newNodeGO = CreateMapModeGameObject(node.mainBody, parent, layer, position); ShipLogAstroObject astroObject = AddShipLogAstroObject(newNodeGO, node.mainBody, greyScaleMaterial, layer); if (node.mainBody.Config.FocalPoint != null) - { + { astroObject._imageObj.GetComponent().enabled = false; astroObject._outlineObj.GetComponent().enabled = false; astroObject._unviewedObj.GetComponent().enabled = false; @@ -605,68 +603,6 @@ namespace NewHorizons.Builder.ShipLog } #endregion - private static Texture2D AutoGenerateMapModePicture(NewHorizonsBody body) - { - Texture2D texture; - - if (body.Config.Star != null) texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModeStar.png"); - else if (body.Config.Atmosphere != null) texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModNoAtmo.png"); - else texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModePlanet.png"); - - var color = GetDominantPlanetColor(body); - var darkColor = new Color(color.r / 3f, color.g / 3f, color.b / 3f); - - texture = ImageUtilities.LerpGreyscaleImage(texture, color, darkColor); - - return texture; - } - - private static Color GetDominantPlanetColor(NewHorizonsBody body) - { - try - { - var starColor = body.Config?.Star?.tint; - if (starColor != null) return starColor.ToColor(); - - var atmoColor = body.Config.Atmosphere?.atmosphereTint; - if (body.Config.Atmosphere?.clouds != null && atmoColor != null) return atmoColor.ToColor(); - - if (body.Config?.HeightMap?.textureMap != null) - { - try - { - var texture = ImageUtilities.GetTexture(body.Mod, body.Config.HeightMap.textureMap); - var landColor = ImageUtilities.GetAverageColor(texture); - if (landColor != null) return landColor; - } - catch (Exception) { } - } - - var waterColor = body.Config.Water?.tint; - if (waterColor != null) return waterColor.ToColor(); - - var lavaColor = body.Config.Lava?.tint; - if (lavaColor != null) return lavaColor.ToColor(); - - var sandColor = body.Config.Sand?.tint; - if (sandColor != null) return sandColor.ToColor(); - - switch (body.Config?.Props?.singularities?.FirstOrDefault()?.type) - { - case SingularityModule.SingularityType.BlackHole: - return Color.black; - case SingularityModule.SingularityType.WhiteHole: - return Color.white; - } - } - catch (Exception) - { - NHLogger.LogWarning($"Something went wrong trying to pick the colour for {body.Config.name} but I'm too lazy to fix it."); - } - - return Color.white; - } - #region Replacement private static List<(NewHorizonsBody, ModBehaviour, MapModeInfo)> _mapModIconsToUpdate = new(); public static void TryReplaceExistingMapModeIcon(NewHorizonsBody body, ModBehaviour mod, MapModeInfo info) @@ -687,7 +623,7 @@ namespace NewHorizons.Builder.ShipLog } private static void ReplaceExistingMapModeIcon(NewHorizonsBody body, ModBehaviour mod, MapModeInfo info) - { + { var astroObject = _astroObjectToShipLog[body.Object]; var gameObject = astroObject.gameObject; var layer = gameObject.layer; diff --git a/NewHorizons/Builder/ShipLog/RumorModeBuilder.cs b/NewHorizons/Builder/ShipLog/RumorModeBuilder.cs index 7f4d28f2..daab11bb 100644 --- a/NewHorizons/Builder/ShipLog/RumorModeBuilder.cs +++ b/NewHorizons/Builder/ShipLog/RumorModeBuilder.cs @@ -246,7 +246,7 @@ namespace NewHorizons.Builder.ShipLog Texture2D newTexture = ImageUtilities.GetTexture(body.Mod, relativePath); Rect rect = new Rect(0, 0, newTexture.width, newTexture.height); Vector2 pivot = new Vector2(newTexture.width / 2, newTexture.height / 2); - return Sprite.Create(newTexture, rect, pivot); + return Sprite.Create(newTexture, rect, pivot, 100, 0, SpriteMeshType.FullRect, Vector4.zero, false); } catch (Exception) { diff --git a/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs b/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs index 70093cc4..81c3c6ce 100644 --- a/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs @@ -11,9 +11,8 @@ namespace NewHorizons.Builder.Volumes { var volume = VolumeBuilder.Make(planetGO, sector, info); - volume.creditsType = info.creditsType; - volume.gameOverText = info.gameOverText; - volume.deathType = EnumUtils.Parse(info.deathType.ToString(), DeathType.Default); + volume.gameOver = info.gameOver; + volume.deathType = info.deathType == null ? null : EnumUtils.Parse(info.deathType.ToString(), DeathType.Default); return volume; } diff --git a/NewHorizons/Components/NHGameOverManager.cs b/NewHorizons/Components/NHGameOverManager.cs new file mode 100644 index 00000000..93a7339a --- /dev/null +++ b/NewHorizons/Components/NHGameOverManager.cs @@ -0,0 +1,138 @@ +using NewHorizons.External.Modules; +using NewHorizons.External.SerializableEnums; +using NewHorizons.Handlers; +using NewHorizons.Utility.OWML; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace NewHorizons.Components +{ + public class NHGameOverManager : MonoBehaviour + { + /// + /// Mod unique id to game over module list + /// Done as a dictionary so that Reload Configs can overwrite entries per mod + /// + public static Dictionary gameOvers = new(); + + public static NHGameOverManager Instance { get; private set; } + + private GameOverController _gameOverController; + private PlayerCameraEffectController _playerCameraEffectController; + + private GameOverModule[] _gameOvers; + + private bool _gameOverSequenceStarted; + + public void Awake() + { + Instance = this; + } + + public void Start() + { + _gameOverController = FindObjectOfType(); + _playerCameraEffectController = FindObjectOfType(); + + _gameOvers = gameOvers.SelectMany(x => x.Value).ToArray(); + } + + public void TryHijackDeathSequence() + { + var gameOver = _gameOvers.FirstOrDefault(x => !string.IsNullOrEmpty(x.condition) && DialogueConditionManager.SharedInstance.GetConditionState(x.condition)); + if (!_gameOverSequenceStarted && gameOver != null && !Locator.GetDeathManager()._finishedDLC) + { + StartGameOverSequence(gameOver, null); + } + } + + public void StartGameOverSequence(GameOverModule gameOver, DeathType? deathType) + { + _gameOverSequenceStarted = true; + Delay.StartCoroutine(GameOver(gameOver, deathType)); + } + + private IEnumerator GameOver(GameOverModule gameOver, DeathType? deathType) + { + OWInput.ChangeInputMode(InputMode.None); + ReticleController.Hide(); + Locator.GetPromptManager().SetPromptsVisible(false); + Locator.GetPauseCommandListener().AddPauseCommandLock(); + + // The PlayerCameraEffectController is what actually kills us, so convince it we're already dead + Locator.GetDeathManager()._isDead = true; + + var fadeLength = 2f; + + if (Locator.GetDeathManager()._isDying) + { + // Player already died at this point, so don't fade + fadeLength = 0f; + } + else if (deathType is DeathType nonNullDeathType) + { + _playerCameraEffectController.OnPlayerDeath(nonNullDeathType); + fadeLength = _playerCameraEffectController._deathFadeLength; + } + else + { + // Wake up relaxed next loop + PlayerData.SetLastDeathType(DeathType.Meditation); + FadeHandler.FadeOut(fadeLength); + } + + yield return new WaitForSeconds(fadeLength); + + if (!string.IsNullOrEmpty(gameOver.text) && _gameOverController != null) + { + _gameOverController._deathText.text = TranslationHandler.GetTranslation(gameOver.text, TranslationHandler.TextType.UI); + _gameOverController.SetupGameOverScreen(5f); + + if (gameOver.colour != null) + { + _gameOverController._deathText.color = gameOver.colour.ToColor(); + } + + // Make sure the fade handler is off now + FadeHandler.FadeIn(0f); + + // We set this to true to stop it from loading the credits scene, so we can do it ourselves + _gameOverController._loading = true; + + yield return new WaitUntil(ReadytoLoadCreditsScene); + } + + LoadCreditsScene(gameOver); + } + + private bool ReadytoLoadCreditsScene() => _gameOverController._fadedOutText && _gameOverController._textAnimator.IsComplete(); + + private void LoadCreditsScene(GameOverModule gameOver) + { + NHLogger.LogVerbose($"Load credits {gameOver.creditsType}"); + + switch (gameOver.creditsType) + { + case NHCreditsType.Fast: + LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack); + break; + case NHCreditsType.Final: + LoadManager.LoadScene(OWScene.Credits_Final, LoadManager.FadeType.ToBlack); + break; + case NHCreditsType.Kazoo: + TimelineObliterationController.s_hasRealityEnded = true; + LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack); + break; + default: + // GameOverController disables post processing + _gameOverController._flashbackCamera.postProcessing.enabled = true; + // For some reason this isn't getting set sometimes + AudioListener.volume = 1; + GlobalMessenger.FireEvent("TriggerFlashback"); + break; + } + } + } +} diff --git a/NewHorizons/Components/Props/ConditionalObjectActivation.cs b/NewHorizons/Components/Props/ConditionalObjectActivation.cs index a0322ecc..4036d610 100644 --- a/NewHorizons/Components/Props/ConditionalObjectActivation.cs +++ b/NewHorizons/Components/Props/ConditionalObjectActivation.cs @@ -13,7 +13,7 @@ namespace NewHorizons.Components.Props public bool CloseEyes; public bool SetActiveWithCondition; - private PlayerCameraEffectController _playerCameraEffectController; + private static PlayerCameraEffectController _playerCameraEffectController; private bool _changeConditionOnExitConversation; private bool _inConversation; @@ -45,7 +45,7 @@ namespace NewHorizons.Components.Props public void Awake() { - _playerCameraEffectController = GameObject.FindObjectOfType(); + if (_playerCameraEffectController == null) _playerCameraEffectController = GameObject.FindObjectOfType(); GlobalMessenger.AddListener("DialogueConditionChanged", OnDialogueConditionChanged); GlobalMessenger.AddListener("ExitConversation", OnExitConversation); GlobalMessenger.AddListener("EnterConversation", OnEnterConversation); diff --git a/NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs b/NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs index cc5636eb..0ee7a574 100644 --- a/NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs +++ b/NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs @@ -63,7 +63,7 @@ namespace NewHorizons.Components.ShipLog } } - /* + /* if(VesselCoordinatePromptHandler.KnowsEyeCoordinates()) { AddSystemCard("EyeOfTheUniverse"); @@ -279,7 +279,7 @@ namespace NewHorizons.Components.ShipLog { var rect = new Rect(0, 0, texture.width, texture.height); var pivot = new Vector2(texture.width / 2, texture.height / 2); - return Sprite.Create(texture, rect, pivot); + return Sprite.Create(texture, rect, pivot, 100, 0, SpriteMeshType.FullRect, Vector4.zero, false); } private void OnTargetReferenceFrame(ReferenceFrame referenceFrame) diff --git a/NewHorizons/Components/SplashColourizer.cs b/NewHorizons/Components/SplashColourizer.cs new file mode 100644 index 00000000..140704b4 --- /dev/null +++ b/NewHorizons/Components/SplashColourizer.cs @@ -0,0 +1,265 @@ +using NewHorizons.External.Configs; +using NewHorizons.External.SerializableData; +using NewHorizons.Utility; +using NewHorizons.Utility.Files; +using NewHorizons.Utility.OuterWilds; +using NewHorizons.Utility.OWML; +using System.Collections.Generic; +using UnityEngine; + +namespace NewHorizons.Components; + +/// +/// When a Fluid Detector enters this volume, it's splash effects will get colourized to match whats on this planet +/// +public class SplashColourizer : MonoBehaviour +{ + public float _radius; + + private SphereShape _sphereShape; + + private Dictionary _cachedOriginalPrefabs = new(); + private Dictionary _cachedModifiedPrefabs = new(); + + private FluidDetector _playerDetector, _shipDetector, _probeDetector; + + private MColor _waterColour, _cloudColour, _plasmaColour, _sandColour; + + private GameObject _prefabHolder; + + private bool _probeInsideVolume; + + private List _customTextures = new(); + + public void Awake() + { + var volume = new GameObject("Volume"); + volume.transform.parent = transform; + volume.transform.localPosition = Vector3.zero; + volume.layer = Layer.BasicEffectVolume; + + _sphereShape = gameObject.AddComponent(); + _sphereShape.radius = _radius; + + volume.AddComponent(); + + _prefabHolder = new GameObject("Prefabs"); + _prefabHolder.SetActive(false); + } + + public static void Make(GameObject planet, PlanetConfig config, float soi) + { + var water = config.Water?.tint; + var cloud = config.Atmosphere?.clouds?.tint; + var plasma = config.Lava?.tint ?? config.Star?.tint; + var sand = config.Sand?.tint; + + if (water != null || cloud != null || plasma != null || sand != null) + { + var size = Mathf.Max( + soi / 1.5f, + config.Water?.size ?? 0f, + config.Atmosphere?.clouds?.outerCloudRadius ?? 0f, + config.Lava?.size ?? 0f, + config.Star?.size ?? 0f, + config.Sand?.size ?? 0f + ) * 1.5f; + + var colourizer = planet.AddComponent(); + + colourizer._radius = size; + if (colourizer._sphereShape != null) colourizer._sphereShape.radius = size; + + colourizer._waterColour = water; + colourizer._cloudColour = cloud; + colourizer._plasmaColour = plasma; + colourizer._sandColour = sand; + } + } + + public void Start() + { + // Cache all prefabs + CachePrefabs(_playerDetector = Locator.GetPlayerDetector().GetComponent()); + CachePrefabs(_shipDetector = Locator.GetShipDetector().GetComponent()); + CachePrefabs(_probeDetector = Locator.GetProbe().GetDetectorObject().GetComponent()); + + GlobalMessenger.AddListener("RetrieveProbe", OnRetrieveProbe); + + // Check if player/ship are already inside + if ((_playerDetector.transform.position - transform.position).magnitude < _radius) + { + SetSplashEffects(_playerDetector, true); + } + if ((_shipDetector.transform.position - transform.position).magnitude < _radius) + { + SetSplashEffects(_shipDetector, true); + } + } + + public void OnDestroy() + { + GlobalMessenger.RemoveListener("RetrieveProbe", OnRetrieveProbe); + } + + private void OnRetrieveProbe(SurveyorProbe probe) + { + if (_probeInsideVolume) + { + // Else it never leaves the volume + SetProbeSplashEffects(false); + } + } + + public void OnTriggerEnter(Collider hitCollider) => OnEnterExit(hitCollider, true); + + public void OnTriggerExit(Collider hitCollider) => OnEnterExit(hitCollider, false); + + /// + /// The probe keeps being null idgi + /// + /// + private bool IsProbeLaunched() + { + return Locator.GetProbe()?.IsLaunched() ?? false; + } + + private void OnEnterExit(Collider hitCollider, bool entering) + { + if (!enabled) return; + + if (hitCollider.attachedRigidbody != null) + { + var isPlayer = hitCollider.attachedRigidbody.CompareTag("Player"); + var isShip = hitCollider.attachedRigidbody.CompareTag("Ship"); + var isProbe = hitCollider.attachedRigidbody.CompareTag("Probe"); + + if (isPlayer) + { + SetSplashEffects(_playerDetector, entering); + if (IsProbeLaunched()) + { + SetProbeSplashEffects(entering); + } + } + else if (isShip) + { + SetSplashEffects(_shipDetector, entering); + if (PlayerState.IsInsideShip()) + { + SetSplashEffects(_playerDetector, entering); + } + if (IsProbeLaunched()) + { + SetProbeSplashEffects(entering); + } + } + else if (isProbe) + { + SetProbeSplashEffects(entering); + } + + // If the probe isn't launched we always consider it as being inside the volume + if (isProbe || !IsProbeLaunched()) + { + _probeInsideVolume = entering; + } + } + } + + public void CachePrefabs(FluidDetector detector) + { + foreach (var splashEffect in detector._splashEffects) + { + if (!_cachedOriginalPrefabs.ContainsKey(splashEffect)) + { + _cachedOriginalPrefabs[splashEffect] = splashEffect.splashPrefab; + } + if (!_cachedModifiedPrefabs.ContainsKey(splashEffect)) + { + Color? colour = null; + if (splashEffect.fluidType == FluidVolume.Type.CLOUD) + { + colour = _cloudColour?.ToColor(); + } + switch(splashEffect.fluidType) + { + case FluidVolume.Type.CLOUD: + colour = _cloudColour?.ToColor(); + break; + case FluidVolume.Type.WATER: + colour = _waterColour?.ToColor(); + break; + case FluidVolume.Type.PLASMA: + colour = _plasmaColour?.ToColor(); + break; + case FluidVolume.Type.SAND: + colour = _sandColour?.ToColor(); + break; + } + + if (colour is Color tint) + { + var flagError = false; + var prefab = splashEffect.splashPrefab.InstantiateInactive(); + var meshRenderers = prefab.GetComponentsInChildren(true); + foreach (var meshRenderer in meshRenderers) + { + if (_customTextures.Contains(meshRenderer.material.mainTexture)) + { + // Might be some shared material stuff? This image is already tinted, so skip it + continue; + } + + // Can't access the textures in memory so we need to have our own copies + var texture = ImageUtilities.GetTexture(Main.Instance, $"Assets/textures/{meshRenderer.material.mainTexture.name}.png"); + if (texture == null) + { + NHLogger.LogError($"Go tell an NH dev to add this image texture to the mod because I can't be bothered to until somebody complains: {meshRenderer.material.mainTexture.name}"); + GameObject.Destroy(prefab); + flagError = true; + } + + _customTextures.Add(texture); + + meshRenderer.material = new(meshRenderer.material) + { + color = Color.white, + mainTexture = ImageUtilities.TintImage(texture, tint) + }; + } + + if (flagError) continue; + + // Have to be active when being used by the base game classes but can't be seen + // Keep them active as children of an inactive game object + prefab.transform.parent = _prefabHolder.transform; + prefab.SetActive(true); + + _cachedModifiedPrefabs[splashEffect] = prefab; + } + } + } + } + + public void SetSplashEffects(FluidDetector detector, bool entering) + { + NHLogger.LogVerbose($"Body {detector.name} {(entering ? "entered" : "left")} colourizing volume on {name}"); + + foreach (var splashEffect in detector._splashEffects) + { + var prefabs = entering ? _cachedModifiedPrefabs : _cachedOriginalPrefabs; + if (prefabs.TryGetValue(splashEffect, out var prefab)) + { + splashEffect.splashPrefab = prefab; + } + } + } + + public void SetProbeSplashEffects(bool entering) + { + _probeInsideVolume = entering; + + SetSplashEffects(_probeDetector, entering); + } +} diff --git a/NewHorizons/Components/Volumes/BlackHoleWarpVolume.cs b/NewHorizons/Components/Volumes/BlackHoleWarpVolume.cs index 941bff2b..ba31f97e 100644 --- a/NewHorizons/Components/Volumes/BlackHoleWarpVolume.cs +++ b/NewHorizons/Components/Volumes/BlackHoleWarpVolume.cs @@ -21,6 +21,7 @@ namespace NewHorizons.Components.Volumes public override void VanishPlayer(OWRigidbody playerBody, RelativeLocationData entryLocation) { Locator.GetPlayerAudioController().PlayOneShotInternal(AudioType.BH_BlackHoleEmission); + FadeHandler.FadeOut(0.2f, false); Main.Instance.ChangeCurrentStarSystem(TargetSolarSystem, PlayerState.AtFlightConsole()); PlayerSpawnHandler.TargetSpawnID = TargetSpawnID; } diff --git a/NewHorizons/Components/Volumes/LoadCreditsVolume.cs b/NewHorizons/Components/Volumes/LoadCreditsVolume.cs index 18edcbd1..26f75831 100644 --- a/NewHorizons/Components/Volumes/LoadCreditsVolume.cs +++ b/NewHorizons/Components/Volumes/LoadCreditsVolume.cs @@ -1,8 +1,4 @@ -using NewHorizons.External.SerializableEnums; -using NewHorizons.Handlers; -using NewHorizons.Utility; -using NewHorizons.Utility.OWML; -using System.Collections; +using NewHorizons.External.Modules; using UnityEngine; @@ -10,78 +6,17 @@ namespace NewHorizons.Components.Volumes { internal class LoadCreditsVolume : BaseVolume { - public NHCreditsType creditsType = NHCreditsType.Fast; - - public string gameOverText; - public DeathType deathType = DeathType.Default; - - private GameOverController _gameOverController; - private PlayerCameraEffectController _playerCameraEffectController; - - public void Start() - { - _gameOverController = FindObjectOfType(); - _playerCameraEffectController = FindObjectOfType(); - } + public GameOverModule gameOver; + public DeathType? deathType; public override void OnTriggerVolumeEntry(GameObject hitObj) { - if (hitObj.CompareTag("PlayerDetector") && enabled) + if (hitObj.CompareTag("PlayerDetector") && enabled && (string.IsNullOrEmpty(gameOver.condition) || DialogueConditionManager.SharedInstance.GetConditionState(gameOver.condition))) { - // Have to run it off the mod behaviour since the game over controller disables everything - Delay.StartCoroutine(GameOver()); + NHGameOverManager.Instance.StartGameOverSequence(gameOver, deathType); } } - private IEnumerator GameOver() - { - OWInput.ChangeInputMode(InputMode.None); - ReticleController.Hide(); - Locator.GetPromptManager().SetPromptsVisible(false); - Locator.GetPauseCommandListener().AddPauseCommandLock(); - - // The PlayerCameraEffectController is what actually kills us, so convince it we're already dead - Locator.GetDeathManager()._isDead = true; - - _playerCameraEffectController.OnPlayerDeath(deathType); - - yield return new WaitForSeconds(_playerCameraEffectController._deathFadeLength); - - if (!string.IsNullOrEmpty(gameOverText) && _gameOverController != null) - { - _gameOverController._deathText.text = TranslationHandler.GetTranslation(gameOverText, TranslationHandler.TextType.UI); - _gameOverController.SetupGameOverScreen(5f); - - // We set this to true to stop it from loading the credits scene, so we can do it ourselves - _gameOverController._loading = true; - - yield return new WaitUntil(ReadytoLoadCreditsScene); - } - - LoadCreditsScene(); - } - - private bool ReadytoLoadCreditsScene() => _gameOverController._fadedOutText && _gameOverController._textAnimator.IsComplete(); - public override void OnTriggerVolumeExit(GameObject hitObj) { } - - private void LoadCreditsScene() - { - NHLogger.LogVerbose($"Load credits {creditsType}"); - - switch (creditsType) - { - case NHCreditsType.Fast: - LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack); - break; - case NHCreditsType.Final: - LoadManager.LoadScene(OWScene.Credits_Final, LoadManager.FadeType.ToBlack); - break; - case NHCreditsType.Kazoo: - TimelineObliterationController.s_hasRealityEnded = true; - LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack); - break; - } - } } } diff --git a/NewHorizons/External/Configs/AddonConfig.cs b/NewHorizons/External/Configs/AddonConfig.cs index 66674575..8254b852 100644 --- a/NewHorizons/External/Configs/AddonConfig.cs +++ b/NewHorizons/External/Configs/AddonConfig.cs @@ -1,3 +1,4 @@ +using NewHorizons.External.Modules; using NewHorizons.OtherMods.AchievementsPlus; using Newtonsoft.Json; @@ -44,5 +45,11 @@ namespace NewHorizons.External.Configs /// The dimensions of the Echoes of the Eye subtitle is 669 x 67, so aim for that size /// public string subtitlePath = "subtitle.png"; + + /// + /// Custom game over messages for this mod. This can either display a title card before looping like in EOTE, or show a message and roll credits like the various time loop escape endings. + /// You must set a dialogue condition for the game over sequence to run. + /// + public GameOverModule[] gameOver; } } diff --git a/NewHorizons/External/Configs/PlanetConfig.cs b/NewHorizons/External/Configs/PlanetConfig.cs index 6184ed36..596ca54f 100644 --- a/NewHorizons/External/Configs/PlanetConfig.cs +++ b/NewHorizons/External/Configs/PlanetConfig.cs @@ -64,6 +64,11 @@ namespace NewHorizons.External.Configs /// public string[] removeChildren; + /// + /// optimization. turn this off if you know you're generating a new body and aren't worried about other addons editing it. + /// + [DefaultValue(true)] public bool checkForExisting = true; + #endregion #region Modules @@ -530,7 +535,7 @@ namespace NewHorizons.External.Configs Spawn.shipSpawnPoints = new SpawnModule.ShipSpawnPoint[] { Spawn.shipSpawn }; } - // Because these guys put TWO spawn points + // Because these guys put TWO spawn points if (starSystem == "2walker2.OogaBooga" && name == "The Campground") { Spawn.playerSpawnPoints[0].isDefault = true; @@ -658,6 +663,25 @@ namespace NewHorizons.External.Configs } } + if (Volumes?.creditsVolume != null) + { + foreach (var volume in Volumes.creditsVolume) + { + if (!string.IsNullOrEmpty(volume.gameOverText)) + { + if (volume.gameOver == null) + { + volume.gameOver = new(); + } + volume.gameOver.text = volume.gameOverText; + } + if (volume.creditsType != null) + { + volume.gameOver.creditsType = (SerializableEnums.NHCreditsType)volume.creditsType; + } + } + } + if (Base.invulnerableToSun) { Base.hasFluidDetector = false; @@ -742,4 +766,4 @@ namespace NewHorizons.External.Configs } #endregion } -} \ No newline at end of file +} diff --git a/NewHorizons/External/Modules/GameOverModule.cs b/NewHorizons/External/Modules/GameOverModule.cs new file mode 100644 index 00000000..00d029bd --- /dev/null +++ b/NewHorizons/External/Modules/GameOverModule.cs @@ -0,0 +1,32 @@ +using NewHorizons.External.SerializableData; +using NewHorizons.External.SerializableEnums; +using Newtonsoft.Json; +using System.ComponentModel; + +namespace NewHorizons.External.Modules +{ + [JsonObject] + public class GameOverModule + { + /// + /// Text displayed in orange on game over. For localization, put translations under UI. + /// + public string text; + + /// + /// Change the colour of the game over text. Leave empty to use the default orange. + /// + public MColor colour; + + /// + /// Condition that must be true for this game over to trigger. If this is on a LoadCreditsVolume, leave empty to always trigger this game over. + /// Note this is a regular dialogue condition, not a persistent condition. + /// + public string condition; + + /// + /// The type of credits that will run after the game over message is shown + /// + [DefaultValue("fast")] public NHCreditsType creditsType = NHCreditsType.Fast; + } +} diff --git a/NewHorizons/External/Modules/ProcGenModule.cs b/NewHorizons/External/Modules/ProcGenModule.cs index 6c54ced8..273005df 100644 --- a/NewHorizons/External/Modules/ProcGenModule.cs +++ b/NewHorizons/External/Modules/ProcGenModule.cs @@ -1,14 +1,78 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; using NewHorizons.External.SerializableData; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace NewHorizons.External.Modules { [JsonObject] public class ProcGenModule { + /// + /// Scale height of the proc gen. + /// + [Range(0, double.MaxValue)] public float scale; + + /// + /// Ground color, only applied if no texture or material is chosen. + /// public MColor color; - [Range(0, double.MaxValue)] public float scale; + /// + /// Can pick a preset material with a texture from the base game. Does not work with color or any textures. + /// + public Material material; + + /// + /// Can use a custom texture. Does not work with material or color. + /// + public string texture; + + /// + /// Relative filepath to the texture used for the terrain's smoothness and metallic, which are controlled by the texture's alpha and red channels respectively. Optional. + /// Typically black with variable transparency, when metallic isn't wanted. + /// + public string smoothnessMap; + + /// + /// How "glossy" the surface is, where 0 is diffuse, and 1 is like a mirror. + /// Multiplies with the alpha of the smoothness map if using one. + /// + [Range(0f, 1f)] + [DefaultValue(0f)] + public float smoothness = 0f; + + /// + /// How metallic the surface is, from 0 to 1. + /// Multiplies with the red of the smoothness map if using one. + /// + [Range(0f, 1f)] + [DefaultValue(0f)] + public float metallic = 0f; + + /// + /// Relative filepath to the texture used for the normal (aka bump) map. Optional. + /// + public string normalMap; + + /// + /// Strength of the normal map. Usually 0-1, but can go above, or negative to invert the map. + /// + [DefaultValue(1f)] + public float normalStrength = 1f; + + [JsonConverter(typeof(StringEnumConverter))] + public enum Material + { + [EnumMember(Value = @"default")] Default = 0, + + [EnumMember(Value = @"ice")] Ice = 1, + + [EnumMember(Value = @"quantum")] Quantum = 2, + + [EnumMember(Value = @"rock")] Rock = 3 + } } } \ No newline at end of file diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs index 82f5abe8..733d803d 100644 --- a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs @@ -1,6 +1,7 @@ using NewHorizons.External.Modules.Props.Audio; using NewHorizons.External.Modules.Props.Dialogue; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse { @@ -46,5 +47,23 @@ namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse /// The dialogue to use for this traveler. If omitted, the first CharacterDialogueTree in the object will be used. /// public DialogueInfo dialogue; + + /// + /// The name of the base game traveler to position this traveler after at the campfire, starting clockwise from Riebeck. Defaults to the end of the list (right before Riebeck). + /// + public TravelerName? afterTraveler; + + + [JsonConverter(typeof(StringEnumConverter))] + public enum TravelerName + { + Riebeck, + Chert, + Esker, + Felspar, + Gabbro, + Solanum, + Prisoner, + } } } diff --git a/NewHorizons/External/Modules/ShipLogModule.cs b/NewHorizons/External/Modules/ShipLogModule.cs index 3b9e5615..558ae650 100644 --- a/NewHorizons/External/Modules/ShipLogModule.cs +++ b/NewHorizons/External/Modules/ShipLogModule.cs @@ -61,7 +61,7 @@ namespace NewHorizons.External.Modules public float offset; /// - /// The path to the sprite (.png/.jpg/.exr) to show when the planet is unexplored in map mode. + /// The path to the sprite (.png/.jpg/.exr) to show when the planet is unexplored in map mode. If empty, a texture will be created and cached based on the revealed sprite. /// public string outlineSprite; diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/LoadCreditsVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/LoadCreditsVolumeInfo.cs index 60be21d5..fcf9b0da 100644 --- a/NewHorizons/External/Modules/Volumes/VolumeInfos/LoadCreditsVolumeInfo.cs +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/LoadCreditsVolumeInfo.cs @@ -1,5 +1,6 @@ using NewHorizons.External.SerializableEnums; using Newtonsoft.Json; +using System; using System.ComponentModel; namespace NewHorizons.External.Modules.Volumes.VolumeInfos @@ -7,16 +8,20 @@ namespace NewHorizons.External.Modules.Volumes.VolumeInfos [JsonObject] public class LoadCreditsVolumeInfo : VolumeInfo { - [DefaultValue("fast")] public NHCreditsType creditsType = NHCreditsType.Fast; + [Obsolete("Use gameOver.creditsType")] + public NHCreditsType? creditsType; - /// - /// Text displayed in orange on game over. For localization, put translations under UI. - /// + [Obsolete("Use gameOver.text")] public string gameOverText; /// - /// The type of death the player will have if they enter this volume. + /// The type of death the player will have if they enter this volume. Don't set to have the camera just fade out. /// - [DefaultValue("default")] public NHDeathType deathType = NHDeathType.Default; + [DefaultValue("default")] public NHDeathType? deathType = null; + + /// + /// The game over message to display. Leave empty to go straight to credits. + /// + public GameOverModule gameOver; } } diff --git a/NewHorizons/External/SerializableEnums/NHCreditsType.cs b/NewHorizons/External/SerializableEnums/NHCreditsType.cs index fe4d08be..83c1dc51 100644 --- a/NewHorizons/External/SerializableEnums/NHCreditsType.cs +++ b/NewHorizons/External/SerializableEnums/NHCreditsType.cs @@ -11,6 +11,8 @@ namespace NewHorizons.External.SerializableEnums [EnumMember(Value = @"final")] Final = 1, - [EnumMember(Value = @"kazoo")] Kazoo = 2 + [EnumMember(Value = @"kazoo")] Kazoo = 2, + + [EnumMember(Value = @"none")] None = 3 } } diff --git a/NewHorizons/Handlers/EyeSceneHandler.cs b/NewHorizons/Handlers/EyeSceneHandler.cs index 1433e9ee..233086e6 100644 --- a/NewHorizons/Handlers/EyeSceneHandler.cs +++ b/NewHorizons/Handlers/EyeSceneHandler.cs @@ -266,28 +266,48 @@ namespace NewHorizons.Handlers 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 - }; + var travelers = new List(); - if (quantumCampsiteController._hasMetSolanum) - { - travelers.Add(quantumCampsiteController._travelerControllers[4].transform); // Solanum - } - if (quantumCampsiteController._hasMetPrisoner) + var hasMetSolanum = quantumCampsiteController._hasMetSolanum; + var hasMetPrisoner = quantumCampsiteController._hasMetPrisoner; + + // The order of the travelers in the base game differs depending on if the player has met both Solanum and the Prisoner or not. + if (hasMetPrisoner && hasMetSolanum) { + travelers.Add(quantumCampsiteController._travelerControllers[0].transform); // Riebeck travelers.Add(quantumCampsiteController._travelerControllers[5].transform); // Prisoner + travelers.Add(quantumCampsiteController._travelerControllers[6].transform); // Esker + travelers.Add(quantumCampsiteController._travelerControllers[1].transform); // Felspar + travelers.Add(quantumCampsiteController._travelerControllers[3].transform); // Gabbro + travelers.Add(quantumCampsiteController._travelerControllers[4].transform); // Solanum + travelers.Add(quantumCampsiteController._travelerControllers[2].transform); // Chert + } + else + { + travelers.Add(quantumCampsiteController._travelerControllers[0].transform); // Riebeck + travelers.Add(quantumCampsiteController._travelerControllers[2].transform); // Chert + travelers.Add(quantumCampsiteController._travelerControllers[6].transform); // Esker + travelers.Add(quantumCampsiteController._travelerControllers[1].transform); // Felspar + travelers.Add(quantumCampsiteController._travelerControllers[3].transform); // Gabbro + if (hasMetSolanum) + travelers.Add(quantumCampsiteController._travelerControllers[4].transform); // Solanum + if (hasMetPrisoner) + travelers.Add(quantumCampsiteController._travelerControllers[5].transform); // Prisoner } - // Custom travelers (starting at index 7) + // Custom travelers (starting at index 7, after Esker). We loop through the array instead of the list of custom travelers in case a non-NH mod added their own. for (int i = 7; i < quantumCampsiteController._travelerControllers.Length; i++) { - travelers.Add(quantumCampsiteController._travelerControllers[i].transform); + var travelerInfo = GetActiveCustomEyeTravelers().FirstOrDefault(t => t.controller == quantumCampsiteController._travelerControllers[i]); + var travelerName = travelerInfo?.info?.afterTraveler; + if (travelerName.HasValue) + { + InsertTravelerAfter(quantumCampsiteController, travelers, travelerInfo.info.afterTraveler.ToString(), quantumCampsiteController._travelerControllers[i].transform); + } + else + { + travelers.Add(quantumCampsiteController._travelerControllers[i].transform); + } } var radius = 2f + 0.2f * travelers.Count; @@ -312,6 +332,22 @@ namespace NewHorizons.Handlers } } + private static void InsertTravelerAfter(QuantumCampsiteController campsite, List travelers, string travelerName, Transform newTraveler) + { + if (travelerName == "Prisoner") + travelerName = "Prisoner_Campfire"; + var existingTraveler = campsite._travelerControllers.FirstOrDefault(c => c.name == travelerName); + if (existingTraveler != null) + { + var index = travelers.IndexOf(existingTraveler.transform); + travelers.Insert(index + 1, newTraveler); + } + else + { + travelers.Add(newTraveler); + } + } + public class EyeTravelerData { public string id; diff --git a/NewHorizons/Handlers/FadeHandler.cs b/NewHorizons/Handlers/FadeHandler.cs index f9d6d4ff..ab2dddc6 100644 --- a/NewHorizons/Handlers/FadeHandler.cs +++ b/NewHorizons/Handlers/FadeHandler.cs @@ -11,24 +11,62 @@ namespace NewHorizons.Handlers /// public static class FadeHandler { - public static void FadeOut(float length) => Delay.StartCoroutine(FadeOutCoroutine(length)); + public static void FadeOut(float length) => Delay.StartCoroutine(FadeOutCoroutine(length, true)); - private static IEnumerator FadeOutCoroutine(float length) + public static void FadeOut(float length, bool fadeSound) => Delay.StartCoroutine(FadeOutCoroutine(length, fadeSound)); + + public static void FadeIn(float length) => Delay.StartCoroutine(FadeInCoroutine(length)); + + private static IEnumerator FadeOutCoroutine(float length, bool fadeSound) + { + // Make sure its not already faded + if (!LoadManager.s_instance._fadeCanvas.enabled) + { + LoadManager.s_instance._fadeCanvas.enabled = true; + float startTime = Time.unscaledTime; + float endTime = Time.unscaledTime + length; + + while (Time.unscaledTime < endTime) + { + var t = Mathf.Clamp01((Time.unscaledTime - startTime) / length); + LoadManager.s_instance._fadeImage.color = Color.Lerp(Color.clear, Color.black, t); + if (fadeSound) + { + AudioListener.volume = 1f - t; + } + yield return new WaitForEndOfFrame(); + } + + LoadManager.s_instance._fadeImage.color = Color.black; + if (fadeSound) + { + AudioListener.volume = 0; + } + yield return new WaitForEndOfFrame(); + } + else + { + yield return new WaitForSeconds(length); + } + } + + private static IEnumerator FadeInCoroutine(float length) { - LoadManager.s_instance._fadeCanvas.enabled = true; float startTime = Time.unscaledTime; float endTime = Time.unscaledTime + length; while (Time.unscaledTime < endTime) { var t = Mathf.Clamp01((Time.unscaledTime - startTime) / length); - LoadManager.s_instance._fadeImage.color = Color.Lerp(Color.clear, Color.black, t); - AudioListener.volume = 1f - t; + LoadManager.s_instance._fadeImage.color = Color.Lerp(Color.black, Color.clear, t); + AudioListener.volume = t; yield return new WaitForEndOfFrame(); } - LoadManager.s_instance._fadeImage.color = Color.black; - AudioListener.volume = 0; + AudioListener.volume = 1; + LoadManager.s_instance._fadeCanvas.enabled = false; + LoadManager.s_instance._fadeImage.color = Color.clear; + yield return new WaitForEndOfFrame(); } @@ -36,7 +74,7 @@ namespace NewHorizons.Handlers private static IEnumerator FadeThenCoroutine(float length, Action action) { - yield return FadeOutCoroutine(length); + yield return FadeOutCoroutine(length, true); action?.Invoke(); } diff --git a/NewHorizons/Handlers/InvulnerabilityHandler.cs b/NewHorizons/Handlers/InvulnerabilityHandler.cs index cacc9e19..85f45601 100644 --- a/NewHorizons/Handlers/InvulnerabilityHandler.cs +++ b/NewHorizons/Handlers/InvulnerabilityHandler.cs @@ -4,7 +4,7 @@ using UnityEngine.SceneManagement; namespace NewHorizons.Handlers { - internal class InvulnerabilityHandler + public static class InvulnerabilityHandler { /// /// Used in patches @@ -32,8 +32,25 @@ namespace NewHorizons.Handlers } } - private static DeathManager GetDeathManager() => GameObject.FindObjectOfType(); - private static PlayerResources GetPlayerResouces() => GameObject.FindObjectOfType(); + private static DeathManager _deathManager; + private static DeathManager GetDeathManager() + { + if (_deathManager == null) + { + _deathManager = GameObject.FindObjectOfType(); + } + return _deathManager; + } + + private static PlayerResources _playerResources; + private static PlayerResources GetPlayerResouces() + { + if (_playerResources == null) + { + _playerResources = GameObject.FindObjectOfType(); + } + return _playerResources; + } static InvulnerabilityHandler() { diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 6898382f..858131e9 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -20,6 +20,10 @@ using System; using System.Collections.Generic; using System.Linq; using UnityEngine; +using NewHorizons.Streaming; +using Newtonsoft.Json; +using NewHorizons.External.Modules.VariableSize; +using NewHorizons.Components; namespace NewHorizons.Handlers { @@ -168,26 +172,29 @@ namespace NewHorizons.Handlers // I don't remember doing this why is it exceptions what am I doing GameObject existingPlanet = null; - try + if (body.Config.checkForExisting) // TODO: remove this when we cache name->fullpath in Find { - existingPlanet = AstroObjectLocator.GetAstroObject(body.Config.name).gameObject; - } - catch (Exception) - { - if (body?.Config?.name == null) + try { - NHLogger.LogError($"How is there no name for {body}"); + existingPlanet = AstroObjectLocator.GetAstroObject(body.Config.name).gameObject; } - else + catch (Exception) { - existingPlanet = SearchUtilities.Find(body.Config.name.Replace(" ", "") + "_Body", false); + if (body?.Config?.name == null) + { + NHLogger.LogError($"How is there no name for {body}"); + } + else + { + existingPlanet = SearchUtilities.Find(body.Config.name.Replace(" ", "") + "_Body", false); + } } - } - if (existingPlanet == null && body.Config.destroy) - { - NHLogger.LogError($"{body.Config.name} was meant to be destroyed, but was not found"); - return false; + if (existingPlanet == null && body.Config.destroy) + { + NHLogger.LogError($"{body.Config.name} was meant to be destroyed, but was not found"); + return false; + } } if (existingPlanet != null) @@ -287,9 +294,9 @@ namespace NewHorizons.Handlers try { NHLogger.Log($"Creating [{body.Config.name}]"); - var planetObject = GenerateBody(body, defaultPrimaryToSun) + var planetObject = GenerateBody(body, defaultPrimaryToSun) ?? throw new NullReferenceException("Something went wrong when generating the body but no errors were logged."); - + planetObject.SetActive(true); var ao = planetObject.GetComponent(); @@ -316,7 +323,7 @@ namespace NewHorizons.Handlers { NHLogger.LogError($"Error in event handler for OnPlanetLoaded on body {body.Config.name}: {e}"); } - + body.UnloadCache(true); _loadedBodies.Add(body); return true; @@ -390,7 +397,7 @@ namespace NewHorizons.Handlers body.Config.MapMarker.enabled = false; const float sphereOfInfluence = 2000f; - + var owRigidBody = RigidBodyBuilder.Make(go, sphereOfInfluence, body.Config); var ao = AstroObjectBuilder.Make(go, null, body, false); @@ -402,7 +409,7 @@ namespace NewHorizons.Handlers BrambleDimensionBuilder.Make(body, go, ao, sector, body.Mod, owRigidBody); go = SharedGenerateBody(body, go, sector, owRigidBody); - + // Not included in SharedGenerate to not mess up gravity on base game planets if (body.Config.Base.surfaceGravity != 0) { @@ -467,7 +474,7 @@ namespace NewHorizons.Handlers } var sphereOfInfluence = GetSphereOfInfluence(body); - + var owRigidBody = RigidBodyBuilder.Make(go, sphereOfInfluence, body.Config); var ao = AstroObjectBuilder.Make(go, primaryBody, body, false); @@ -581,7 +588,7 @@ namespace NewHorizons.Handlers GameObject procGen = null; if (body.Config.ProcGen != null) { - procGen = ProcGenBuilder.Make(go, sector, body.Config.ProcGen); + procGen = ProcGenBuilder.Make(body.Mod, go, sector, body.Config.ProcGen); } if (body.Config.Star != null) @@ -686,7 +693,7 @@ namespace NewHorizons.Handlers SunOverrideBuilder.Make(go, sector, body.Config.Atmosphere, body.Config.Water, surfaceSize); } } - + if (body.Config.Atmosphere.fogSize != 0) { fog = FogBuilder.Make(go, sector, body.Config.Atmosphere, body.Mod); @@ -764,6 +771,8 @@ namespace NewHorizons.Handlers SpawnPointBuilder.Make(go, body.Config.Spawn, rb); } + SplashColourizer.Make(go, body.Config, sphereOfInfluence); + // We allow removing children afterwards so you can also take bits off of the modules you used if (body.Config.removeChildren != null) RemoveChildren(go, body); @@ -997,15 +1006,10 @@ namespace NewHorizons.Handlers private static void RemoveChildren(GameObject go, NewHorizonsBody body) { - var goPath = go.transform.GetPath(); - var transforms = go.GetComponentsInChildren(true); foreach (var childPath in body.Config.removeChildren) { - // Multiple children can have the same path so we delete all that match - var path = $"{goPath}/{childPath}"; - var flag = true; - foreach (var childObj in transforms.Where(x => x.GetPath() == path)) + foreach (var childObj in go.transform.FindAll(childPath)) { flag = false; // idk why we wait here but we do diff --git a/NewHorizons/Handlers/StreamingHandler.cs b/NewHorizons/Handlers/StreamingHandler.cs index c037594d..321a07d6 100644 --- a/NewHorizons/Handlers/StreamingHandler.cs +++ b/NewHorizons/Handlers/StreamingHandler.cs @@ -2,6 +2,7 @@ using NewHorizons.Utility; using System.Collections.Generic; using System.Linq; using UnityEngine; +using UnityEngine.Profiling; namespace NewHorizons.Handlers { @@ -51,6 +52,9 @@ namespace NewHorizons.Handlers /// public static void SetUpStreaming(GameObject obj, Sector sector) { + // TODO: used OFTEN by detail builder. 20-40ms adds up to seconds. speed up! + + Profiler.BeginSample("get bundles"); // find the asset bundles to load // tries the cache first, then builds if (!_objectCache.TryGetValue(obj, out var assetBundles)) @@ -94,7 +98,9 @@ namespace NewHorizons.Handlers assetBundles = assetBundlesList.ToArray(); _objectCache[obj] = assetBundles; } + Profiler.EndSample(); + Profiler.BeginSample("get sectors"); foreach (var assetBundle in assetBundles) { // Track the sector even if its null. null means stay loaded forever @@ -105,7 +111,9 @@ namespace NewHorizons.Handlers } sectors.SafeAdd(sector); } + Profiler.EndSample(); + Profiler.BeginSample("load assets"); if (sector) { sector.OnOccupantEnterSector += _ => @@ -128,6 +136,7 @@ namespace NewHorizons.Handlers foreach (var assetBundle in assetBundles) StreamingManager.LoadStreamingAssets(assetBundle); } + Profiler.EndSample(); } public static bool IsBundleInUse(string assetBundle) @@ -152,4 +161,4 @@ namespace NewHorizons.Handlers } } } -} \ No newline at end of file +} diff --git a/NewHorizons/Handlers/SubtitlesHandler.cs b/NewHorizons/Handlers/SubtitlesHandler.cs index 1a42bc79..326b2e0c 100644 --- a/NewHorizons/Handlers/SubtitlesHandler.cs +++ b/NewHorizons/Handlers/SubtitlesHandler.cs @@ -109,7 +109,7 @@ namespace NewHorizons.Handlers var tex = ImageUtilities.GetTexture(mod, filepath, false); if (tex == null) return; - var sprite = Sprite.Create(tex, new Rect(0.0f, 0.0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100.0f); + var sprite = Sprite.Create(tex, new Rect(0.0f, 0.0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100, 0, SpriteMeshType.FullRect, Vector4.zero, false); AddSubtitle(sprite); } diff --git a/NewHorizons/Handlers/VesselCoordinatePromptHandler.cs b/NewHorizons/Handlers/VesselCoordinatePromptHandler.cs index d53eb875..a55be9c9 100644 --- a/NewHorizons/Handlers/VesselCoordinatePromptHandler.cs +++ b/NewHorizons/Handlers/VesselCoordinatePromptHandler.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using UnityEngine; +using UnityEngine.UI; using static NewHorizons.External.Configs.StarSystemConfig; namespace NewHorizons.Handlers @@ -47,7 +48,7 @@ namespace NewHorizons.Handlers if (_textureCache == null) _textureCache = new List(); _textureCache.Add(texture); - var sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(texture.width / 2f, texture.height / 2f)); + var sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(texture.width / 2f, texture.height / 2f), 100, 0, SpriteMeshType.FullRect, Vector4.zero, false); var name = ShipLogStarChartMode.UniqueIDToName(systemID); diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index 3d0e95e9..3fc6b689 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -6,6 +6,7 @@ using NewHorizons.Builder.Props; using NewHorizons.Builder.Props.Audio; using NewHorizons.Builder.Props.EchoesOfTheEye; using NewHorizons.Builder.Props.TranslatorText; +using NewHorizons.Components; using NewHorizons.Components.EOTE; using NewHorizons.Components.Fixers; using NewHorizons.Components.Ship; @@ -311,6 +312,7 @@ namespace NewHorizons ImageUtilities.ClearCache(); AudioUtilities.ClearCache(); AssetBundleUtilities.ClearCache(); + ProcGenBuilder.ClearCache(); } IsSystemReady = false; @@ -478,6 +480,8 @@ namespace NewHorizons // Fix spawn point PlayerSpawnHandler.SetUpPlayerSpawn(); + new GameObject(nameof(NHGameOverManager)).AddComponent(); + if (isSolarSystem) { // Warp drive @@ -834,6 +838,10 @@ namespace NewHorizons AssetBundleUtilities.PreloadBundle(bundle, mod); } } + if (addonConfig.gameOver != null) + { + NHGameOverManager.gameOvers[mod.ModHelper.Manifest.UniqueName] = addonConfig.gameOver; + } AddonConfigs[mod] = addonConfig; } diff --git a/NewHorizons/NewHorizons.csproj b/NewHorizons/NewHorizons.csproj index 5ae0cd1c..a38229e0 100644 --- a/NewHorizons/NewHorizons.csproj +++ b/NewHorizons/NewHorizons.csproj @@ -10,6 +10,8 @@ true None 1701;1702;1591 + + none @@ -39,4 +41,4 @@ - \ No newline at end of file + diff --git a/NewHorizons/Patches/DeathManagerPatches.cs b/NewHorizons/Patches/DeathManagerPatches.cs new file mode 100644 index 00000000..40f7b14b --- /dev/null +++ b/NewHorizons/Patches/DeathManagerPatches.cs @@ -0,0 +1,15 @@ +using HarmonyLib; +using NewHorizons.Components; + +namespace NewHorizons.Patches; + +[HarmonyPatch] +public static class DeathManagerPatches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(DeathManager), nameof(DeathManager.FinishDeathSequence))] + public static void DeathManager_FinishDeathSequence() + { + NHGameOverManager.Instance.TryHijackDeathSequence(); + } +} diff --git a/NewHorizons/Patches/ProfilerPatch.cs b/NewHorizons/Patches/ProfilerPatch.cs new file mode 100644 index 00000000..9cdde508 --- /dev/null +++ b/NewHorizons/Patches/ProfilerPatch.cs @@ -0,0 +1,73 @@ +#if ENABLE_PROFILER + +using HarmonyLib; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using UnityEngine.Profiling; + +namespace NewHorizons.Patches; + +/// +/// attach profiler markers to important methods +/// +[HarmonyPatch] +public static class ProfilerPatch +{ + private static string FriendlyName(this MethodBase @this) => $"{@this.DeclaringType.Name}.{@this.Name}"; + + [HarmonyTargetMethods] + public static IEnumerable TargetMethods() + { + foreach (var type in Assembly.GetExecutingAssembly().GetTypes()) + { + if (!( + type.Name == "Main" || + type.Name.EndsWith("Builder") || + type.Name.EndsWith("Handler") || + type.Name.EndsWith("Utilities") + )) continue; + + foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) + { + if (method.ContainsGenericParameters) continue; + + // Main.Instance.ModHelper.Console.WriteLine($"[profiler] profiling {method.FriendlyName()}"); + yield return method; + } + } + } + + [HarmonyPrefix] + public static void Prefix(MethodBase __originalMethod /*, out Stopwatch __state*/) + { + Profiler.BeginSample(__originalMethod.FriendlyName()); + + // __state = new Stopwatch(); + // __state.Start(); + } + + [HarmonyPostfix] + public static void Postfix( /*MethodBase __originalMethod, Stopwatch __state*/) + { + Profiler.EndSample(); + + // __state.Stop(); + // Main.Instance.ModHelper.Console.WriteLine($"[profiler] {__originalMethod.MethodName()} took {__state.Elapsed.TotalMilliseconds:f1} ms"); + } +} + +/// +/// bundle loading causes log spam that slows loading, but only in unity dev profiler mode. +/// patch it out so it doesnt do false-positive slowness. +/// +[HarmonyPatch] +public static class DisableShaderLogSpamPatch +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(StackTraceUtility), "ExtractStackTrace")] + [HarmonyPatch(typeof(Application), "CallLogCallback")] + private static bool DisableShaderLogSpam() => false; +} + +#endif diff --git a/NewHorizons/Schemas/addon_manifest_schema.json b/NewHorizons/Schemas/addon_manifest_schema.json index a1e1d34a..d68f545c 100644 --- a/NewHorizons/Schemas/addon_manifest_schema.json +++ b/NewHorizons/Schemas/addon_manifest_schema.json @@ -38,6 +38,13 @@ "type": "string", "description": "The path to the addons subtitle for the main menu.\nDefaults to \"subtitle.png\".\nThe dimensions of the Echoes of the Eye subtitle is 669 x 67, so aim for that size" }, + "gameOver": { + "type": "array", + "description": "Custom game over messages for this mod. This can either display a title card before looping like in EOTE, or show a message and roll credits like the various time loop escape endings.\nYou must set a dialogue condition for the game over sequence to run.", + "items": { + "$ref": "#/definitions/GameOverModule" + } + }, "$schema": { "type": "string", "description": "The schema to validate with" @@ -79,6 +86,80 @@ } } } + }, + "GameOverModule": { + "type": "object", + "additionalProperties": false, + "properties": { + "text": { + "type": "string", + "description": "Text displayed in orange on game over. For localization, put translations under UI." + }, + "colour": { + "description": "Change the colour of the game over text. Leave empty to use the default orange.", + "$ref": "#/definitions/MColor" + }, + "condition": { + "type": "string", + "description": "Condition that must be true for this game over to trigger. If this is on a LoadCreditsVolume, leave empty to always trigger this game over.\nNote this is a regular dialogue condition, not a persistent condition." + }, + "creditsType": { + "description": "The type of credits that will run after the game over message is shown", + "default": "fast", + "$ref": "#/definitions/NHCreditsType" + } + } + }, + "MColor": { + "type": "object", + "additionalProperties": false, + "properties": { + "r": { + "type": "integer", + "description": "The red component of this colour from 0-255, higher values will make the colour glow if applicable.", + "format": "int32", + "maximum": 2147483647.0, + "minimum": 0.0 + }, + "g": { + "type": "integer", + "description": "The green component of this colour from 0-255, higher values will make the colour glow if applicable.", + "format": "int32", + "maximum": 2147483647.0, + "minimum": 0.0 + }, + "b": { + "type": "integer", + "description": "The blue component of this colour from 0-255, higher values will make the colour glow if applicable.", + "format": "int32", + "maximum": 2147483647.0, + "minimum": 0.0 + }, + "a": { + "type": "integer", + "description": "The alpha (opacity) component of this colour", + "format": "int32", + "default": 255, + "maximum": 255.0, + "minimum": 0.0 + } + } + }, + "NHCreditsType": { + "type": "string", + "description": "", + "x-enumNames": [ + "Fast", + "Final", + "Kazoo", + "None" + ], + "enum": [ + "fast", + "final", + "kazoo", + "none" + ] } }, "$docs": { diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index 1f779391..32632247 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -43,6 +43,11 @@ "type": "string" } }, + "checkForExisting": { + "type": "boolean", + "description": "optimization. turn this off if you know you're generating a new body and aren't worried about other addons editing it.", + "default": true + }, "AmbientLights": { "type": "array", "description": "Add ambient lights to this body", @@ -353,16 +358,72 @@ "type": "object", "additionalProperties": false, "properties": { - "color": { - "$ref": "#/definitions/MColor" - }, "scale": { "type": "number", + "description": "Scale height of the proc gen.", "format": "float", "minimum": 0.0 + }, + "color": { + "description": "Ground color, only applied if no texture or material is chosen.", + "$ref": "#/definitions/MColor" + }, + "material": { + "description": "Can pick a preset material with a texture from the base game. Does not work with color or any textures.", + "$ref": "#/definitions/Material" + }, + "texture": { + "type": "string", + "description": "Can use a custom texture. Does not work with material or color." + }, + "smoothnessMap": { + "type": "string", + "description": "Relative filepath to the texture used for the terrain's smoothness and metallic, which are controlled by the texture's alpha and red channels respectively. Optional.\nTypically black with variable transparency, when metallic isn't wanted." + }, + "smoothness": { + "type": "number", + "description": "How \"glossy\" the surface is, where 0 is diffuse, and 1 is like a mirror.\nMultiplies with the alpha of the smoothness map if using one.", + "format": "float", + "default": 0.0, + "maximum": 1.0, + "minimum": 0.0 + }, + "metallic": { + "type": "number", + "description": "How metallic the surface is, from 0 to 1.\nMultiplies with the red of the smoothness map if using one.", + "format": "float", + "default": 0.0, + "maximum": 1.0, + "minimum": 0.0 + }, + "normalMap": { + "type": "string", + "description": "Relative filepath to the texture used for the normal (aka bump) map. Optional." + }, + "normalStrength": { + "type": "number", + "description": "Strength of the normal map. Usually 0-1, but can go above, or negative to invert the map.", + "format": "float", + "default": 1.0 } } }, + "Material": { + "type": "string", + "description": "", + "x-enumNames": [ + "Default", + "Ice", + "Quantum", + "Rock" + ], + "enum": [ + "default", + "ice", + "quantum", + "rock" + ] + }, "AtmosphereModule": { "type": "object", "additionalProperties": false, @@ -1022,6 +1083,17 @@ "dialogue": { "description": "The dialogue to use for this traveler. If omitted, the first CharacterDialogueTree in the object will be used.", "$ref": "#/definitions/DialogueInfo" + }, + "afterTraveler": { + "description": "The name of the base game traveler to position this traveler after at the campfire, starting clockwise from Riebeck. Defaults to the end of the list (right before Riebeck).", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/TravelerName" + } + ] } } }, @@ -1466,6 +1538,28 @@ "none" ] }, + "TravelerName": { + "type": "string", + "description": "", + "x-enumNames": [ + "Riebeck", + "Chert", + "Esker", + "Felspar", + "Gabbro", + "Solanum", + "Prisoner" + ], + "enum": [ + "Riebeck", + "Chert", + "Esker", + "Felspar", + "Gabbro", + "Solanum", + "Prisoner" + ] + }, "InstrumentZoneInfo": { "type": "object", "additionalProperties": false, @@ -4690,7 +4784,7 @@ }, "outlineSprite": { "type": "string", - "description": "The path to the sprite (.png/.jpg/.exr) to show when the planet is unexplored in map mode." + "description": "The path to the sprite (.png/.jpg/.exr) to show when the planet is unexplored in map mode. If empty, a texture will be created and cached based on the revealed sprite." }, "remove": { "type": "boolean", @@ -6391,18 +6485,44 @@ "type": "string", "description": "An optional rename of this object" }, - "creditsType": { - "default": "fast", - "$ref": "#/definitions/NHCreditsType" + "deathType": { + "description": "The type of death the player will have if they enter this volume. Don't set to have the camera just fade out.", + "default": "default", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/NHDeathType" + } + ] }, - "gameOverText": { + "gameOver": { + "description": "The game over message to display. Leave empty to go straight to credits.", + "$ref": "#/definitions/GameOverModule" + } + } + }, + "GameOverModule": { + "type": "object", + "additionalProperties": false, + "properties": { + "text": { "type": "string", "description": "Text displayed in orange on game over. For localization, put translations under UI." }, - "deathType": { - "description": "The type of death the player will have if they enter this volume.", - "default": "default", - "$ref": "#/definitions/NHDeathType" + "colour": { + "description": "Change the colour of the game over text. Leave empty to use the default orange.", + "$ref": "#/definitions/MColor" + }, + "condition": { + "type": "string", + "description": "Condition that must be true for this game over to trigger. If this is on a LoadCreditsVolume, leave empty to always trigger this game over.\nNote this is a regular dialogue condition, not a persistent condition." + }, + "creditsType": { + "description": "The type of credits that will run after the game over message is shown", + "default": "fast", + "$ref": "#/definitions/NHCreditsType" } } }, @@ -6412,12 +6532,14 @@ "x-enumNames": [ "Fast", "Final", - "Kazoo" + "Kazoo", + "None" ], "enum": [ "fast", "final", - "kazoo" + "kazoo", + "none" ] }, "CometTailModule": { diff --git a/NewHorizons/Utility/Files/AssetBundleUtilities.cs b/NewHorizons/Utility/Files/AssetBundleUtilities.cs index 87170770..0cdd2a9b 100644 --- a/NewHorizons/Utility/Files/AssetBundleUtilities.cs +++ b/NewHorizons/Utility/Files/AssetBundleUtilities.cs @@ -12,6 +12,7 @@ namespace NewHorizons.Utility.Files public static class AssetBundleUtilities { public static Dictionary AssetBundles = new(); + private static Dictionary _prefabCache = new(); private static readonly List _loadingBundles = new(); @@ -52,6 +53,7 @@ namespace NewHorizons.Utility.Files } AssetBundles = AssetBundles.Where(x => x.Value.keepLoaded).ToDictionary(x => x.Key, x => x.Value); + _prefabCache.Clear(); } public static void PreloadBundle(string assetBundleRelativeDir, IModBehaviour mod) @@ -113,11 +115,17 @@ namespace NewHorizons.Utility.Files public static GameObject LoadPrefab(string assetBundleRelativeDir, string pathInBundle, IModBehaviour mod) { - var prefab = Load(assetBundleRelativeDir, pathInBundle, mod); + if (_prefabCache.TryGetValue(assetBundleRelativeDir + pathInBundle, out var prefab)) + return prefab; + + prefab = Load(assetBundleRelativeDir, pathInBundle, mod); prefab.SetActive(false); ReplaceShaders(prefab); + + // replacing shaders is expensive, so cache it + _prefabCache.Add(assetBundleRelativeDir + pathInBundle, prefab); return prefab; } diff --git a/NewHorizons/Utility/Files/ImageUtilities.cs b/NewHorizons/Utility/Files/ImageUtilities.cs index 09ff831f..65f73aba 100644 --- a/NewHorizons/Utility/Files/ImageUtilities.cs +++ b/NewHorizons/Utility/Files/ImageUtilities.cs @@ -1,9 +1,12 @@ using NewHorizons.Builder.Props; +using NewHorizons.External; +using NewHorizons.External.Modules.VariableSize; using NewHorizons.Utility.OWML; using OWML.Common; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using UnityEngine; namespace NewHorizons.Utility.Files @@ -43,7 +46,17 @@ namespace NewHorizons.Utility.Files return null; } // Copied from OWML but without the print statement lol - var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, filename); + string path; + try + { + path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, filename); + } + catch (Exception e) + { + NHLogger.LogError($"Invalid path: Couldn't combine {mod.ModHelper.Manifest.ModFolderPath} {filename} - {e}"); + return null; + } + var key = GetKey(path); if (_textureCache.TryGetValue(key, out var existingTexture)) { @@ -91,8 +104,8 @@ namespace NewHorizons.Utility.Files DeleteTexture(key, texture); } - public static void DeleteTexture(string key, Texture2D texture) - { + public static void DeleteTexture(string key, Texture2D texture) + { if (_textureCache.ContainsKey(key)) { if (_textureCache[key] == texture) @@ -247,7 +260,7 @@ namespace NewHorizons.Utility.Files return texture; } - public static Texture2D MakeOutline(Texture2D texture, Color color, int thickness) + private static Texture2D MakeOutline(Texture2D texture, Color color, int thickness) { var key = $"{texture.name} > outline {color} {thickness}"; if (_textureCache.TryGetValue(key, out var existingTexture)) return (Texture2D)existingTexture; @@ -368,7 +381,7 @@ namespace NewHorizons.Utility.Files var pixels = image.GetPixels(); for (int i = 0; i < pixels.Length; i++) { - var amount = (i % image.width) / (float) image.width; + var amount = (i % image.width) / (float)image.width; var lightTint = LerpColor(lightTintStart, lightTintEnd, amount); var darkTint = LerpColor(darkTintStart, darkTintEnd, amount); @@ -489,5 +502,119 @@ namespace NewHorizons.Utility.Files sprite.name = texture.name; return sprite; } + + public static Texture2D GetCachedOutlineOrCreate(NewHorizonsBody body, Texture2D original, string originalPath) + { + if (string.IsNullOrEmpty(originalPath)) + { + Texture2D defaultTexture = null; + if (body.Config.Star != null) + { + defaultTexture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModeStarOutline.png"); + } + else if (body.Config.Atmosphere != null) + { + defaultTexture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModNoAtmoOutline.png"); + } + else + { + defaultTexture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModePlanetOutline.png"); + } + + return defaultTexture; + } + else + { + var cachedPath = Path.Combine(body.Mod.ModHelper.Manifest.ModFolderPath, $"TextureCache_{Main.Instance.CurrentStarSystem}", originalPath); + var outlineTexture = ImageUtilities.GetTexture(body.Mod, cachedPath); + + if (outlineTexture == null) + { + NHLogger.LogVerbose($"Caching outline to {cachedPath}"); + + var newTexture = ImageUtilities.MakeOutline(original, Color.white, 10); + + Directory.CreateDirectory(Path.GetDirectoryName(cachedPath)); + File.WriteAllBytes(cachedPath, newTexture.EncodeToPNG()); + + return newTexture; + } + else + { + return outlineTexture; + } + } + } + + public static Texture2D AutoGenerateMapModePicture(NewHorizonsBody body) + { + Texture2D texture; + + if (body.Config.Star != null) + { + texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModeStar.png"); + } + else if (body.Config.Atmosphere != null) + { + texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModNoAtmo.png"); + } + else + { + texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModePlanet.png"); + } + + var color = GetDominantPlanetColor(body); + var darkColor = new Color(color.r / 3f, color.g / 3f, color.b / 3f); + + texture = ImageUtilities.LerpGreyscaleImage(texture, color, darkColor); + + return texture; + } + + private static Color GetDominantPlanetColor(NewHorizonsBody body) + { + try + { + var starColor = body.Config?.Star?.tint; + if (starColor != null) return starColor.ToColor(); + + var atmoColor = body.Config.Atmosphere?.atmosphereTint; + if (body.Config.Atmosphere?.clouds != null && atmoColor != null) return atmoColor.ToColor(); + + if (body.Config?.HeightMap?.textureMap != null) + { + try + { + var texture = ImageUtilities.GetTexture(body.Mod, body.Config.HeightMap.textureMap); + var landColor = ImageUtilities.GetAverageColor(texture); + if (landColor != null) return landColor; + } + catch (Exception) { } + } + + var waterColor = body.Config.Water?.tint; + if (waterColor != null) return waterColor.ToColor(); + + var lavaColor = body.Config.Lava?.tint; + if (lavaColor != null) return lavaColor.ToColor(); + + var sandColor = body.Config.Sand?.tint; + if (sandColor != null) return sandColor.ToColor(); + + switch (body.Config?.Props?.singularities?.FirstOrDefault()?.type) + { + case SingularityModule.SingularityType.BlackHole: + return Color.black; + case SingularityModule.SingularityType.WhiteHole: + return Color.white; + } + } + catch (Exception) + { + NHLogger.LogWarning($"Something went wrong trying to pick the colour for {body.Config.name} but I'm too lazy to fix it."); + } + + return Color.white; + } } } diff --git a/NewHorizons/Utility/SearchUtilities.cs b/NewHorizons/Utility/SearchUtilities.cs index 36e656e6..b6086917 100644 --- a/NewHorizons/Utility/SearchUtilities.cs +++ b/NewHorizons/Utility/SearchUtilities.cs @@ -2,6 +2,7 @@ using NewHorizons.Utility.OWML; using System.Collections.Generic; using System.Linq; using UnityEngine; +using UnityEngine.Profiling; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; @@ -116,19 +117,23 @@ namespace NewHorizons.Utility if (CachedGameObjects.TryGetValue(path, out var go)) return go; // 1: normal find + Profiler.BeginSample("1"); go = GameObject.Find(path); if (go) { CachedGameObjects.Add(path, go); + Profiler.EndSample(); return go; } + Profiler.EndSample(); + Profiler.BeginSample("2"); // 2: find inactive using root + transform.find var names = path.Split('/'); // Cache the root objects so we don't loop through all of them each time var rootName = names[0]; - if (!CachedRootGameObjects.TryGetValue(rootName, out var root)) + if (!CachedRootGameObjects.TryGetValue(rootName, out var root)) { root = SceneManager.GetActiveScene().GetRootGameObjects().FirstOrDefault(x => x.name == rootName); if (root != null) @@ -142,9 +147,12 @@ namespace NewHorizons.Utility if (go) { CachedGameObjects.Add(path, go); + Profiler.EndSample(); return go; } + Profiler.EndSample(); + Profiler.BeginSample("3"); var name = names.Last(); if (warn) NHLogger.LogWarning($"Couldn't find object in path {path}, will look for potential matches for name {name}"); // 3: find resource to include inactive objects (but skip prefabs) @@ -153,10 +161,12 @@ namespace NewHorizons.Utility if (go) { CachedGameObjects.Add(path, go); + Profiler.EndSample(); return go; } if (warn) NHLogger.LogWarning($"Couldn't find object with name {name}"); + Profiler.EndSample(); return null; } @@ -169,5 +179,31 @@ namespace NewHorizons.Utility } return children; } + + /// + /// transform.find but works for gameobjects with same name + /// + public static List FindAll(this Transform @this, string path) + { + var names = path.Split('/'); + var currentTransforms = new List { @this }; + foreach (var name in names) + { + var newTransforms = new List(); + foreach (var currentTransform in currentTransforms) + { + foreach (Transform child in currentTransform) + { + if (child.name == name) + { + newTransforms.Add(child); + } + } + } + currentTransforms = newTransforms; + } + + return currentTransforms; + } } } diff --git a/NewHorizons/manifest.json b/NewHorizons/manifest.json index 2f76acba..8d7ca082 100644 --- a/NewHorizons/manifest.json +++ b/NewHorizons/manifest.json @@ -4,7 +4,7 @@ "author": "xen, Bwc9876, JohnCorby, MegaPiggy, Trifid, and friends", "name": "New Horizons", "uniqueName": "xen.NewHorizons", - "version": "1.26.1", + "version": "1.27.0", "owmlVersion": "2.12.1", "dependencies": [ "JohnCorby.VanillaFix", "xen.CommonCameraUtility", "dgarro.CustomShipLogModes" ], "conflicts": [ "PacificEngine.OW_CommonResources" ], diff --git a/docs/src/content/docs/guides/dialogue.md b/docs/src/content/docs/guides/dialogue.md index a5057772..0375e773 100644 --- a/docs/src/content/docs/guides/dialogue.md +++ b/docs/src/content/docs/guides/dialogue.md @@ -15,38 +15,14 @@ A dialogue tree is an entire conversation, it's made up of dialogue nodes. A node is a set of pages shown to the player followed by options the player can choose from to change the flow of the conversation. -### Condition +### Conditions -A condition is a yes/no value stored **for this loop and this loop only**. It can be used to show new dialogue options, stop someone from talking to you (looking at you Slate), and more. - -### Persistent Condition - -A persistent condition is similar to a condition, except it _persists_ through loops, and is saved on the players save file. +In dialogue, the available conversation topics can be limited by what the player knows, defined using dialogue conditions, persistent conditions, and ship log facts. Dialogue can also set conditions to true or false, and reveal ship log facts to the player. This is covered in detail later on this page. ### Remote Trigger A remote trigger is used to have an NPC talk to you from a distance; ex: Slate stopping you for the umpteenth time to tell you information you already knew. -### ReuseDialogueOptionsListFrom - -This is a custom XML node introduced by New Horizons. Use it when adding new dialogue to existing characters, to repeat the dialogue options list from another node. - -For example, Slate's first dialogue with options is named `Scientist5`. To make a custom DialogueNode using these dialogue options (meaning new dialogue said by Slate, but reusing the possible player responses) you can write: - -```xml - - ... - - NEW DIALOGUE FOR SLATE HERE. - - - Scientist5 - - -``` - -Note: If you're loading dialogue in code, 2 frames must pass before entering the conversation in order for ReuseDialogueOptionsListFrom to take effect. - ## Example XML Here's an example dialogue XML: @@ -176,11 +152,39 @@ In addition to ``, there are other ways to control the flow of Defining `` in the `` tag instead of a `` will make the conversation go directly to that target after the character is done talking. -### DialogueTargetShipLogCondition +### EntryCondition -Used in tandem with `DialogueTarget`, makes it so you must have a [ship log fact](/guides/ship-log#explore-facts) to go to the next node. +The first dialogue node that opens when a player starts talking to a character is chosen using this property. To mark a DialogueNode as beginning the dialogue by default, use the condition DEFAULT (a DialogueTree should always have a node with the DEFAULT entry condition to ensure there is a way to start dialogue). -### Adding to existing dialogue +The entry condition can be either a condition or a persistent condition. + +### Condition + +A condition is a yes/no value stored **for this loop and this loop only**. It can be used to show new dialogue options, stop someone from talking to you (looking at you Slate), and more. + +Conditions can be set in dialogue using `CONDITION_NAME`. This can go in a DialogueNode in which case it will set the condition to true when that node is read. There is a similar version of this for DialogueOptions called `CONDITION_NAME` which will set it to true when that option is selected. Conditions can be disabled using `CONDITION_NAME` in a DialogueOption, but cannot be disabled just by entering a DialogueNode. + +You can lock a DialogueOption behind a condition using `CONDITION_NAME`, or remove a DialogueOption after the condition is set to true using `CONDITION_NAME`. + +Dialogue conditions can also be set in code with `DialogueConditionManager.SharedInstance.SetConditionState("CONDITION_NAME", true/false)` or read with `DialogueConditionManager.SharedInstance.GetConditionState("CONDITION_NAME")`. + +Note that `CONDITION_NAME` is a placeholder that you would replace with whatever you want to call your condition. Consider appending conditions with the name of your mod to make for better compatibility between mods, for example a condition name like `SPOKEN_TO` is very generic and might conflict with other mods whereas `NH_EXAMPLES_SPOKEN_TO_ERNESTO` is much less likely to conflict with another mod. + +### Persistent Condition + +A persistent condition is similar to a condition, except it _persists_ through loops, and is saved on the players save file. + +Persistent conditions shared many similar traits with regular dialogue conditions. You can use ``, ``. On dialogue options you can use ``, `` + +Persistent conditions can also be set in code with `PlayerData.SetPersistentCondition("PERSISTENT_CONDITION_NAME", true/false)` and read using `PlayerData.GetPersistentCondition("PERSISTENT_CONDITION_NAME")`. + +### Ship Logs + +Dialogue can interact with ship logs, either granting them to the player (`` on a DialogueNode) or locking dialogue behind ship log completion (`` on a DialogueOption). + +You can also use `` in tandem with `DialogueTarget` to make it so you must have a [ship log fact](/guides/ship-log#explore-facts) to go to the next node. + +## Adding to existing dialogue Here's an example of how to add new dialogue to Slate, without overwriting their existing dialogue. This will also allow multiple mods to all add new dialogue to the same character. @@ -221,8 +225,33 @@ To use this additional dialogue you need to reference it in a planet config file ] ``` +### ReuseDialogueOptionsListFrom + +This is a custom XML node introduced by New Horizons. Use it when adding new dialogue to existing characters, to repeat the dialogue options list from another node. + +For example, Slate's first dialogue with options is named `Scientist5`. To make a custom DialogueNode using these dialogue options (meaning new dialogue said by Slate, but reusing the possible player responses) you can write: + +```xml + + ... + + NEW DIALOGUE FOR SLATE HERE. + + + Scientist5 + + +``` + +Note: If you're loading dialogue in code, 2 frames must pass before entering the conversation in order for ReuseDialogueOptionsListFrom to take effect. + + ## Dialogue FAQ ### How do I easily position my dialogue relative to a speaking character Use `pathToAnimController` to specify the path to the speaking character (if they are a Nomai or Hearthian make sure this goes directly to whatever script controls their animations), then set `isRelativeToParent` to true (this is setting available on all NH props for easier positioning). Now when you set their `position`, it will be relative to the speaker. Since this position is normally where the character is standing, set the `y` position to match how tall the character is. Instead of `pathToAnimController` you can also use `parentPath`. + +### How do I have the dialogue prompt say "Read" or "Play recording" + +`` sets the name of the character, which will then show in the prompt to start dialogue. You can alternatively use `SIGN` to have the prompt say "Read", and `RECORDING` to have it say "Play recording". \ No newline at end of file diff --git a/docs/src/content/docs/guides/nomai-text.md b/docs/src/content/docs/guides/nomai-text.md new file mode 100644 index 00000000..959aeeb4 --- /dev/null +++ b/docs/src/content/docs/guides/nomai-text.md @@ -0,0 +1,26 @@ +--- +title: Nomai Text +description: Guide to making Nomai Text in New Horizons +--- + +This page goes over how to use Nomai text in New Horizons. + +## Understanding Nomai Text + +Nomai text is the backbone of many story mods. There are two parts to setting up Nomai text: The XML file and the planet config. + +### XML + +In your XML, you define the actual raw text which will be displayed, the ship logs it unlocks, and the way it branches. See [the Nomai text XML schema](/schemas/text-schema/) for more info. + +Nomai text contains a root `` node, followed by `` nodes and optionally a `` node. + +Nomai text is made up of `TextBlock`s. Each text block has an `ID` which must be unique (you can just number them for simplicity). After the first defined text block, each must have a `ParentID`. For scrolls and regular wall text, the text block only gets revealed after its parent block. Multiple text blocks can have the same parent, allowing for branching paths. In recorders and computers, each text block must procede in order (the second parented to the first, the third to the second, etc). In cairns, there is only one text block. + +To unlock ship logs after reading each text block, add a `` node. This can contains multiple `` nodes, each one defining a ``, ``. The ship log conditions node can either have `` or ``, which means the logs will unlock only if you are at that location. The `` lists the TextBlock ids which must be read to reveal the fact as a comma delimited list (e.g., `1,2,4`).. + +### Json + +In your planet config, you must define where the Nomai text is positioned. See [the translator text json schema](/schemas/body-schema/defs/translatortextinfo/) for more info. + +You can input a `seed` for a wall of text which will randomly generate the position of each arc. To test out different combinations, just keep incrementing the number and then hit "Reload Configs" from the pause menu with debug mode on. This seed ensures the same positioning each time the mod is played. Alternatively, you can use `arcInfo` to set the position and rotation of all text arcs, as well as determining their types (adult, teenager, child, or Stranger). The various age stages make the text look messier, while Stranger allows you to make a translatable version of the DLC text. \ No newline at end of file diff --git a/docs/src/content/docs/guides/publishing.md b/docs/src/content/docs/guides/publishing.md index dc1dba39..c1ab4204 100644 --- a/docs/src/content/docs/guides/publishing.md +++ b/docs/src/content/docs/guides/publishing.md @@ -14,6 +14,7 @@ Before you release anything, you'll want to make sure: - Your repo has the description field (click the cog in the right column on the "Code" tab) set. (this will be shown in the manager) - There's no `config.json` in your addon. (Not super important, but good practice) - Your manifest has a valid name, author, and unique name. +- You have included any caches New Horizons has made (i.e., slide reel caches). Since these are made in the install location of the mod you will have to manually copy them into the mod repo and ensure they stay up to date. While these files are not required, they ensure that your players will have faster loading times and reduced memory usage on their first loop (after which the caches will generate for them locally). ## Releasing