forked from nikita/muzika-gromche
				
			Compare commits
	
		
			10 Commits
		
	
	
		
			c7b67b9042
			...
			d02e594457
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | d02e594457 | |
|  | 05749ff122 | |
|  | f131ad7148 | |
|  | f50989b5ae | |
|  | 72adb9e713 | |
|  | 76e9ca3595 | |
|  | b6f2ca355b | |
|  | 78370da460 | |
|  | 4d84a2d001 | |
|  | 0eb02698eb | 
|  | @ -2,6 +2,9 @@ | ||||||
| 
 | 
 | ||||||
| ## MuzikaGromche 13.37.9001 | ## MuzikaGromche 13.37.9001 | ||||||
| 
 | 
 | ||||||
|  | - Fixed more missing flickering behaviours for some animators controllers. | ||||||
|  | - Fixed some powered lights not fully turning off or flickering when there are multiple Light components per container. | ||||||
|  | - Improved performance by pre-loading certain assets at the start of round instead of at a timing-critical frame update. | ||||||
| 
 | 
 | ||||||
| ## MuzikaGromche 13.37.1337 - Photosensitivity Warning Edition | ## MuzikaGromche 13.37.1337 - Photosensitivity Warning Edition | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,6 @@ | ||||||
| using DunGen; | using DunGen; | ||||||
|  | using HarmonyLib; | ||||||
|  | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.IO; | using System.IO; | ||||||
| using System.Linq; | using System.Linq; | ||||||
|  | @ -7,54 +9,21 @@ using UnityEngine; | ||||||
| 
 | 
 | ||||||
| namespace MuzikaGromche | namespace MuzikaGromche | ||||||
| { | { | ||||||
|     public class DiscoBallManager : MonoBehaviour |     public static class DiscoBallManager | ||||||
|     { |     { | ||||||
|         // A struct holding a disco ball container object and the name of a tile for which it was designed. |         // A struct holding a disco ball container object and the name of a tile for which it was designed. | ||||||
|         public readonly record struct Data(string TileName, GameObject DiscoBallContainer) |         private readonly record struct TilePatch(string TileName, GameObject DiscoBallContainer) | ||||||
|         { |         { | ||||||
|             // We are specifically looking for cloned tiles, not the original prototypes. |             // We are specifically looking for cloned tiles, not the original prototypes. | ||||||
|             public readonly string TileCloneName = $"{TileName}(Clone)"; |             public readonly string TileCloneName = $"{TileName}(Clone)"; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public static readonly List<Data> Containers = []; |         private static TilePatch[] Patches = []; | ||||||
|         private static readonly List<GameObject> InstantiatedContainers = []; |  | ||||||
| 
 | 
 | ||||||
|         public static void Initialize() |         private static readonly List<GameObject> CachedDiscoBalls = []; | ||||||
|         { |         private static readonly List<Animator> CachedDiscoBallAnimators = []; | ||||||
|             string bundlePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "muzikagromche_discoball"); |  | ||||||
|             var bundle = AssetBundle.LoadFromFile(bundlePath); |  | ||||||
| 
 | 
 | ||||||
|             foreach ((string prefabPath, string tileName) in new[] { |         private static readonly string[] AnimatorContainersNames = [ | ||||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerManor.prefab", "ManorStartRoomSmall"), |  | ||||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerManorOLD.prefab", "ManorStartRoom"), |  | ||||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerFactory.prefab", "StartRoom"), |  | ||||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerMineShaft.prefab", "MineshaftStartTile"), |  | ||||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerLargeForkTileB.prefab", "LargeForkTileB"), |  | ||||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerBirthdayRoomTile.prefab", "BirthdayRoomTile"), |  | ||||||
|             }) |  | ||||||
|             { |  | ||||||
|                 var container = bundle.LoadAsset<GameObject>(prefabPath); |  | ||||||
|                 Containers.Add(new(tileName, container)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public static void Enable() |  | ||||||
|         { |  | ||||||
|             // Just in case |  | ||||||
|             Disable(); |  | ||||||
| 
 |  | ||||||
|             var query = from tile in Resources.FindObjectsOfTypeAll<Tile>() |  | ||||||
|                         join container in Containers |  | ||||||
|                         on tile.gameObject.name equals container.TileCloneName |  | ||||||
|                         select (tile, container); |  | ||||||
| 
 |  | ||||||
|             foreach (var (tile, container) in query) |  | ||||||
|             { |  | ||||||
|                 Enable(tile, container); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private static readonly string[] animatorNames = [ |  | ||||||
|             "DiscoBallProp/AnimContainer", |             "DiscoBallProp/AnimContainer", | ||||||
|             "DiscoBallProp1/AnimContainer", |             "DiscoBallProp1/AnimContainer", | ||||||
|             "DiscoBallProp2/AnimContainer", |             "DiscoBallProp2/AnimContainer", | ||||||
|  | @ -63,29 +32,134 @@ namespace MuzikaGromche | ||||||
|             "DiscoBallProp5/AnimContainer", |             "DiscoBallProp5/AnimContainer", | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|         private static void Enable(Tile tile, Data container) |         public static void Load() | ||||||
|         { |         { | ||||||
|             Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Enabling at '{tile.gameObject.name}'"); |             const string BundleFileName = "muzikagromche_discoball"; | ||||||
|             var discoBall = Instantiate(container.DiscoBallContainer, tile.transform); |             string bundlePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), BundleFileName); | ||||||
|             InstantiatedContainers.Add(discoBall); |             var assetBundle = AssetBundle.LoadFromFile(bundlePath) | ||||||
|  |                 ?? throw new NullReferenceException("Failed to load bundle"); | ||||||
| 
 | 
 | ||||||
|             foreach (var animatorName in animatorNames) |             (string PrefabPath, string TileName)[] patchDescriptors = | ||||||
|  |             [ | ||||||
|  |                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerManor.prefab", "ManorStartRoomSmall"), | ||||||
|  |                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerManorOLD.prefab", "ManorStartRoom"), | ||||||
|  |                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerFactory.prefab", "StartRoom"), | ||||||
|  |                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerMineShaft.prefab", "MineshaftStartTile"), | ||||||
|  |                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerLargeForkTileB.prefab", "LargeForkTileB"), | ||||||
|  |                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerBirthdayRoomTile.prefab", "BirthdayRoomTile"), | ||||||
|  |             ]; | ||||||
|  | 
 | ||||||
|  |             Patches = [.. patchDescriptors.Select(d => | ||||||
|  |                 new TilePatch(d.TileName, assetBundle.LoadAsset<GameObject>(d.PrefabPath)) | ||||||
|  |             )]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         internal static void Patch(Tile tile) | ||||||
|         { |         { | ||||||
|                 if (discoBall.transform.Find(animatorName)?.gameObject is GameObject animator) |             var query = from patch in Patches | ||||||
|  |                         where tile.gameObject.name == patch.TileCloneName | ||||||
|  |                         select patch; | ||||||
|  | 
 | ||||||
|  |             // Should be just one, but FirstOrDefault() isn't usable with structs | ||||||
|  |             foreach (var patch in query) | ||||||
|             { |             { | ||||||
|                     animator.GetComponent<Animator>().SetBool("on", true); |                 Patch(tile, patch); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         static void Patch(Tile tile, TilePatch patch) | ||||||
|  |         { | ||||||
|  |             var discoBall = UnityEngine.Object.Instantiate(patch.DiscoBallContainer, tile.transform); | ||||||
|  |             if (discoBall == null) | ||||||
|  |             { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             foreach (var animator in FindDiscoBallAnimators(discoBall)) | ||||||
|  |             { | ||||||
|  |                 CachedDiscoBallAnimators.Add(animator); | ||||||
|  |             } | ||||||
|  |             CachedDiscoBalls.Add(discoBall); | ||||||
|  |             discoBall.SetActive(false); | ||||||
|  | 
 | ||||||
|  |             Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Patched tile '{tile.gameObject.name}'"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         static IEnumerable<Animator> FindDiscoBallAnimators(GameObject discoBall) | ||||||
|  |         { | ||||||
|  |             foreach (var animatorContainerName in AnimatorContainersNames) | ||||||
|  |             { | ||||||
|  |                 var transform = discoBall.transform.Find(animatorContainerName); | ||||||
|  |                 if (transform == null) | ||||||
|  |                 { | ||||||
|  |                     // Not all prefabs have all possible animators, and it's OK | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 var animator = transform.gameObject?.GetComponent<Animator>(); | ||||||
|  |                 if (animator == null) | ||||||
|  |                 { | ||||||
|  |                     // This would be weird | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 yield return animator; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public static void Toggle(bool on) | ||||||
|  |         { | ||||||
|  |             Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Toggle {(on ? "ON" : "OFF")} {CachedDiscoBallAnimators.Count} animators"); | ||||||
|  | 
 | ||||||
|  |             foreach (var discoBall in CachedDiscoBalls) | ||||||
|  |             { | ||||||
|  |                 discoBall.SetActive(true); | ||||||
|  |             } | ||||||
|  |             foreach (var animator in CachedDiscoBallAnimators) | ||||||
|  |             { | ||||||
|  |                 animator?.SetBool("on", on); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public static void Enable() | ||||||
|  |         { | ||||||
|  |             Toggle(true); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public static void Disable() |         public static void Disable() | ||||||
|         { |         { | ||||||
|             foreach (var discoBall in InstantiatedContainers) |             Toggle(false); | ||||||
|             { |  | ||||||
|                 Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)}: Disabling {discoBall.name}"); |  | ||||||
|                 Destroy(discoBall); |  | ||||||
|         } |         } | ||||||
|             InstantiatedContainers.Clear(); | 
 | ||||||
|  |         internal static void Clear() | ||||||
|  |         { | ||||||
|  |             Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Clearing {CachedDiscoBalls.Count} disco balls & {CachedDiscoBallAnimators.Count} animators"); | ||||||
|  |             CachedDiscoBallAnimators.Clear(); | ||||||
|  |             CachedDiscoBalls.Clear(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     [HarmonyPatch(typeof(Tile))] | ||||||
|  |     static class DiscoBallTilePatch | ||||||
|  |     { | ||||||
|  |         [HarmonyPatch(nameof(Tile.AddTriggerVolume))] | ||||||
|  |         [HarmonyPostfix] | ||||||
|  |         static void OnAddTriggerVolume(Tile __instance) | ||||||
|  |         { | ||||||
|  |             DiscoBallManager.Patch(__instance); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     [HarmonyPatch(typeof(RoundManager))] | ||||||
|  |     static class DiscoBallDespawnPatch | ||||||
|  |     { | ||||||
|  |         [HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))] | ||||||
|  |         [HarmonyPatch(nameof(RoundManager.OnDestroy))] | ||||||
|  |         [HarmonyPrefix] | ||||||
|  |         static void OnDestroy(RoundManager __instance) | ||||||
|  |         { | ||||||
|  |             var _ = __instance; | ||||||
|  |             DiscoBallManager.Clear(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -41,7 +41,7 @@ namespace MuzikaGromche | ||||||
|                 .Select(a => $" Trying... {a}") |                 .Select(a => $" Trying... {a}") | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|         public static Track[] Tracks = [ |         public static readonly Track[] Tracks = [ | ||||||
|             new Track |             new Track | ||||||
|             { |             { | ||||||
|                 Name = "MuzikaGromche", |                 Name = "MuzikaGromche", | ||||||
|  | @ -482,8 +482,8 @@ namespace MuzikaGromche | ||||||
|             return tracks[trackId]; |             return tracks[trackId]; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public static Track? CurrentTrack; |         internal static Track? CurrentTrack; | ||||||
|         public static BeatTimeState? BeatTimeState; |         internal static BeatTimeState? BeatTimeState; | ||||||
| 
 | 
 | ||||||
|         public static void SetLightColor(Color color) |         public static void SetLightColor(Color color) | ||||||
|         { |         { | ||||||
|  | @ -513,7 +513,14 @@ namespace MuzikaGromche | ||||||
|             return distance <= AudioMaxDistance; |             return distance <= AudioMaxDistance; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private void Awake() |         public static void DisplayLyrics(string text) | ||||||
|  |         { | ||||||
|  |             HUDManager.Instance.DisplayTip("[Lyrics]", text); | ||||||
|  |             // Don't interrupt the music with constant HUD audio pings | ||||||
|  |             HUDManager.Instance.UIAudio.Stop(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         void Awake() | ||||||
|         { |         { | ||||||
|             string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); |             string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); | ||||||
|             UnityWebRequest[] requests = new UnityWebRequest[Tracks.Length * 2]; |             UnityWebRequest[] requests = new UnityWebRequest[Tracks.Length * 2]; | ||||||
|  | @ -537,12 +544,16 @@ namespace MuzikaGromche | ||||||
|                     track.LoadedLoop = DownloadHandlerAudioClip.GetContent(requests[i * 2 + 1]); |                     track.LoadedLoop = DownloadHandlerAudioClip.GetContent(requests[i * 2 + 1]); | ||||||
|                 } |                 } | ||||||
|                 Config = new Config(base.Config); |                 Config = new Config(base.Config); | ||||||
|                 DiscoBallManager.Initialize(); |                 DiscoBallManager.Load(); | ||||||
|                 PoweredLightsAnimators.Load(); |                 PoweredLightsAnimators.Load(); | ||||||
|                 var harmony = new Harmony(PluginInfo.PLUGIN_NAME); |                 var harmony = new Harmony(PluginInfo.PLUGIN_NAME); | ||||||
|                 harmony.PatchAll(typeof(JesterPatch)); |                 harmony.PatchAll(typeof(JesterPatch)); | ||||||
|                 harmony.PatchAll(typeof(EnemyAIPatch)); |                 harmony.PatchAll(typeof(EnemyAIPatch)); | ||||||
|                 harmony.PatchAll(typeof(PoweredLightsAnimatorsPatch)); |                 harmony.PatchAll(typeof(PoweredLightsAnimatorsPatch)); | ||||||
|  |                 harmony.PatchAll(typeof(AllPoweredLightsPatch)); | ||||||
|  |                 harmony.PatchAll(typeof(DiscoBallTilePatch)); | ||||||
|  |                 harmony.PatchAll(typeof(DiscoBallDespawnPatch)); | ||||||
|  |                 harmony.PatchAll(typeof(SpawnRatePatch)); | ||||||
|             } |             } | ||||||
|             else |             else | ||||||
|             { |             { | ||||||
|  | @ -552,7 +563,7 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     public record Language(string Short, string Full) |     public readonly record struct Language(string Short, string Full) | ||||||
|     { |     { | ||||||
|         public static readonly Language ENGLISH = new("EN", "English"); |         public static readonly Language ENGLISH = new("EN", "English"); | ||||||
|         public static readonly Language RUSSIAN = new("RU", "Russian"); |         public static readonly Language RUSSIAN = new("RU", "Russian"); | ||||||
|  | @ -577,9 +588,9 @@ namespace MuzikaGromche | ||||||
|                 ? Mathf.Pow(2f, 20f * x - 10f) / 2f |                 ? Mathf.Pow(2f, 20f * x - 10f) / 2f | ||||||
|                 : (2f - Mathf.Pow(2f, -20f * x + 10f)) / 2f); |                 : (2f - Mathf.Pow(2f, -20f * x + 10f)) / 2f); | ||||||
| 
 | 
 | ||||||
|         public static Easing[] All = [Linear, InCubic, OutCubic, InOutCubic, InExpo, OutExpo, InOutExpo]; |         public static readonly Easing[] All = [Linear, InCubic, OutCubic, InOutCubic, InExpo, OutExpo, InOutExpo]; | ||||||
| 
 | 
 | ||||||
|         public static string[] AllNames => [.. All.Select(easing => easing.Name)]; |         public static readonly string[] AllNames = [.. All.Select(easing => easing.Name)]; | ||||||
| 
 | 
 | ||||||
|         public static Easing FindByName(string Name) |         public static Easing FindByName(string Name) | ||||||
|         { |         { | ||||||
|  | @ -592,9 +603,9 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public record Palette(Color[] Colors) |     public readonly record struct Palette(Color[] Colors) | ||||||
|     { |     { | ||||||
|         public static Palette DEFAULT = new([Color.magenta, Color.cyan, Color.green, Color.yellow]); |         public static readonly Palette DEFAULT = new([Color.magenta, Color.cyan, Color.green, Color.yellow]); | ||||||
| 
 | 
 | ||||||
|         public static Palette Parse(string[] hexColors) |         public static Palette Parse(string[] hexColors) | ||||||
|         { |         { | ||||||
|  | @ -776,7 +787,7 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public readonly record struct BeatTimestamp |     readonly record struct BeatTimestamp | ||||||
|     { |     { | ||||||
|         // Number of beats in the loop audio segment. |         // Number of beats in the loop audio segment. | ||||||
|         public readonly int LoopBeats; |         public readonly int LoopBeats; | ||||||
|  | @ -826,7 +837,7 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public readonly record struct BeatTimeSpan |     readonly record struct BeatTimeSpan | ||||||
|     { |     { | ||||||
|         public readonly int LoopBeats; |         public readonly int LoopBeats; | ||||||
|         public readonly float HalfLoopBeats => LoopBeats / 2f; |         public readonly float HalfLoopBeats => LoopBeats / 2f; | ||||||
|  | @ -976,7 +987,7 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public class BeatTimeState |     class BeatTimeState | ||||||
|     { |     { | ||||||
|         // The object is newly created, the Start audio began to play but its time hasn't adjanced from 0.0f yet. |         // The object is newly created, the Start audio began to play but its time hasn't adjanced from 0.0f yet. | ||||||
|         private bool hasStarted = false; |         private bool hasStarted = false; | ||||||
|  | @ -1243,9 +1254,9 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public abstract class BaseEvent; |     abstract class BaseEvent; | ||||||
| 
 | 
 | ||||||
|     public class SetLightsColorEvent(Color color) : BaseEvent |     class SetLightsColorEvent(Color color) : BaseEvent | ||||||
|     { |     { | ||||||
|         public readonly Color Color = color; |         public readonly Color Color = color; | ||||||
|         public override string ToString() |         public override string ToString() | ||||||
|  | @ -1254,7 +1265,7 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public class SetLightsColorTransitionEvent(Color from, Color to, Easing easing, float t) |     class SetLightsColorTransitionEvent(Color from, Color to, Easing easing, float t) | ||||||
|         : SetLightsColorEvent(Color.Lerp(from, to, Mathf.Clamp(easing.Eval(t), 0f, 1f))) |         : SetLightsColorEvent(Color.Lerp(from, to, Mathf.Clamp(easing.Eval(t), 0f, 1f))) | ||||||
|     { |     { | ||||||
|         // Additional context for debugging |         // Additional context for debugging | ||||||
|  | @ -1268,12 +1279,12 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public class FlickerLightsEvent : BaseEvent |     class FlickerLightsEvent : BaseEvent | ||||||
|     { |     { | ||||||
|         public override string ToString() => "Flicker"; |         public override string ToString() => "Flicker"; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public class LyricsEvent(string text) : BaseEvent |     class LyricsEvent(string text) : BaseEvent | ||||||
|     { |     { | ||||||
|         public readonly string Text = text; |         public readonly string Text = text; | ||||||
|         public override string ToString() |         public override string ToString() | ||||||
|  | @ -1282,14 +1293,14 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public class WindUpZeroBeatEvent : BaseEvent |     class WindUpZeroBeatEvent : BaseEvent | ||||||
|     { |     { | ||||||
|         public override string ToString() => "WindUp"; |         public override string ToString() => "WindUp"; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Default C#/.NET remainder operator % returns negative result for negative input |     // Default C#/.NET remainder operator % returns negative result for negative input | ||||||
|     // which is unsuitable as an index for an array. |     // which is unsuitable as an index for an array. | ||||||
|     public static class Mod |     static class Mod | ||||||
|     { |     { | ||||||
|         public static int Positive(int x, int m) |         public static int Positive(int x, int m) | ||||||
|         { |         { | ||||||
|  | @ -1309,7 +1320,7 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public readonly struct RandomWeightedIndex |     readonly struct RandomWeightedIndex | ||||||
|     { |     { | ||||||
|         public RandomWeightedIndex(int[] weights) |         public RandomWeightedIndex(int[] weights) | ||||||
|         { |         { | ||||||
|  | @ -1396,7 +1407,7 @@ namespace MuzikaGromche | ||||||
|         readonly public int TotalWeights { get; } |         readonly public int TotalWeights { get; } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static class SyncedEntryExtensions |     static class SyncedEntryExtensions | ||||||
|     { |     { | ||||||
|         // Update local values on clients. Even though the clients couldn't |         // Update local values on clients. Even though the clients couldn't | ||||||
|         // edit them, they could at least see the new values. |         // edit them, they could at least see the new values. | ||||||
|  | @ -1409,7 +1420,7 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public class Config : SyncedConfig2<Config> |     class Config : SyncedConfig2<Config> | ||||||
|     { |     { | ||||||
|         public static ConfigEntry<bool> DisplayLyrics { get; private set; } = null!; |         public static ConfigEntry<bool> DisplayLyrics { get; private set; } = null!; | ||||||
| 
 | 
 | ||||||
|  | @ -1417,6 +1428,8 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|         public static ConfigEntry<bool> SkipExplicitTracks { get; private set; } = null!; |         public static ConfigEntry<bool> SkipExplicitTracks { get; private set; } = null!; | ||||||
| 
 | 
 | ||||||
|  |         public static SyncedEntry<bool> OverrideSpawnRates { get; private set; } = null!; | ||||||
|  | 
 | ||||||
|         public static bool ShouldSkipWindingPhase { get; private set; } = false; |         public static bool ShouldSkipWindingPhase { get; private set; } = false; | ||||||
| 
 | 
 | ||||||
|         public static Palette? PaletteOverride { get; private set; } = null; |         public static Palette? PaletteOverride { get; private set; } = null; | ||||||
|  | @ -1430,7 +1443,7 @@ namespace MuzikaGromche | ||||||
|         public static float? ColorTransitionOutOverride { get; private set; } = null; |         public static float? ColorTransitionOutOverride { get; private set; } = null; | ||||||
|         public static string? ColorTransitionEasingOverride { get; private set; } = null; |         public static string? ColorTransitionEasingOverride { get; private set; } = null; | ||||||
| 
 | 
 | ||||||
|         public Config(ConfigFile configFile) : base(PluginInfo.PLUGIN_GUID) |         internal Config(ConfigFile configFile) : base(PluginInfo.PLUGIN_GUID) | ||||||
|         { |         { | ||||||
|             DisplayLyrics = configFile.Bind("General", "Display Lyrics", true, |             DisplayLyrics = configFile.Bind("General", "Display Lyrics", true, | ||||||
|                 new ConfigDescription("Display lyrics in the HUD tooltip when you hear the music.")); |                 new ConfigDescription("Display lyrics in the HUD tooltip when you hear the music.")); | ||||||
|  | @ -1445,6 +1458,11 @@ namespace MuzikaGromche | ||||||
|                 new ConfigDescription("When choosing tracks at random, skip the ones with Explicit Content/Lyrics.")); |                 new ConfigDescription("When choosing tracks at random, skip the ones with Explicit Content/Lyrics.")); | ||||||
|             LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(SkipExplicitTracks, requiresRestart: false)); |             LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(SkipExplicitTracks, requiresRestart: false)); | ||||||
| 
 | 
 | ||||||
|  |             OverrideSpawnRates = configFile.BindSyncedEntry("General", "Override Spawn Rates", false, | ||||||
|  |                 new ConfigDescription("Deviate from vanilla spawn rates to experience content of this mod more often.")); | ||||||
|  |             LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates.Entry, Default(new BoolCheckBoxOptions()))); | ||||||
|  |             CSyncHackAddSyncedEntry(OverrideSpawnRates); | ||||||
|  | 
 | ||||||
| #if DEBUG | #if DEBUG | ||||||
|             SetupEntriesToSkipWinding(configFile); |             SetupEntriesToSkipWinding(configFile); | ||||||
|             SetupEntriesForPaletteOverride(configFile); |             SetupEntriesForPaletteOverride(configFile); | ||||||
|  | @ -1759,19 +1777,26 @@ namespace MuzikaGromche | ||||||
|     // farAudio is during windup, Start overrides popGoesTheWeaselTheme |     // farAudio is during windup, Start overrides popGoesTheWeaselTheme | ||||||
|     // creatureVoice is when popped, Loop overrides screamingSFX |     // creatureVoice is when popped, Loop overrides screamingSFX | ||||||
|     [HarmonyPatch(typeof(JesterAI))] |     [HarmonyPatch(typeof(JesterAI))] | ||||||
|     internal class JesterPatch |     static class JesterPatch | ||||||
|     { |     { | ||||||
| #if DEBUG | #if DEBUG | ||||||
|         [HarmonyPatch(nameof(JesterAI.SetJesterInitialValues))] |         [HarmonyPatch(nameof(JesterAI.SetJesterInitialValues))] | ||||||
|         [HarmonyPostfix] |         [HarmonyPostfix] | ||||||
|         public static void AlmostInstantFollowTimerPostfix(JesterAI __instance) |         static void AlmostInstantFollowTimerPostfix(JesterAI __instance) | ||||||
|         { |         { | ||||||
|             __instance.beginCrankingTimer = 1f; |             __instance.beginCrankingTimer = 1f; | ||||||
|         } |         } | ||||||
| #endif | #endif | ||||||
|  | 
 | ||||||
|  |         class State | ||||||
|  |         { | ||||||
|  |             public required AudioSource farAudio; | ||||||
|  |             public required int previousState; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         [HarmonyPatch(nameof(JesterAI.Update))] |         [HarmonyPatch(nameof(JesterAI.Update))] | ||||||
|         [HarmonyPrefix] |         [HarmonyPrefix] | ||||||
|         public static void DoNotStopTheMusicPrefix(JesterAI __instance, out State __state) |         static void JesterUpdatePrefix(JesterAI __instance, out State __state) | ||||||
|         { |         { | ||||||
|             __state = new State |             __state = new State | ||||||
|             { |             { | ||||||
|  | @ -1793,7 +1818,7 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|         [HarmonyPatch(nameof(JesterAI.Update))] |         [HarmonyPatch(nameof(JesterAI.Update))] | ||||||
|         [HarmonyPostfix] |         [HarmonyPostfix] | ||||||
|         public static void DoNotStopTheMusic(JesterAI __instance, State __state) |         static void JesterUpdatePostfix(JesterAI __instance, State __state) | ||||||
|         { |         { | ||||||
|             if (__instance.previousState == 1 && __state.previousState != 1) |             if (__instance.previousState == 1 && __state.previousState != 1) | ||||||
|             { |             { | ||||||
|  | @ -1875,9 +1900,7 @@ namespace MuzikaGromche | ||||||
|                         case LyricsEvent e: |                         case LyricsEvent e: | ||||||
|                             if (Plugin.LocalPlayerCanHearMusic(__instance)) |                             if (Plugin.LocalPlayerCanHearMusic(__instance)) | ||||||
|                             { |                             { | ||||||
|                                 HUDManager.Instance.DisplayTip("[Lyrics]", e.Text); |                                 Plugin.DisplayLyrics(e.Text); | ||||||
|                                 // Don't interrupt the music with constant HUD audio pings |  | ||||||
|                                 HUDManager.Instance.UIAudio.Stop(); |  | ||||||
|                             } |                             } | ||||||
|                             break; |                             break; | ||||||
|                     } |                     } | ||||||
|  | @ -1887,13 +1910,13 @@ namespace MuzikaGromche | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HarmonyPatch(typeof(EnemyAI))] |     [HarmonyPatch(typeof(EnemyAI))] | ||||||
|     internal class EnemyAIPatch |     static class EnemyAIPatch | ||||||
|     { |     { | ||||||
|         // JesterAI class does not override abstract method OnDestroy, |         // JesterAI class does not override abstract method OnDestroy, | ||||||
|         // so we have to patch its superclass directly. |         // so we have to patch its superclass directly. | ||||||
|         [HarmonyPatch(nameof(EnemyAI.OnDestroy))] |         [HarmonyPatch(nameof(EnemyAI.OnDestroy))] | ||||||
|         [HarmonyPrefix] |         [HarmonyPrefix] | ||||||
|         public static void CleanUpOnDestroy(EnemyAI __instance) |         static void CleanUpOnDestroy(EnemyAI __instance) | ||||||
|         { |         { | ||||||
|             if (__instance is JesterAI) |             if (__instance is JesterAI) | ||||||
|             { |             { | ||||||
|  | @ -1905,10 +1928,4 @@ namespace MuzikaGromche | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     internal class State |  | ||||||
|     { |  | ||||||
|         public required AudioSource farAudio; |  | ||||||
|         public required int previousState; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,9 +9,17 @@ using UnityEngine; | ||||||
| 
 | 
 | ||||||
| namespace MuzikaGromche | namespace MuzikaGromche | ||||||
| { | { | ||||||
|     internal class PoweredLightsAnimators |     static class PoweredLightsAnimators | ||||||
|     { |     { | ||||||
|         private readonly record struct AnimatorPatch(string AnimatorContainerPath, RuntimeAnimatorController AnimatorController); |         private const string PoweredLightTag = "PoweredLight"; | ||||||
|  | 
 | ||||||
|  |         private delegate void ManualPatch(GameObject animatorContainer); | ||||||
|  | 
 | ||||||
|  |         private readonly record struct AnimatorPatch( | ||||||
|  |             string AnimatorContainerPath, | ||||||
|  |             RuntimeAnimatorController AnimatorController, | ||||||
|  |             bool AddTagAndAnimator, | ||||||
|  |             ManualPatch? ManualPatch); | ||||||
| 
 | 
 | ||||||
|         private readonly record struct TilePatch(string TileName, AnimatorPatch[] Patches) |         private readonly record struct TilePatch(string TileName, AnimatorPatch[] Patches) | ||||||
|         { |         { | ||||||
|  | @ -19,13 +27,17 @@ namespace MuzikaGromche | ||||||
|             public readonly string TileCloneName = $"{TileName}(Clone)"; |             public readonly string TileCloneName = $"{TileName}(Clone)"; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private readonly record struct AnimatorPatchDescriptor(string AnimatorContainerPath, string AnimatorControllerAssetPath) |         private readonly record struct AnimatorPatchDescriptor( | ||||||
|  |             string AnimatorContainerPath, | ||||||
|  |             string AnimatorControllerAssetPath, | ||||||
|  |             bool AddTagAndAnimator = false, | ||||||
|  |             ManualPatch? ManualPatch = null) | ||||||
|         { |         { | ||||||
|             public AnimatorPatch Load(AssetBundle assetBundle) |             public AnimatorPatch Load(AssetBundle assetBundle) | ||||||
|             { |             { | ||||||
|                 var animationController = assetBundle.LoadAsset<RuntimeAnimatorController>(AnimatorControllerAssetPath) |                 var animationController = assetBundle.LoadAsset<RuntimeAnimatorController>(AnimatorControllerAssetPath) | ||||||
|                     ?? throw new FileNotFoundException($"RuntimeAnimatorController not found: {AnimatorControllerAssetPath}", AnimatorControllerAssetPath); |                     ?? throw new FileNotFoundException($"RuntimeAnimatorController not found: {AnimatorControllerAssetPath}", AnimatorControllerAssetPath); | ||||||
|                 return new(AnimatorContainerPath, animationController); |                 return new(AnimatorContainerPath, animationController, AddTagAndAnimator, ManualPatch); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -45,6 +57,10 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|         private static IDictionary<string, TilePatch> Patches = new Dictionary<string, TilePatch>(); |         private static IDictionary<string, TilePatch> Patches = new Dictionary<string, TilePatch>(); | ||||||
| 
 | 
 | ||||||
|  |         private static AudioClip AudioClipOn = null!; | ||||||
|  |         private static AudioClip AudioClipOff = null!; | ||||||
|  |         private static AudioClip AudioClipFlicker = null!; | ||||||
|  | 
 | ||||||
|         public static void Load() |         public static void Load() | ||||||
|         { |         { | ||||||
|             const string BundleFileName = "muzikagromche_poweredlightsanimators"; |             const string BundleFileName = "muzikagromche_poweredlightsanimators"; | ||||||
|  | @ -52,16 +68,26 @@ namespace MuzikaGromche | ||||||
|             var assetBundle = AssetBundle.LoadFromFile(bundlePath) |             var assetBundle = AssetBundle.LoadFromFile(bundlePath) | ||||||
|                 ?? throw new NullReferenceException("Failed to load bundle"); |                 ?? throw new NullReferenceException("Failed to load bundle"); | ||||||
| 
 | 
 | ||||||
|  |             AudioClipOn = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightOn.ogg"); | ||||||
|  |             AudioClipOff = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightOff.ogg"); | ||||||
|  |             AudioClipFlicker = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightFlicker.ogg"); | ||||||
|  | 
 | ||||||
|             const string BasePath = "Assets/LethalCompany/Mods/MuzikaGromche/AnimatorControllers/"; |             const string BasePath = "Assets/LethalCompany/Mods/MuzikaGromche/AnimatorControllers/"; | ||||||
| 
 | 
 | ||||||
|             const string PointLight4 = $"{BasePath}Point Light (4) (Patched).controller"; |             const string PointLight4 = $"{BasePath}Point Light (4) (Patched).controller"; | ||||||
|             const string MineshaftSpotlight = $"{BasePath}MineshaftSpotlight (Patched).controller"; |             const string MineshaftSpotlight = $"{BasePath}MineshaftSpotlight (Patched).controller"; | ||||||
|             const string LightbulbsLine = $"{BasePath}lightbulbsLineMesh (Patched).controller"; |             const string LightbulbsLine = $"{BasePath}lightbulbsLineMesh (Patched).controller"; | ||||||
|             const string CeilingFan = $"{BasePath}CeilingFan (originally GameObject) (Patched).controller"; |             const string CeilingFan = $"{BasePath}CeilingFan (originally GameObject) (Patched).controller"; | ||||||
| 
 |             const string LEDHangingLight = $"{BasePath}LEDHangingLight (Patched).controller"; | ||||||
|  |             const string MineshaftStartTileSpotlight = $"{BasePath}MineshaftStartTileSpotlight (New).controller"; | ||||||
| 
 | 
 | ||||||
|             TilePatchDescriptor[] descriptors = |             TilePatchDescriptor[] descriptors = | ||||||
|             [ |             [ | ||||||
|  |                 // any version | ||||||
|  |                 new("KitchenTile", [ | ||||||
|  |                     new("PoweredLightTypeB", PointLight4), | ||||||
|  |                     new("PoweredLightTypeB (1)", PointLight4), | ||||||
|  |                 ]), | ||||||
|                 // < v70 |                 // < v70 | ||||||
|                 new("ManorStartRoom", [ |                 new("ManorStartRoom", [ | ||||||
|                     new("ManorStartRoom/Chandelier/PoweredLightTypeB (1)", PointLight4), |                     new("ManorStartRoom/Chandelier/PoweredLightTypeB (1)", PointLight4), | ||||||
|  | @ -72,6 +98,10 @@ namespace MuzikaGromche | ||||||
|                     new("ManorStartRoomMesh/Chandelier/PoweredLightTypeB (1)", PointLight4), |                     new("ManorStartRoomMesh/Chandelier/PoweredLightTypeB (1)", PointLight4), | ||||||
|                     new("ManorStartRoomMesh/Chandelier2/PoweredLightTypeB", PointLight4), |                     new("ManorStartRoomMesh/Chandelier2/PoweredLightTypeB", PointLight4), | ||||||
|                 ]), |                 ]), | ||||||
|  |                 new("NarrowHallwayTile2x2", [ | ||||||
|  |                     new("MineshaftSpotlight (1)", MineshaftSpotlight), | ||||||
|  |                     new("MineshaftSpotlight (2)", MineshaftSpotlight), | ||||||
|  |                 ]), | ||||||
|                 new("BirthdayRoomTile", [ |                 new("BirthdayRoomTile", [ | ||||||
|                     new("Lights/MineshaftSpotlight", MineshaftSpotlight), |                     new("Lights/MineshaftSpotlight", MineshaftSpotlight), | ||||||
|                 ]), |                 ]), | ||||||
|  | @ -83,6 +113,21 @@ namespace MuzikaGromche | ||||||
|                     new("CeilingFanAnimContainer", CeilingFan), |                     new("CeilingFanAnimContainer", CeilingFan), | ||||||
|                     new("MineshaftSpotlight (1)", MineshaftSpotlight), |                     new("MineshaftSpotlight (1)", MineshaftSpotlight), | ||||||
|                 ]), |                 ]), | ||||||
|  |                 new("GarageTile", [ | ||||||
|  |                     new("HangingLEDBarLight (3)", LEDHangingLight), | ||||||
|  |                     new("HangingLEDBarLight (4)", LEDHangingLight, | ||||||
|  |                         // This HangingLEDBarLight's IndirectLight is wrongly named, so animator couldn't find it | ||||||
|  |                         ManualPatch: RenameGameObjectPatch("IndirectLight (1)", "IndirectLight")), | ||||||
|  |                 ]), | ||||||
|  |                 new("PoolTile", [ | ||||||
|  |                     new("PoolLights/HangingLEDBarLight", LEDHangingLight), | ||||||
|  |                     new("PoolLights/HangingLEDBarLight (4)", LEDHangingLight), | ||||||
|  |                     new("PoolLights/HangingLEDBarLight (5)", LEDHangingLight), | ||||||
|  |                 ]), | ||||||
|  |                 new("MineshaftStartTile", [ | ||||||
|  |                     new("Cylinder.001 (1)", MineshaftStartTileSpotlight, AddTagAndAnimator: true), | ||||||
|  |                     new("Cylinder.001 (2)", MineshaftStartTileSpotlight, AddTagAndAnimator: true), | ||||||
|  |                 ]), | ||||||
|             ]; |             ]; | ||||||
| 
 | 
 | ||||||
|             Patches = descriptors |             Patches = descriptors | ||||||
|  | @ -112,7 +157,41 @@ namespace MuzikaGromche | ||||||
| #pragma warning restore CS0162 // Unreachable code detected | #pragma warning restore CS0162 // Unreachable code detected | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     var animator = animationContainerTransform.gameObject.GetComponent<Animator>(); |                     GameObject animationContainer = animationContainerTransform.gameObject; | ||||||
|  |                     Animator animator = animationContainer.GetComponent<Animator>(); | ||||||
|  | 
 | ||||||
|  |                     if (patch.AddTagAndAnimator) | ||||||
|  |                     { | ||||||
|  |                         animationContainer.tag = PoweredLightTag; | ||||||
|  |                         if (animator == null) | ||||||
|  |                         { | ||||||
|  |                             animator = animationContainer.AddComponent<Animator>(); | ||||||
|  |                         } | ||||||
|  |                         if (!animationContainer.TryGetComponent<PlayAudioAnimationEvent>(out var audioScript)) | ||||||
|  |                         { | ||||||
|  |                             audioScript = animationContainer.AddComponent<PlayAudioAnimationEvent>(); | ||||||
|  |                             audioScript.audioClip = AudioClipOn; | ||||||
|  |                             audioScript.audioClip2 = AudioClipOff; | ||||||
|  |                             audioScript.audioClip3 = AudioClipFlicker; | ||||||
|  |                         } | ||||||
|  |                         if (!animationContainer.TryGetComponent<AudioSource>(out var audioSource)) | ||||||
|  |                         { | ||||||
|  |                             // Copy from an existing AudioSource of another light animator | ||||||
|  |                             var otherSource = tile.gameObject.GetComponentInChildren<AudioSource>(); | ||||||
|  |                             if (otherSource != null) | ||||||
|  |                             { | ||||||
|  |                                 audioSource = animationContainer.AddComponent<AudioSource>(); | ||||||
|  |                                 audioSource.spatialBlend = 1; | ||||||
|  |                                 audioSource.playOnAwake = false; | ||||||
|  |                                 audioSource.outputAudioMixerGroup = otherSource.outputAudioMixerGroup; | ||||||
|  |                                 audioSource.spread = otherSource.spread; | ||||||
|  |                                 audioSource.rolloffMode = otherSource.rolloffMode; | ||||||
|  |                                 audioSource.maxDistance = otherSource.maxDistance; | ||||||
|  |                                 audioSource.SetCustomCurve(AudioSourceCurveType.CustomRolloff, otherSource.GetCustomCurve(AudioSourceCurveType.CustomRolloff)); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|                     if (animator == null) |                     if (animator == null) | ||||||
|                     { |                     { | ||||||
| #if DEBUG | #if DEBUG | ||||||
|  | @ -123,21 +202,89 @@ namespace MuzikaGromche | ||||||
| #pragma warning restore CS0162 // Unreachable code detected | #pragma warning restore CS0162 // Unreachable code detected | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|  |                     patch.ManualPatch?.Invoke(animationContainer); | ||||||
|  | 
 | ||||||
|                     animator.runtimeAnimatorController = patch.AnimatorController; |                     animator.runtimeAnimatorController = patch.AnimatorController; | ||||||
|                     Debug.Log($"{nameof(MuzikaGromche)} {nameof(PoweredLightsAnimatorsPatch)} {tilePatch.TileName}/{patch.AnimatorContainerPath}: Replaced animator controller"); |                     Debug.Log($"{nameof(MuzikaGromche)} {nameof(PoweredLightsAnimatorsPatch)} {tilePatch.TileName}/{patch.AnimatorContainerPath}: Replaced animator controller"); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         private static ManualPatch RenameGameObjectPatch(string relativePath, string newName) => animatorContainer => | ||||||
|  |         { | ||||||
|  |             var targetObject = animatorContainer.transform.Find(relativePath)?.gameObject; | ||||||
|  |             if (targetObject == null) | ||||||
|  |             { | ||||||
|  | #if DEBUG | ||||||
|  |                 throw new NullReferenceException($"{animatorContainer.name}/{relativePath}: GameObject not found!"); | ||||||
|  | #endif | ||||||
|  | #pragma warning disable CS0162 // Unreachable code detected | ||||||
|  |                 return; | ||||||
|  | #pragma warning restore CS0162 // Unreachable code detected | ||||||
|  |             } | ||||||
|  |             targetObject.name = newName; | ||||||
|  |             Debug.Log($"{nameof(MuzikaGromche)} {nameof(PoweredLightsAnimatorsPatch)} {animatorContainer.name}/{relativePath}: Renamed GameObject"); | ||||||
|  |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HarmonyPatch(typeof(Tile))] |     [HarmonyPatch(typeof(Tile))] | ||||||
|     internal class PoweredLightsAnimatorsPatch |     static class PoweredLightsAnimatorsPatch | ||||||
|     { |     { | ||||||
|         [HarmonyPatch("AddTriggerVolume")] |         [HarmonyPatch(nameof(Tile.AddTriggerVolume))] | ||||||
|         [HarmonyPostfix] |         [HarmonyPostfix] | ||||||
|         public static void OnAddTriggerVolume(Tile __instance) |         static void OnAddTriggerVolume(Tile __instance) | ||||||
|         { |         { | ||||||
|             PoweredLightsAnimators.Patch(__instance); |             PoweredLightsAnimators.Patch(__instance); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     [HarmonyPatch(typeof(RoundManager))] | ||||||
|  |     static class AllPoweredLightsPatch | ||||||
|  |     { | ||||||
|  |         // Vanilla method assumes that GameObjects with tag "PoweredLight" only contain a single Light component each. | ||||||
|  |         // This is, however, not true for certains double-light setups, such as: | ||||||
|  |         // - double PointLight (even though one of them is 'Point' another is 'Spot') inside CeilingFanAnimContainer in BedroomTile/BedroomTileB; | ||||||
|  |         // - MineshaftSpotlight when it has not only `Point Light` but also `IndirectLight` in BirthdayRoomTile; | ||||||
|  |         // - (maybe more?) | ||||||
|  |         // In order to fix that, replace singular GetComponentInChildren<Light> with plural GetComponentsInChildren<Light> version. | ||||||
|  |         [HarmonyPatch(nameof(RoundManager.RefreshLightsList))] | ||||||
|  |         [HarmonyPrefix] | ||||||
|  |         static bool OnRefreshLightsList(RoundManager __instance) | ||||||
|  |         { | ||||||
|  |             RefreshLightsListPatched(__instance); | ||||||
|  |             // Skip the original method | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         static void RefreshLightsListPatched(RoundManager self) | ||||||
|  |         { | ||||||
|  |             // Reusable list to reduce allocations | ||||||
|  |             List<Light> lights = []; | ||||||
|  | 
 | ||||||
|  |             self.allPoweredLights.Clear(); | ||||||
|  |             self.allPoweredLightsAnimators.Clear(); | ||||||
|  | 
 | ||||||
|  |             GameObject[] gameObjects = GameObject.FindGameObjectsWithTag("PoweredLight"); | ||||||
|  |             if (gameObjects == null) | ||||||
|  |             { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             foreach (var gameObject in gameObjects) | ||||||
|  |             { | ||||||
|  |                 Animator animator = gameObject.GetComponentInChildren<Animator>(); | ||||||
|  |                 if (!(animator == null)) | ||||||
|  |                 { | ||||||
|  |                     self.allPoweredLightsAnimators.Add(animator); | ||||||
|  |                     // Patched section: Use list instead of singular GetComponentInChildren<Light> | ||||||
|  |                     gameObject.GetComponentsInChildren(includeInactive: true, lights); | ||||||
|  |                     self.allPoweredLights.AddRange(lights); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             foreach (var animator in self.allPoweredLightsAnimators) | ||||||
|  |             { | ||||||
|  |                 animator.SetFloat("flickerSpeed", UnityEngine.Random.Range(0.6f, 1.4f)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,69 @@ | ||||||
|  | using HarmonyLib; | ||||||
|  | using System; | ||||||
|  | using UnityEngine; | ||||||
|  | 
 | ||||||
|  | namespace MuzikaGromche | ||||||
|  | { | ||||||
|  |     [HarmonyPatch(typeof(RoundManager))] | ||||||
|  |     static class SpawnRatePatch | ||||||
|  |     { | ||||||
|  |         const string JesterEnemyName = "Jester"; | ||||||
|  | 
 | ||||||
|  |         // If set to null, do not override spawn chances. Otherwise, it is an index of Jester in RoundManager.currentLevel.Enemies | ||||||
|  |         static int? JesterEnemyIndex = null; | ||||||
|  |         static float? SpawnTime = null; | ||||||
|  | 
 | ||||||
|  |         [HarmonyPatch(nameof(RoundManager.AssignRandomEnemyToVent))] | ||||||
|  |         [HarmonyPrefix] | ||||||
|  |         static void AssignRandomEnemyToVentPrefix(RoundManager __instance, EnemyVent vent, float spawnTime) | ||||||
|  |         { | ||||||
|  |             if (!Config.OverrideSpawnRates.Value) | ||||||
|  |             { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             var index = __instance.currentLevel.Enemies.FindIndex(enemy => enemy.enemyType.enemyName == JesterEnemyName); | ||||||
|  |             if (index == -1) | ||||||
|  |             { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             JesterEnemyIndex = index; | ||||||
|  |             SpawnTime = spawnTime; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         [HarmonyPatch(nameof(RoundManager.AssignRandomEnemyToVent))] | ||||||
|  |         [HarmonyPostfix] | ||||||
|  |         static void AssignRandomEnemyToVentPostfix(RoundManager __instance, EnemyVent vent, float spawnTime) | ||||||
|  |         { | ||||||
|  |             JesterEnemyIndex = null; | ||||||
|  |             SpawnTime = null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         [HarmonyPatch(nameof(RoundManager.GetRandomWeightedIndex))] | ||||||
|  |         [HarmonyPostfix] | ||||||
|  |         static void GetRandomWeightedIndexPostfix(RoundManager __instance, ref int[] weights, ref System.Random randomSeed) | ||||||
|  |         { | ||||||
|  |             if (JesterEnemyIndex is int index && SpawnTime is float spawnTime) | ||||||
|  |             { | ||||||
|  |                 if (__instance.EnemyCannotBeSpawned(index)) | ||||||
|  |                 { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 var minMultiplierTime = 3 * __instance.timeScript.lengthOfHours; | ||||||
|  |                 var maxMultiplierTime = 7 * __instance.timeScript.lengthOfHours; | ||||||
|  |                 var normalizedMultiplierTime = Math.Clamp((spawnTime - minMultiplierTime) / (maxMultiplierTime - minMultiplierTime), 0f, 1f); | ||||||
|  | 
 | ||||||
|  |                 // TODO: Try Expo function instead of Lerp? | ||||||
|  |                 var minMultiplier = Mathf.Max(1, __instance.minEnemiesToSpawn); | ||||||
|  |                 var maxMultiplier = 9 + __instance.minEnemiesToSpawn; | ||||||
|  |                 var multiplier = Mathf.Lerp(minMultiplier, maxMultiplier, normalizedMultiplierTime); | ||||||
|  | 
 | ||||||
|  |                 var newWeight = (int)(weights[index] * multiplier); | ||||||
|  |                 Debug.Log($"{nameof(MuzikaGromche)} Overriding Jester spawn weight {weights[index]} * {multiplier} => {newWeight}"); | ||||||
|  |                 weights[index] = newWeight; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
										
											Binary file not shown.
										
									
								
							
		Loading…
	
		Reference in New Issue