forked from nikita/muzika-gromche
				
			Compare commits
	
		
			4 Commits
		
	
	
		
			585ef604ff
			...
			ceaac4e01b
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | ceaac4e01b | |
|  | aea755361b | |
|  | e67c72951e | |
|  | 0fadf50bf4 | 
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -1,5 +1,10 @@ | ||||||
| # Changelog | # Changelog | ||||||
| 
 | 
 | ||||||
|  | ## MuzikaGromche 1337.420.9004 - Life Support Edition | ||||||
|  | 
 | ||||||
|  | - Override Death Screen / Game Over text in certain cases. | ||||||
|  | - Added a new track AttentionPls featuring multiple intro variants and new visual effects. | ||||||
|  | 
 | ||||||
| ## MuzikaGromche 1337.420.9003 - Lights Out Edition | ## MuzikaGromche 1337.420.9003 - Lights Out Edition | ||||||
| 
 | 
 | ||||||
| - Fixed wrong colors during fade out transition, e.g. in Mineshaft tunnel tiles. | - Fixed wrong colors during fade out transition, e.g. in Mineshaft tunnel tiles. | ||||||
|  |  | ||||||
|  | @ -0,0 +1,72 @@ | ||||||
|  | using System.Collections; | ||||||
|  | using HarmonyLib; | ||||||
|  | using TMPro; | ||||||
|  | using UnityEngine; | ||||||
|  | 
 | ||||||
|  | namespace MuzikaGromche | ||||||
|  | { | ||||||
|  |     static class DeathScreenGameOverTextManager | ||||||
|  |     { | ||||||
|  |         private const string GameOverTextVanilla = "[LIFE SUPPORT: OFFLINE]"; | ||||||
|  | 
 | ||||||
|  |         public const string GameOverTextModdedDefault = "[ MUZIKA: GROMCHE ]"; | ||||||
|  | 
 | ||||||
|  |         public static void Clear() | ||||||
|  |         { | ||||||
|  |             SetTextImpl(GameOverTextVanilla); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public static void SetText(string? text) | ||||||
|  |         { | ||||||
|  |             SetTextImpl(text ?? GameOverTextModdedDefault); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public static IEnumerator SetTextAndClear(string? text) | ||||||
|  |         { | ||||||
|  |             SetText(text); | ||||||
|  |             // Game Over animation duration is about 4.25 seconds | ||||||
|  |             yield return new WaitForSeconds(5f); | ||||||
|  |             Clear(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static void SetTextImpl(string text) | ||||||
|  |         { | ||||||
|  |             GameObject textGameObject = GameObject.Find("Systems/UI/Canvas/DeathScreen/GameOverText"); | ||||||
|  |             if (textGameObject == null) | ||||||
|  |             { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             TextMeshProUGUI tmp = textGameObject.GetComponent<TextMeshProUGUI>(); | ||||||
|  |             if (tmp == null) | ||||||
|  |             { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             var transform = textGameObject.GetComponent<RectTransform>(); | ||||||
|  |             if (transform == null) | ||||||
|  |             { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Default transform width is 645.8 which only fit the default message and no extra characters | ||||||
|  |             Vector2 size = transform.sizeDelta; | ||||||
|  |             if (Mathf.Approximately(size.x, 645.8f)) | ||||||
|  |             { | ||||||
|  |                 size.x += 100f; | ||||||
|  |                 transform.sizeDelta = size; | ||||||
|  |             } | ||||||
|  |             tmp.text = text; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     [HarmonyPatch(typeof(RoundManager))] | ||||||
|  |     static class DeathScreenGameOverTextResetPatch | ||||||
|  |     { | ||||||
|  |         [HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))] | ||||||
|  |         [HarmonyPatch(nameof(RoundManager.OnDestroy))] | ||||||
|  |         [HarmonyPrefix] | ||||||
|  |         static void OnDestroy(RoundManager __instance) | ||||||
|  |         { | ||||||
|  |             var _ = __instance; | ||||||
|  |             DeathScreenGameOverTextManager.Clear(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
|         <AssemblyName>Ratijas.MuzikaGromche</AssemblyName> |         <AssemblyName>Ratijas.MuzikaGromche</AssemblyName> | ||||||
|         <Product>Muzika Gromche</Product> |         <Product>Muzika Gromche</Product> | ||||||
|         <Description>Add some content to your inverse teleporter experience on Titan!</Description> |         <Description>Add some content to your inverse teleporter experience on Titan!</Description> | ||||||
|         <Version>1337.420.9003</Version> |         <Version>1337.420.9004</Version> | ||||||
|         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> |         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||||
|         <LangVersion>latest</LangVersion> |         <LangVersion>latest</LangVersion> | ||||||
|         <Nullable>enable</Nullable> |         <Nullable>enable</Nullable> | ||||||
|  | @ -52,6 +52,15 @@ | ||||||
|         <Reference Include="Unity.Netcode.Runtime" Publicize="true" Private="false"> |         <Reference Include="Unity.Netcode.Runtime" Publicize="true" Private="false"> | ||||||
|             <HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Netcode.Runtime.dll</HintPath> |             <HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Netcode.Runtime.dll</HintPath> | ||||||
|         </Reference> |         </Reference> | ||||||
|  |         <Reference Include="Unity.TextMeshPro" Publicize="true" Private="False"> | ||||||
|  |             <HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.TextMeshPro.dll</HintPath>  | ||||||
|  |         </Reference> | ||||||
|  |         <Reference Include="UnityEngine.UI" Publicize="true" Private="False"> | ||||||
|  |             <HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\UnityEngine.UI.dll</HintPath>  | ||||||
|  |         </Reference> | ||||||
|  |         <Reference Include="Unity.RenderPipelines.Core.Runtime" Publicize="true" Private="False"> | ||||||
|  |             <HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.RenderPipelines.Core.Runtime.dll</HintPath>  | ||||||
|  |         </Reference> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
| 
 | 
 | ||||||
|     <ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'"> |     <ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'"> | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ using System.Net.NetworkInformation; | ||||||
| using System.Net.Sockets; | using System.Net.Sockets; | ||||||
| using System.Reflection; | using System.Reflection; | ||||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||||
|  | using System.Text; | ||||||
| using Unity.Netcode; | using Unity.Netcode; | ||||||
| using UnityEngine; | using UnityEngine; | ||||||
| using UnityEngine.Networking; | using UnityEngine.Networking; | ||||||
|  | @ -56,6 +57,9 @@ namespace MuzikaGromche | ||||||
|                 ColorTransitionOut = 0.25f, |                 ColorTransitionOut = 0.25f, | ||||||
|                 ColorTransitionEasing = Easing.OutExpo, |                 ColorTransitionEasing = Easing.OutExpo, | ||||||
|                 FlickerLightsTimeSeries = [-5, 29, 61], |                 FlickerLightsTimeSeries = [-5, 29, 61], | ||||||
|  |                 DrunknessLoopOffsetTimeSeries = new( | ||||||
|  |                     [-2f, 0.0f, 1.0f, 03f, 30f,  32f,  33f, 35f, 62f], | ||||||
|  |                     [ 0f, 0.4f, 0.6f, 0f,   0f, 0.5f, 0.7f, 0f,   0f]), | ||||||
|                 Palette = Palette.Parse(["#B300FF", "#FFF100", "#00FF51", "#474747", "#FF00B3", "#0070FF"]), |                 Palette = Palette.Parse(["#B300FF", "#FFF100", "#00FF51", "#474747", "#FF00B3", "#0070FF"]), | ||||||
|                 Lyrics = [ |                 Lyrics = [ | ||||||
|                     (-68, "Devchata pljashut pod spidami"), |                     (-68, "Devchata pljashut pod spidami"), | ||||||
|  | @ -132,6 +136,9 @@ namespace MuzikaGromche | ||||||
|                 ColorTransitionOut = 0.25f, |                 ColorTransitionOut = 0.25f, | ||||||
|                 ColorTransitionEasing = Easing.OutExpo, |                 ColorTransitionEasing = Easing.OutExpo, | ||||||
|                 FlickerLightsTimeSeries = [-101, -93, -77, -61, -37, -5, 27], |                 FlickerLightsTimeSeries = [-101, -93, -77, -61, -37, -5, 27], | ||||||
|  |                 DrunknessLoopOffsetTimeSeries = new( | ||||||
|  |                     [-48f, -46f, -42f, 16f, 19f, 23f], | ||||||
|  |                     [  0f, 0.7f,   0f,  0f, 0.3f, 0f]), | ||||||
|                 Palette = Palette.Parse(["#217F87", "#BAFF00", "#73BE25", "#78AB4E", "#FFFF00"]), |                 Palette = Palette.Parse(["#217F87", "#BAFF00", "#73BE25", "#78AB4E", "#FFFF00"]), | ||||||
|                 Lyrics = [ |                 Lyrics = [ | ||||||
|                     (-111, "Deploy Destroy, porjadok eto otstoj"), |                     (-111, "Deploy Destroy, porjadok eto otstoj"), | ||||||
|  | @ -260,6 +267,7 @@ namespace MuzikaGromche | ||||||
|                 FadeOutDuration = 4, |                 FadeOutDuration = 4, | ||||||
|                 FlickerLightsTimeSeries = [-5, 31], |                 FlickerLightsTimeSeries = [-5, 31], | ||||||
|                 Lyrics = [], |                 Lyrics = [], | ||||||
|  |                 GameOverText = "[MUZIKA GROMCHE: K-POP]", | ||||||
|             }, |             }, | ||||||
|             new SelectableAudioTrack |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|  | @ -278,6 +286,7 @@ namespace MuzikaGromche | ||||||
|                 FadeOutDuration = 4, |                 FadeOutDuration = 4, | ||||||
|                 FlickerLightsTimeSeries = [-5], |                 FlickerLightsTimeSeries = [-5], | ||||||
|                 Lyrics = [], |                 Lyrics = [], | ||||||
|  |                 GameOverText = "[COULD'VE BEEN: IMMORTAL]", | ||||||
|             }, |             }, | ||||||
|             new SelectableAudioTrack |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|  | @ -296,6 +305,7 @@ namespace MuzikaGromche | ||||||
|                 FadeOutDuration = 4, |                 FadeOutDuration = 4, | ||||||
|                 FlickerLightsTimeSeries = [-5.5f, 31, 63.9f], |                 FlickerLightsTimeSeries = [-5.5f, 31, 63.9f], | ||||||
|                 Lyrics = [], |                 Lyrics = [], | ||||||
|  |                 GameOverText = "[ HEY, YOUNG BLOOD ]", | ||||||
|             }, |             }, | ||||||
|             new SelectableAudioTrack |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|  | @ -443,6 +453,7 @@ namespace MuzikaGromche | ||||||
|                     (96, $"\t\t\tresolving ur private IP\n/{PwnLyricsVariants[^1]}"), |                     (96, $"\t\t\tresolving ur private IP\n/{PwnLyricsVariants[^1]}"), | ||||||
|                     (98, $"\t\t\tresolving ur private IP\nP_WNED"), |                     (98, $"\t\t\tresolving ur private IP\nP_WNED"), | ||||||
|                 ], |                 ], | ||||||
|  |                 GameOverText = "[HACK3D BY: RUSSI4NS]", | ||||||
|             }, |             }, | ||||||
|             new SelectableAudioTrack |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|  | @ -467,6 +478,7 @@ namespace MuzikaGromche | ||||||
|                 FadeOutDuration = 6, |                 FadeOutDuration = 6, | ||||||
|                 FlickerLightsTimeSeries = [-120.5f, -105, -89, -8, 44, 45], |                 FlickerLightsTimeSeries = [-120.5f, -105, -89, -8, 44, 45], | ||||||
|                 Lyrics = [], |                 Lyrics = [], | ||||||
|  |                 GameOverText = "[DIDN'T PUMP IT: LOUDER]", | ||||||
|             }, |             }, | ||||||
|             new SelectableAudioTrack |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|  | @ -578,6 +590,57 @@ namespace MuzikaGromche | ||||||
|                 FlickerLightsTimeSeries = [-68.5f, -16.5f, 30.5f], |                 FlickerLightsTimeSeries = [-68.5f, -16.5f, 30.5f], | ||||||
|                 Lyrics = [], |                 Lyrics = [], | ||||||
|             }, |             }, | ||||||
|  |             new SelectableTracksGroup | ||||||
|  |             { | ||||||
|  |                 Name = "AttentionPls", | ||||||
|  |                 Language = Language.RUSSIAN, | ||||||
|  |                 IsExplicit = true, | ||||||
|  |                 Tracks = | ||||||
|  |                 [ | ||||||
|  |                     new SelectableAudioTrack | ||||||
|  |                     { | ||||||
|  |                         Name = "AttentionPls1", | ||||||
|  |                         FileNameLoop = "AttentionPlsLoop.ogg", | ||||||
|  |                         AudioType = AudioType.OGGVORBIS, | ||||||
|  |                         Language = Language.RUSSIAN, | ||||||
|  |                         WindUpTimer = 39.19f, | ||||||
|  |                         Bars = 8, | ||||||
|  |                         BeatsOffset = 0.3f, | ||||||
|  |                         ColorTransitionIn = 0.4f, | ||||||
|  |                         ColorTransitionOut = 0.4f, | ||||||
|  |                         ColorTransitionEasing = Easing.OutExpo, | ||||||
|  |                         Palette = Palette.Parse(["#FCEB3C", "#FC3C9D", "#65C7FA", "#89FC8F", "#FEE9E9", "#FCEB3C", "#89FC8F", "#FC3C9D"]), | ||||||
|  |                         LoopOffset = 0, | ||||||
|  |                         FadeOutBeat = -6, | ||||||
|  |                         FadeOutDuration = 5, | ||||||
|  |                         FlickerLightsTimeSeries = [-8, 31], | ||||||
|  |                         Lyrics = [], | ||||||
|  |                         DrunknessLoopOffsetTimeSeries = new([7f, 12f, 15f], [0f, 0.90f, 0f]), | ||||||
|  |                         CondensationLoopOffsetTimeSeries = new([23f, 28f, 31f], [0f, 0.4f, 0f]), | ||||||
|  |                     }, | ||||||
|  |                     new SelectableAudioTrack | ||||||
|  |                     { | ||||||
|  |                         Name = "AttentionPls2", | ||||||
|  |                         FileNameLoop = "AttentionPlsLoop.ogg", | ||||||
|  |                         AudioType = AudioType.OGGVORBIS, | ||||||
|  |                         Language = Language.RUSSIAN, | ||||||
|  |                         WindUpTimer = 39.19f, | ||||||
|  |                         Bars = 8, | ||||||
|  |                         BeatsOffset = 0.3f, | ||||||
|  |                         ColorTransitionIn = 0.4f, | ||||||
|  |                         ColorTransitionOut = 0.4f, | ||||||
|  |                         ColorTransitionEasing = Easing.OutExpo, | ||||||
|  |                         Palette = Palette.Parse(["#FCEB3C", "#FC3C9D", "#65C7FA", "#89FC8F", "#FEE9E9", "#FCEB3C", "#89FC8F", "#FC3C9D"]), | ||||||
|  |                         LoopOffset = 0, | ||||||
|  |                         FadeOutBeat = -6, | ||||||
|  |                         FadeOutDuration = 5, | ||||||
|  |                         FlickerLightsTimeSeries = [-8, 31], | ||||||
|  |                         Lyrics = [], | ||||||
|  |                         DrunknessLoopOffsetTimeSeries = new([7f, 12f, 15f], [0f, 0.90f, 0f]), | ||||||
|  |                         CondensationLoopOffsetTimeSeries = new([23f, 28f, 31f], [0f, 0.4f, 0f]), | ||||||
|  |                     }, | ||||||
|  |                 ], | ||||||
|  |             }, | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|         public static ISelectableTrack ChooseTrack() |         public static ISelectableTrack ChooseTrack() | ||||||
|  | @ -680,6 +743,8 @@ namespace MuzikaGromche | ||||||
|                 harmony.PatchAll(typeof(DiscoBallTilePatch)); |                 harmony.PatchAll(typeof(DiscoBallTilePatch)); | ||||||
|                 harmony.PatchAll(typeof(DiscoBallDespawnPatch)); |                 harmony.PatchAll(typeof(DiscoBallDespawnPatch)); | ||||||
|                 harmony.PatchAll(typeof(SpawnRatePatch)); |                 harmony.PatchAll(typeof(SpawnRatePatch)); | ||||||
|  |                 harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch)); | ||||||
|  |                 harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch)); | ||||||
|                 NetcodePatcher(); |                 NetcodePatcher(); | ||||||
|                 Compatibility.Register(this); |                 Compatibility.Register(this); | ||||||
|             } |             } | ||||||
|  | @ -789,6 +854,35 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public readonly struct TimeSeries<T> | ||||||
|  |     { | ||||||
|  |         public TimeSeries() : this([], []) { } | ||||||
|  | 
 | ||||||
|  |         public TimeSeries(float[] beats, T[] values) | ||||||
|  |         { | ||||||
|  |             if (beats.Length != values.Length) | ||||||
|  |             { | ||||||
|  |                 throw new ArgumentOutOfRangeException($"Time series length mismatch: {beats.Length} != {values.Length}"); | ||||||
|  |             } | ||||||
|  |             var dict = new SortedDictionary<float, T>(); | ||||||
|  |             for (int i = 0; i < values.Length; i++) | ||||||
|  |             { | ||||||
|  |                 dict.Add(beats[i], values[i]); | ||||||
|  |             } | ||||||
|  |             Beats = [.. dict.Keys]; | ||||||
|  |             Values = [.. dict.Values]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public readonly int Length => Beats.Length; | ||||||
|  |         public readonly float[] Beats { get; } = []; | ||||||
|  |         public readonly T[] Values { get; } = []; | ||||||
|  | 
 | ||||||
|  |         public override string ToString() | ||||||
|  |         { | ||||||
|  |             return $"{nameof(TimeSeries<T>)}([{string.Join(", ", Beats)}], [{string.Join(", ", Values)}])"; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // An instance of a track which appears as a configuration entry and |     // An instance of a track which appears as a configuration entry and | ||||||
|     // can be selected using weighted random from a list of selectable tracks. |     // can be selected using weighted random from a list of selectable tracks. | ||||||
|     public interface ISelectableTrack |     public interface ISelectableTrack | ||||||
|  | @ -881,7 +975,12 @@ namespace MuzikaGromche | ||||||
|         // If the chosen alternative is an empty string, lyrics event shall be skipped. |         // If the chosen alternative is an empty string, lyrics event shall be skipped. | ||||||
|         public string[] LyricsLines { get; } |         public string[] LyricsLines { get; } | ||||||
| 
 | 
 | ||||||
|  |         public TimeSeries<float> DrunknessLoopOffsetTimeSeries { get; } | ||||||
|  |         public TimeSeries<float> CondensationLoopOffsetTimeSeries { get; } | ||||||
|  | 
 | ||||||
|         public Palette Palette { get; } |         public Palette Palette { get; } | ||||||
|  | 
 | ||||||
|  |         public string? GameOverText { get => null; } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // A proxy audio track with default implementation for every IAudioTrack method that simply forwards requests to the inner IAudioTrack. |     // A proxy audio track with default implementation for every IAudioTrack method that simply forwards requests to the inner IAudioTrack. | ||||||
|  | @ -906,7 +1005,10 @@ namespace MuzikaGromche | ||||||
|         float[] IAudioTrack.FlickerLightsTimeSeries => Track.FlickerLightsTimeSeries; |         float[] IAudioTrack.FlickerLightsTimeSeries => Track.FlickerLightsTimeSeries; | ||||||
|         float[] IAudioTrack.LyricsTimeSeries => Track.LyricsTimeSeries; |         float[] IAudioTrack.LyricsTimeSeries => Track.LyricsTimeSeries; | ||||||
|         string[] IAudioTrack.LyricsLines => Track.LyricsLines; |         string[] IAudioTrack.LyricsLines => Track.LyricsLines; | ||||||
|  |         TimeSeries<float> IAudioTrack.DrunknessLoopOffsetTimeSeries => Track.DrunknessLoopOffsetTimeSeries; | ||||||
|  |         TimeSeries<float> IAudioTrack.CondensationLoopOffsetTimeSeries => Track.CondensationLoopOffsetTimeSeries; | ||||||
|         Palette IAudioTrack.Palette => Track.Palette; |         Palette IAudioTrack.Palette => Track.Palette; | ||||||
|  |         string? IAudioTrack.GameOverText => Track.GameOverText; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Core audio track implementation with some defaults and config overrides. |     // Core audio track implementation with some defaults and config overrides. | ||||||
|  | @ -981,7 +1083,12 @@ namespace MuzikaGromche | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         public TimeSeries<float> DrunknessLoopOffsetTimeSeries { get; init; } = new(); | ||||||
|  |         public TimeSeries<float> CondensationLoopOffsetTimeSeries { get; init; } = new(); | ||||||
|  | 
 | ||||||
|         public Palette Palette { get; set; } = Palette.DEFAULT; |         public Palette Palette { get; set; } = Palette.DEFAULT; | ||||||
|  | 
 | ||||||
|  |         public string? GameOverText { get; init; } = null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Standalone, top-level, selectable audio track |     // Standalone, top-level, selectable audio track | ||||||
|  | @ -1187,9 +1294,19 @@ namespace MuzikaGromche | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public readonly float Duration() |         public readonly float Duration(bool longest = false) | ||||||
|         { |         { | ||||||
|             if (IsEmpty()) |             if (longest) | ||||||
|  |             { | ||||||
|  |                 var to = BeatToInclusive; | ||||||
|  |                 if (BeatFromExclusive >= 0f && BeatToInclusive >= 0f && to < BeatFromExclusive) | ||||||
|  |                 { | ||||||
|  |                     // wrapped | ||||||
|  |                     to += LoopBeats; | ||||||
|  |                 } | ||||||
|  |                 return Mathf.Max(0f, to - BeatFromExclusive); | ||||||
|  |             } | ||||||
|  |             else if (IsEmpty()) | ||||||
|             { |             { | ||||||
|                 return 0f; |                 return 0f; | ||||||
|             } |             } | ||||||
|  | @ -1444,8 +1561,8 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|             if (AudioState.HasStarted) |             if (AudioState.HasStarted) | ||||||
|             { |             { | ||||||
|                 var loopTimestamp = Update(LoopLoopingState); |                 var loopOffsetTimestamp = Update(LoopLoopingState); | ||||||
|                 var loopOffsetSpan = BeatTimeSpan.Between(LastKnownLoopOffsetBeat, loopTimestamp); |                 var loopOffsetSpan = BeatTimeSpan.Between(LastKnownLoopOffsetBeat, loopOffsetTimestamp); | ||||||
| 
 | 
 | ||||||
|                 // Do not go back in time |                 // Do not go back in time | ||||||
|                 if (!loopOffsetSpan.IsEmpty()) |                 if (!loopOffsetSpan.IsEmpty()) | ||||||
|  | @ -1456,8 +1573,8 @@ namespace MuzikaGromche | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     var windUpOffsetTimestamp = Update(WindUpLoopingState); |                     var windUpOffsetTimestamp = Update(WindUpLoopingState); | ||||||
|                     LastKnownLoopOffsetBeat = loopTimestamp.Beat; |                     LastKnownLoopOffsetBeat = loopOffsetTimestamp.Beat; | ||||||
|                     var events = GetEvents(loopOffsetSpan, windUpOffsetTimestamp); |                     var events = GetEvents(loopOffsetTimestamp, loopOffsetSpan, windUpOffsetTimestamp); | ||||||
| #if DEBUG | #if DEBUG | ||||||
|                     Debug.Log($"{nameof(MuzikaGromche)} looping? {(LoopLoopingState.IsLooping ? 'X' : '_')}{(WindUpLoopingState.IsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} Time={Time.realtimeSinceStartup:N4} events={string.Join(",", events)}"); |                     Debug.Log($"{nameof(MuzikaGromche)} looping? {(LoopLoopingState.IsLooping ? 'X' : '_')}{(WindUpLoopingState.IsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} Time={Time.realtimeSinceStartup:N4} events={string.Join(",", events)}"); | ||||||
| #endif | #endif | ||||||
|  | @ -1473,13 +1590,13 @@ namespace MuzikaGromche | ||||||
|             return loopingState.Update(AudioState.Time, AudioState.IsExtrapolated, AdditionalOffset()); |             return loopingState.Update(AudioState.Time, AudioState.IsExtrapolated, AdditionalOffset()); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Timings that may be changes through config |         // Timings that may be changed through config | ||||||
|         private float AdditionalOffset() |         private float AdditionalOffset() | ||||||
|         { |         { | ||||||
|             return Config.AudioOffset.Value + track.BeatsOffsetInSeconds; |             return Config.AudioOffset.Value + track.BeatsOffsetInSeconds; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private List<BaseEvent> GetEvents(BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp) |         private List<BaseEvent> GetEvents(BeatTimestamp loopOffsetTimestamp, BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp) | ||||||
|         { |         { | ||||||
|             List<BaseEvent> events = []; |             List<BaseEvent> events = []; | ||||||
| 
 | 
 | ||||||
|  | @ -1500,7 +1617,6 @@ namespace MuzikaGromche | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // TODO: quick editor |             // TODO: quick editor | ||||||
|             // loopOffsetSpan.GetLastIndex(Config.LyricsTimeSeries) |  | ||||||
|             if (Config.DisplayLyrics.Value) |             if (Config.DisplayLyrics.Value) | ||||||
|             { |             { | ||||||
|                 var index = loopOffsetSpan.GetLastIndex(track.LyricsTimeSeries); |                 var index = loopOffsetSpan.GetLastIndex(track.LyricsTimeSeries); | ||||||
|  | @ -1517,6 +1633,16 @@ namespace MuzikaGromche | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             if (GetInterpolation(loopOffsetTimestamp, track.DrunknessLoopOffsetTimeSeries, Easing.Linear) is { } drunkness) | ||||||
|  |             { | ||||||
|  |                 events.Add(new DrunkEvent(drunkness)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (GetInterpolation(loopOffsetTimestamp, track.CondensationLoopOffsetTimeSeries, Easing.Linear) is { } condensation) | ||||||
|  |             { | ||||||
|  |                 events.Add(new CondensationEvent(condensation)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             return events; |             return events; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -1620,6 +1746,84 @@ namespace MuzikaGromche | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         private float? GetInterpolation(BeatTimestamp timestamp, TimeSeries<float> timeSeries, Easing easing) | ||||||
|  |         { | ||||||
|  |             if (timeSeries.Length == 0) | ||||||
|  |             { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |             else if (timeSeries.Length == 1) | ||||||
|  |             { | ||||||
|  |                 return timeSeries.Values[0]; | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 int? indexOfPrevious = null; | ||||||
|  |                 // Find index of the previous time. If looped, wrap backwards. In either case it is possibly missing. | ||||||
|  |                 for (int i = timeSeries.Length - 1; i >= 0; i--) | ||||||
|  |                 { | ||||||
|  |                     if (timeSeries.Beats[i] <= timestamp.Beat) | ||||||
|  |                     { | ||||||
|  |                         indexOfPrevious = i; | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 if (indexOfPrevious == null && timestamp.IsLooping) | ||||||
|  |                 { | ||||||
|  |                     indexOfPrevious = timeSeries.Length - 1; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Find index of the next time. If looped, wrap forward. | ||||||
|  |                 int? indexOfNext = null; | ||||||
|  |                 for (int i = 0; i < timeSeries.Length; i++) | ||||||
|  |                 { | ||||||
|  |                     if (timeSeries.Beats[i] >= timestamp.Beat) | ||||||
|  |                     { | ||||||
|  |                         indexOfNext = i; | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 if (indexOfNext == null && timestamp.IsLooping) | ||||||
|  |                 { | ||||||
|  |                     for (int i = 0; i < timeSeries.Length; i++) | ||||||
|  |                     { | ||||||
|  |                         if (timeSeries.Beats[i] >= 0f) | ||||||
|  |                         { | ||||||
|  |                             indexOfNext = i; | ||||||
|  |                             break; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 switch (indexOfPrevious, indexOfNext) | ||||||
|  |                 { | ||||||
|  |                     case (null, null): | ||||||
|  |                         return null; | ||||||
|  | 
 | ||||||
|  |                     case (null, { } index): | ||||||
|  |                         return timeSeries.Values[index]; | ||||||
|  | 
 | ||||||
|  |                     case ({ } index, null): | ||||||
|  |                         return timeSeries.Values[index]; | ||||||
|  | 
 | ||||||
|  |                     case ({ } prev, { } next) when prev == next || timeSeries.Beats[prev] == timeSeries.Beats[next]: | ||||||
|  |                         return timeSeries.Values[prev]; | ||||||
|  | 
 | ||||||
|  |                     case ({ } prev, { } next): | ||||||
|  |                         var prevBeat = timeSeries.Beats[prev]; | ||||||
|  |                         var nextBeat = timeSeries.Beats[next]; | ||||||
|  |                         var prevTimestamp = new BeatTimestamp(timestamp.LoopBeats, isLooping: false, prevBeat, false); | ||||||
|  |                         var nextTimestamp = new BeatTimestamp(timestamp.LoopBeats, isLooping: false, nextBeat, false); | ||||||
|  |                         var t = BeatTimeSpan.Between(prevTimestamp, timestamp).Duration(longest: true) | ||||||
|  |                             / BeatTimeSpan.Between(prevTimestamp, nextTimestamp).Duration(longest: true); | ||||||
|  |                         var prevVal = timeSeries.Values[prev]; | ||||||
|  |                         var nextVal = timeSeries.Values[next]; | ||||||
|  |                         var val = Mathf.Lerp(prevVal, nextVal, easing.Eval(t)); | ||||||
|  |                         return val; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     abstract class BaseEvent; |     abstract class BaseEvent; | ||||||
|  | @ -1695,6 +1899,20 @@ namespace MuzikaGromche | ||||||
|         public override string ToString() => "WindUp"; |         public override string ToString() => "WindUp"; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     abstract class HUDEvent : BaseEvent; | ||||||
|  | 
 | ||||||
|  |     class DrunkEvent(float drunkness) : HUDEvent | ||||||
|  |     { | ||||||
|  |         public readonly float Drunkness = drunkness; | ||||||
|  |         public override string ToString() => $"Drunk({Drunkness:N2})"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     class CondensationEvent(float condensation) : HUDEvent | ||||||
|  |     { | ||||||
|  |         public readonly float Condensation = condensation; | ||||||
|  |         public override string ToString() => $"Condensation({Condensation:N2})"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // 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. | ||||||
|     static class Mod |     static class Mod | ||||||
|  | @ -1829,6 +2047,8 @@ namespace MuzikaGromche | ||||||
|         private static string? ColorTransitionEasingOverride = null; |         private static string? ColorTransitionEasingOverride = null; | ||||||
|         private static float[]? FlickerLightsTimeSeriesOverride = null; |         private static float[]? FlickerLightsTimeSeriesOverride = null; | ||||||
|         private static float[]? LyricsTimeSeriesOverride = null; |         private static float[]? LyricsTimeSeriesOverride = null; | ||||||
|  |         private static TimeSeries<float>? DrunknessLoopOffsetTimeSeriesOverride = null; | ||||||
|  |         private static TimeSeries<float>? CondensationLoopOffsetTimeSeriesOverride = null; | ||||||
|         private static Palette? PaletteOverride = null; |         private static Palette? PaletteOverride = null; | ||||||
| 
 | 
 | ||||||
|         private class AudioTrackWithConfigOverride(IAudioTrack track) : ProxyAudioTrack(track), IAudioTrack |         private class AudioTrackWithConfigOverride(IAudioTrack track) : ProxyAudioTrack(track), IAudioTrack | ||||||
|  | @ -1853,6 +2073,9 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|             float[] IAudioTrack.LyricsTimeSeries => LyricsTimeSeriesOverride ?? Track.LyricsTimeSeries; |             float[] IAudioTrack.LyricsTimeSeries => LyricsTimeSeriesOverride ?? Track.LyricsTimeSeries; | ||||||
| 
 | 
 | ||||||
|  |             TimeSeries<float> IAudioTrack.DrunknessLoopOffsetTimeSeries => DrunknessLoopOffsetTimeSeriesOverride ?? Track.DrunknessLoopOffsetTimeSeries; | ||||||
|  |             TimeSeries<float> IAudioTrack.CondensationLoopOffsetTimeSeries => CondensationLoopOffsetTimeSeriesOverride ?? Track.CondensationLoopOffsetTimeSeries; | ||||||
|  | 
 | ||||||
|             Palette IAudioTrack.Palette => PaletteOverride ?? Track.Palette; |             Palette IAudioTrack.Palette => PaletteOverride ?? Track.Palette; | ||||||
|         } |         } | ||||||
| #endif | #endif | ||||||
|  | @ -1877,6 +2100,8 @@ namespace MuzikaGromche | ||||||
|             LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions()))); |             LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions()))); | ||||||
| 
 | 
 | ||||||
| #if DEBUG | #if DEBUG | ||||||
|  |             SetupEntriesForGameOverText(configFile); | ||||||
|  |             SetupEntriesForScreenFilters(configFile); | ||||||
|             SetupEntriesForExtrapolation(configFile); |             SetupEntriesForExtrapolation(configFile); | ||||||
|             SetupEntriesToSkipWinding(configFile); |             SetupEntriesToSkipWinding(configFile); | ||||||
|             SetupEntriesForPaletteOverride(configFile); |             SetupEntriesForPaletteOverride(configFile); | ||||||
|  | @ -2071,6 +2296,8 @@ namespace MuzikaGromche | ||||||
|             ConfigEntry<float> fadeOutDurationEntry = null!; |             ConfigEntry<float> fadeOutDurationEntry = null!; | ||||||
|             ConfigEntry<string> flickerLightsTimeSeriesEntry = null!; |             ConfigEntry<string> flickerLightsTimeSeriesEntry = null!; | ||||||
|             ConfigEntry<string> lyricsTimeSeriesEntry = null!; |             ConfigEntry<string> lyricsTimeSeriesEntry = null!; | ||||||
|  |             ConfigEntry<string> drunknessTimeSeriesEntry = null!; | ||||||
|  |             ConfigEntry<string> condensationTimeSeriesEntry = null!; | ||||||
|             ConfigEntry<float> beatsOffsetEntry = null!; |             ConfigEntry<float> beatsOffsetEntry = null!; | ||||||
|             ConfigEntry<float> colorTransitionInEntry = null!; |             ConfigEntry<float> colorTransitionInEntry = null!; | ||||||
|             ConfigEntry<float> colorTransitionOutEntry = null!; |             ConfigEntry<float> colorTransitionOutEntry = null!; | ||||||
|  | @ -2091,9 +2318,13 @@ namespace MuzikaGromche | ||||||
|             fadeOutDurationEntry = configFile.Bind(section, "Fade Out Duration", 0f, |             fadeOutDurationEntry = configFile.Bind(section, "Fade Out Duration", 0f, | ||||||
|                 new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 10))); |                 new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 10))); | ||||||
|             flickerLightsTimeSeriesEntry = configFile.Bind(section, "Flicker Lights Time Series", "", |             flickerLightsTimeSeriesEntry = configFile.Bind(section, "Flicker Lights Time Series", "", | ||||||
|                 new ConfigDescription("Time series of beat offsets when to flicker the lights.")); |                 new ConfigDescription("Time series of loop offset beats when to flicker the lights.")); | ||||||
|             lyricsTimeSeriesEntry = configFile.Bind(section, "Lyrics Time Series", "", |             lyricsTimeSeriesEntry = configFile.Bind(section, "Lyrics Time Series", "", | ||||||
|                 new ConfigDescription("Time series of beat offsets when to show lyrics lines.")); |                 new ConfigDescription("Time series of loop offset beats when to show lyrics lines.")); | ||||||
|  |             drunknessTimeSeriesEntry = configFile.Bind(section, "Drunkness", "", | ||||||
|  |                 new ConfigDescription("Time series of loop offset beats which are keyframes for the drunkness effect. Format: 'time1: value1, time2: value2")); | ||||||
|  |             condensationTimeSeriesEntry = configFile.Bind(section, "Helmet Condensation Drops", "", | ||||||
|  |                 new ConfigDescription("Time series of loop offset beats which are keyframes for the Helmet Condensation Drops effect. Format: 'time1: value1, time2: value2")); | ||||||
|             beatsOffsetEntry = configFile.Bind(section, "Beats Offset", 0f, |             beatsOffsetEntry = configFile.Bind(section, "Beats Offset", 0f, | ||||||
|                 new ConfigDescription("How much to offset the whole beat. More is later", new AcceptableValueRange<float>(-0.5f, 0.5f))); |                 new ConfigDescription("How much to offset the whole beat. More is later", new AcceptableValueRange<float>(-0.5f, 0.5f))); | ||||||
|             colorTransitionInEntry = configFile.Bind(section, "Color Transition In", 0.25f, |             colorTransitionInEntry = configFile.Bind(section, "Color Transition In", 0.25f, | ||||||
|  | @ -2108,6 +2339,8 @@ namespace MuzikaGromche | ||||||
|             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutDurationEntry, floatSliderOptions)); |             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutDurationEntry, floatSliderOptions)); | ||||||
|             LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(flickerLightsTimeSeriesEntry, Default(new TextInputFieldOptions()))); |             LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(flickerLightsTimeSeriesEntry, Default(new TextInputFieldOptions()))); | ||||||
|             LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(lyricsTimeSeriesEntry, Default(new TextInputFieldOptions()))); |             LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(lyricsTimeSeriesEntry, Default(new TextInputFieldOptions()))); | ||||||
|  |             LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(drunknessTimeSeriesEntry, Default(new TextInputFieldOptions()))); | ||||||
|  |             LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(condensationTimeSeriesEntry, Default(new TextInputFieldOptions()))); | ||||||
|             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(beatsOffsetEntry, floatSliderOptions)); |             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(beatsOffsetEntry, floatSliderOptions)); | ||||||
|             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionInEntry, floatSliderOptions)); |             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionInEntry, floatSliderOptions)); | ||||||
|             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutEntry, floatSliderOptions)); |             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutEntry, floatSliderOptions)); | ||||||
|  | @ -2117,6 +2350,8 @@ namespace MuzikaGromche | ||||||
|             registerStruct(fadeOutDurationEntry, t => t.FadeOutDuration, x => FadeOutDurationOverride = x); |             registerStruct(fadeOutDurationEntry, t => t.FadeOutDuration, x => FadeOutDurationOverride = x); | ||||||
|             registerArray(flickerLightsTimeSeriesEntry, t => t.FlickerLightsTimeSeries, xs => FlickerLightsTimeSeriesOverride = xs, float.Parse, sort: true); |             registerArray(flickerLightsTimeSeriesEntry, t => t.FlickerLightsTimeSeries, xs => FlickerLightsTimeSeriesOverride = xs, float.Parse, sort: true); | ||||||
|             registerArray(lyricsTimeSeriesEntry, t => t.LyricsTimeSeries, xs => LyricsTimeSeriesOverride = xs, float.Parse, sort: true); |             registerArray(lyricsTimeSeriesEntry, t => t.LyricsTimeSeries, xs => LyricsTimeSeriesOverride = xs, float.Parse, sort: true); | ||||||
|  |             registerTimeSeries(drunknessTimeSeriesEntry, t => t.DrunknessLoopOffsetTimeSeries, xs => DrunknessLoopOffsetTimeSeriesOverride = xs, float.Parse, f => f.ToString()); | ||||||
|  |             registerTimeSeries(condensationTimeSeriesEntry, t => t.CondensationLoopOffsetTimeSeries, xs => CondensationLoopOffsetTimeSeriesOverride = xs, float.Parse, f => f.ToString()); | ||||||
|             registerStruct(beatsOffsetEntry, t => t.BeatsOffset, x => BeatsOffsetOverride = x); |             registerStruct(beatsOffsetEntry, t => t.BeatsOffset, x => BeatsOffsetOverride = x); | ||||||
|             registerStruct(colorTransitionInEntry, t => t.ColorTransitionIn, x => ColorTransitionInOverride = x); |             registerStruct(colorTransitionInEntry, t => t.ColorTransitionIn, x => ColorTransitionInOverride = x); | ||||||
|             registerStruct(colorTransitionOutEntry, t => t.ColorTransitionOut, x => ColorTransitionOutOverride = x); |             registerStruct(colorTransitionOutEntry, t => t.ColorTransitionOut, x => ColorTransitionOutOverride = x); | ||||||
|  | @ -2150,7 +2385,76 @@ namespace MuzikaGromche | ||||||
|                         } |                         } | ||||||
|                         setter.Invoke(overrideTimingsEntry.Value ? values : null); |                         setter.Invoke(overrideTimingsEntry.Value ? values : null); | ||||||
|                     }); |                     }); | ||||||
|  |             void registerTimeSeries<T>(ConfigEntry<string> entry, Func<IAudioTrack, TimeSeries<T>> getter, Action<TimeSeries<T>?> setter, Func<string, T> parser, Func<T, string> formatter) => | ||||||
|  |                 register(entry, | ||||||
|  |                     (track) => | ||||||
|  |                     { | ||||||
|  |                         var ts = getter(track); | ||||||
|  |                         return formatTimeSeries(ts, formatter); | ||||||
|  |                     }, | ||||||
|  |                     () => | ||||||
|  |                     { | ||||||
|  |                         var ts = parseTimeSeries(entry.Value, parser); | ||||||
|  |                         if (ts is { } timeSeries) | ||||||
|  |                         { | ||||||
|  |                             entry.Value = formatTimeSeries(timeSeries, formatter); | ||||||
|  |                         } | ||||||
|  |                         setter.Invoke(overrideTimingsEntry.Value ? ts : null); | ||||||
|  |                     }); | ||||||
| 
 | 
 | ||||||
|  |             // current restriction is that formatted value can not contain commas or semicolons. | ||||||
|  |             TimeSeries<T>? parseTimeSeries<T>(string str, Func<string, T> parser) | ||||||
|  |             { | ||||||
|  |                 try | ||||||
|  |                 { | ||||||
|  |                     if (string.IsNullOrWhiteSpace(str)) | ||||||
|  |                     { | ||||||
|  |                         return null; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     List<float> beats = []; | ||||||
|  |                     List<T> values = []; | ||||||
|  |                     foreach (var pair in str.Split(",")) | ||||||
|  |                     { | ||||||
|  |                         if (string.IsNullOrWhiteSpace(pair)) | ||||||
|  |                         { | ||||||
|  |                             continue; | ||||||
|  |                         } | ||||||
|  |                         var keyvalue = pair.Split(":"); | ||||||
|  |                         if (keyvalue.Length != 2) | ||||||
|  |                         { | ||||||
|  |                             throw new FormatException($"Pair must be separated by exactly one semicolon: '{pair}'"); | ||||||
|  |                         } | ||||||
|  |                         var beat = float.Parse(keyvalue[0].Trim()); | ||||||
|  |                         var val = parser(keyvalue[1].Trim()); | ||||||
|  |                         beats.Add(beat); | ||||||
|  |                         values.Add(val); | ||||||
|  |                     } | ||||||
|  |                     var ts = new TimeSeries<T>(beats.ToArray(), values.ToArray()); | ||||||
|  |                     return ts; | ||||||
|  |                 } | ||||||
|  |                 catch (Exception e) | ||||||
|  |                 { | ||||||
|  |                     Debug.Log($"{nameof(MuzikaGromche)} Unable to parse time series: {e}"); | ||||||
|  |                     return null; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             string formatTimeSeries<T>(TimeSeries<T> ts, Func<T, string> formatter) | ||||||
|  |             { | ||||||
|  |                 StringBuilder strings = new(); | ||||||
|  |                 for (int i = 0; i < ts.Length; i++) | ||||||
|  |                 { | ||||||
|  |                     var beat = ts.Beats[i]; | ||||||
|  |                     var value = formatter(ts.Values[i]); | ||||||
|  |                     strings.Append($"{beat}: {value}"); | ||||||
|  |                     if (i != ts.Length - 1) | ||||||
|  |                     { | ||||||
|  |                         strings.Append(", "); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 Debug.Log($"{nameof(MuzikaGromche)} format time series {ts} {strings}"); | ||||||
|  |                 return strings.ToString(); | ||||||
|  |             } | ||||||
|             T[]? parseStringArray<T>(string str, Func<string, T> parser, bool sort = false) where T : struct |             T[]? parseStringArray<T>(string str, Func<string, T> parser, bool sort = false) where T : struct | ||||||
|             { |             { | ||||||
|                 try |                 try | ||||||
|  | @ -2182,6 +2486,59 @@ namespace MuzikaGromche | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         private void SetupEntriesForGameOverText(ConfigFile configFile) | ||||||
|  |         { | ||||||
|  |             const string section = "Game Over"; | ||||||
|  |             var gameOverTextConfigEntry = configFile.Bind(section, "Game Over Text", DeathScreenGameOverTextManager.GameOverTextModdedDefault, | ||||||
|  |                 new ConfigDescription("Custom Game Over text to show.")); | ||||||
|  |             LethalConfigManager.AddConfigItem(new GenericButtonConfigItem(section, "Game Over Animation", | ||||||
|  |                 "Run Death Screen / Game Over animation 3 times.", "Trigger", () => | ||||||
|  |                 { | ||||||
|  |                     HUDManager.Instance.StartCoroutine(AnimateGameOverText(gameOverTextConfigEntry.Value)); | ||||||
|  |                 })); | ||||||
|  |             LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(gameOverTextConfigEntry, requiresRestart: false)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         static IEnumerator AnimateGameOverText(string text) | ||||||
|  |         { | ||||||
|  |             yield return new WaitForSeconds(1f); | ||||||
|  |             for (int i = 0; i < 3; i++) | ||||||
|  |             { | ||||||
|  |                 DeathScreenGameOverTextManager.SetText(text); | ||||||
|  |                 HUDManager.Instance.gameOverAnimator.SetTrigger("gameOver"); | ||||||
|  |                 yield return new WaitForSeconds(5f); | ||||||
|  |                 HUDManager.Instance.gameOverAnimator.SetTrigger("revive"); | ||||||
|  |                 yield return new WaitForSeconds(1f); | ||||||
|  |             } | ||||||
|  |             DeathScreenGameOverTextManager.Clear(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private void SetupEntriesForScreenFilters(ConfigFile configFile) | ||||||
|  |         { | ||||||
|  |             const string section = "Screen Filters"; | ||||||
|  | 
 | ||||||
|  |             var drunkConfigEntry = configFile.Bind(section, "Drunkness Level", 0f, | ||||||
|  |                 new ConfigDescription("Override drunkness level in Screen Filters Manager.")); | ||||||
|  |             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(drunkConfigEntry, requiresRestart: false)); | ||||||
|  |             drunkConfigEntry.SettingChanged += (sender, args) => | ||||||
|  |             { | ||||||
|  |                 ScreenFiltersManager.Drunkness = drunkConfigEntry.Value; | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             var condensationConfigEntry = configFile.Bind(section, "Condensation Level", 0f, | ||||||
|  |                 new ConfigDescription("Override drunkness level in Screen Filters Manager.")); | ||||||
|  |             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(condensationConfigEntry, new FloatSliderOptions() | ||||||
|  |             { | ||||||
|  |                 Min = 0f, | ||||||
|  |                 Max = 0.27f, | ||||||
|  |                 RequiresRestart = false, | ||||||
|  |             })); | ||||||
|  |             condensationConfigEntry.SettingChanged += (sender, args) => | ||||||
|  |             { | ||||||
|  |                 ScreenFiltersManager.HelmetCondensationDrops = condensationConfigEntry.Value; | ||||||
|  |             }; | ||||||
|  |         } | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|         private T Default<T>(T options) where T : BaseOptions |         private T Default<T>(T options) where T : BaseOptions | ||||||
|  | @ -2335,6 +2692,16 @@ namespace MuzikaGromche | ||||||
|             LoopAudioSource.PlayScheduled(loopStartDspTime); |             LoopAudioSource.PlayScheduled(loopStartDspTime); | ||||||
|             Debug.Log($"{nameof(MuzikaGromche)} Play Intro: dspTime={AudioSettings.dspTime:N4}, intro.time={IntroAudioSource.time:N4}/{IntroAudioSource.clip.length:N4}, scheduled Loop={loopStartDspTime}"); |             Debug.Log($"{nameof(MuzikaGromche)} Play Intro: dspTime={AudioSettings.dspTime:N4}, intro.time={IntroAudioSource.time:N4}/{IntroAudioSource.clip.length:N4}, scheduled Loop={loopStartDspTime}"); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         public void OverrideDeathScreenGameOverText() | ||||||
|  |         { | ||||||
|  |             if (CurrentTrack == null) | ||||||
|  |             { | ||||||
|  |                 // Playing as a client with a host who doesn't have the mod | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             StartCoroutine(DeathScreenGameOverTextManager.SetTextAndClear(CurrentTrack.GameOverText)); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HarmonyPatch(typeof(JesterAI))] |     [HarmonyPatch(typeof(JesterAI))] | ||||||
|  | @ -2449,6 +2816,7 @@ namespace MuzikaGromche | ||||||
|             { |             { | ||||||
|                 PoweredLightsBehaviour.Instance.ResetLightColor(); |                 PoweredLightsBehaviour.Instance.ResetLightColor(); | ||||||
|                 DiscoBallManager.Disable(); |                 DiscoBallManager.Disable(); | ||||||
|  |                 ScreenFiltersManager.Clear(); | ||||||
|                 // Rotate track groups |                 // Rotate track groups | ||||||
|                 behaviour.ChooseTrackServerRpc(); |                 behaviour.ChooseTrackServerRpc(); | ||||||
|                 behaviour.BeatTimeState = null; |                 behaviour.BeatTimeState = null; | ||||||
|  | @ -2458,6 +2826,7 @@ namespace MuzikaGromche | ||||||
|             else if ((__instance.previousState == 1 || __instance.previousState == 2) && behaviour.BeatTimeState is { } beatTimeState) |             else if ((__instance.previousState == 1 || __instance.previousState == 2) && behaviour.BeatTimeState is { } beatTimeState) | ||||||
|             { |             { | ||||||
|                 var events = beatTimeState.Update(introAudioSource, loopAudioSource); |                 var events = beatTimeState.Update(introAudioSource, loopAudioSource); | ||||||
|  |                 var localPlayerCanHearMusic = Plugin.LocalPlayerCanHearMusic(__instance); | ||||||
|                 foreach (var ev in events) |                 foreach (var ev in events) | ||||||
|                 { |                 { | ||||||
|                     switch (ev) |                     switch (ev) | ||||||
|  | @ -2474,16 +2843,33 @@ namespace MuzikaGromche | ||||||
|                             RoundManager.Instance.FlickerLights(true); |                             RoundManager.Instance.FlickerLights(true); | ||||||
|                             break; |                             break; | ||||||
| 
 | 
 | ||||||
|                         case LyricsEvent e: |                         case LyricsEvent e when localPlayerCanHearMusic: | ||||||
|                             if (Plugin.LocalPlayerCanHearMusic(__instance)) |  | ||||||
|                             { |  | ||||||
|                             Plugin.DisplayLyrics(e.Text); |                             Plugin.DisplayLyrics(e.Text); | ||||||
|                             } |                             break; | ||||||
|  | 
 | ||||||
|  |                         case DrunkEvent e when localPlayerCanHearMusic: | ||||||
|  |                             ScreenFiltersManager.Drunkness = e.Drunkness; | ||||||
|  |                             break; | ||||||
|  | 
 | ||||||
|  |                         case CondensationEvent e when localPlayerCanHearMusic: | ||||||
|  |                             ScreenFiltersManager.HelmetCondensationDrops = e.Condensation; | ||||||
|                             break; |                             break; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         [HarmonyPatch(nameof(JesterAI.killPlayerAnimation))] | ||||||
|  |         [HarmonyPrefix] | ||||||
|  |         static void JesterKillPlayerAnimationPrefix(JesterAI __instance, int playerId) | ||||||
|  |         { | ||||||
|  |             // Note on cast to int: base game already downcasts ulong to int anyway | ||||||
|  |             if (playerId == (int)GameNetworkManager.Instance.localPlayerController.playerClientId) | ||||||
|  |             { | ||||||
|  |                 var behaviour = __instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>(); | ||||||
|  |                 behaviour.OverrideDeathScreenGameOverText(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HarmonyPatch(typeof(EnemyAI))] |     [HarmonyPatch(typeof(EnemyAI))] | ||||||
|  | @ -2499,8 +2885,10 @@ namespace MuzikaGromche | ||||||
|             { |             { | ||||||
|                 PoweredLightsBehaviour.Instance.ResetLightColor(); |                 PoweredLightsBehaviour.Instance.ResetLightColor(); | ||||||
|                 DiscoBallManager.Disable(); |                 DiscoBallManager.Disable(); | ||||||
|  |                 DeathScreenGameOverTextManager.Clear(); | ||||||
|  |                 ScreenFiltersManager.Clear(); | ||||||
|                 // Just in case if players have spawned multiple Jesters, |                 // Just in case if players have spawned multiple Jesters, | ||||||
|                 // Don't reset Plugin.CurrentTrack to null, |                 // Don't reset Config.CurrentTrack to null, | ||||||
|                 // so that the latest chosen track remains set. |                 // so that the latest chosen track remains set. | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,146 @@ | ||||||
|  | using System.Collections; | ||||||
|  | using HarmonyLib; | ||||||
|  | using UnityEngine; | ||||||
|  | 
 | ||||||
|  | namespace MuzikaGromche | ||||||
|  | { | ||||||
|  |     static class ScreenFiltersManager | ||||||
|  |     { | ||||||
|  |         private const float VibilityThreshold = 0.01f; | ||||||
|  | 
 | ||||||
|  |         private static bool drunknessChangedThisFrame = false; | ||||||
|  |         private static float drunkness = 0f; | ||||||
|  |         public static float Drunkness | ||||||
|  |         { | ||||||
|  |             get => drunkness; | ||||||
|  |             set | ||||||
|  |             { | ||||||
|  |                 drunkness = value; | ||||||
|  |                 drunknessChangedThisFrame = true; | ||||||
|  |                 ScheduleUpdate(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static bool helmetCondensationDropsChangedThisFrame = false; | ||||||
|  |         private static float helmetCondensationDrops = 0f; | ||||||
|  |         public static float HelmetCondensationDrops | ||||||
|  |         { | ||||||
|  |             get => helmetCondensationDrops; | ||||||
|  |             set | ||||||
|  |             { | ||||||
|  |                 helmetCondensationDrops = value; | ||||||
|  |                 helmetCondensationDropsChangedThisFrame = true; | ||||||
|  |                 ScheduleUpdate(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static Coroutine? scheduledUpdate = null; | ||||||
|  | 
 | ||||||
|  |         private static void ScheduleUpdate() | ||||||
|  |         { | ||||||
|  |             CancelScheduledUpdate(); | ||||||
|  |             scheduledUpdate = HUDManager.Instance.StartCoroutine(ScheduledUpdate()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static void CancelScheduledUpdate() | ||||||
|  |         { | ||||||
|  |             if (scheduledUpdate != null) | ||||||
|  |             { | ||||||
|  |                 HUDManager.Instance.StopCoroutine(scheduledUpdate); | ||||||
|  |                 scheduledUpdate = null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static IEnumerator ScheduledUpdate() | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 yield return new WaitForEndOfFrame(); | ||||||
|  |                 Update(); | ||||||
|  |             } | ||||||
|  |             finally | ||||||
|  |             { | ||||||
|  |                 scheduledUpdate = null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static void Update() | ||||||
|  |         { | ||||||
|  |             CancelScheduledUpdate(); | ||||||
|  |             var hud = HUDManager.Instance; | ||||||
|  | 
 | ||||||
|  |             if (!drunknessChangedThisFrame) | ||||||
|  |             { | ||||||
|  |                 // animated roll-off | ||||||
|  |                 drunkness = Mathf.Clamp(drunkness - Time.deltaTime / 2f, 0f, 1f); | ||||||
|  |             } | ||||||
|  |             drunknessChangedThisFrame = false; | ||||||
|  |             if (drunkness > VibilityThreshold) | ||||||
|  |             { | ||||||
|  |                 var moddedDrunknessFilterWeight = StartOfRound.Instance.drunknessSideEffect.Evaluate(drunkness); | ||||||
|  |                 var moddedGasImageAlphaAlpha = moddedDrunknessFilterWeight * 1.5f; | ||||||
|  |                 // set the final value to the greatest of the two, so that we don't accidentally undo TZP's visual effect. | ||||||
|  |                 hud.drunknessFilter.weight = Mathf.Max(hud.drunknessFilter.weight, moddedDrunknessFilterWeight); | ||||||
|  |                 hud.gasImageAlpha.alpha = Mathf.Max(hud.gasImageAlpha.alpha, moddedGasImageAlphaAlpha); | ||||||
|  |                 // Image alpha only makes sense if the animator is running | ||||||
|  |                 hud.gasHelmetAnimator.SetBool("gasEmitting", value: true); | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 ClearDrunkness(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!helmetCondensationDropsChangedThisFrame) | ||||||
|  |             { | ||||||
|  |                 // animated roll-off | ||||||
|  |                 helmetCondensationDrops = Mathf.Clamp(helmetCondensationDrops - Time.deltaTime / 6f, 0f, 1f); | ||||||
|  |             } | ||||||
|  |             helmetCondensationDropsChangedThisFrame = false; | ||||||
|  |             if (helmetCondensationDrops > VibilityThreshold) | ||||||
|  |             { | ||||||
|  |                 // HelmetCondensationDrops | ||||||
|  |                 Color color = hud.helmetCondensationMaterial.color; | ||||||
|  |                 // set the final value to the greatest of the two, so that we don't accidentally undo steam's visual effect. | ||||||
|  |                 color.a = Mathf.Clamp(Mathf.Max(color.a, helmetCondensationDrops), 0f, 0.27f); | ||||||
|  |                 hud.helmetCondensationMaterial.color = color; | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 ClearCondensation(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public static void Clear() | ||||||
|  |         { | ||||||
|  |             ClearDrunkness(); | ||||||
|  |             ClearCondensation(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static void ClearDrunkness() | ||||||
|  |         { | ||||||
|  |             drunkness = 0f; | ||||||
|  |             // Only the stop animation if vanilla doesn't animate TZP right now. | ||||||
|  |             if (GameNetworkManager.Instance.localPlayerController.drunkness == 0f) | ||||||
|  |             { | ||||||
|  |                 HUDManager.Instance.gasHelmetAnimator.SetBool("gasEmitting", value: false); | ||||||
|  |             } | ||||||
|  |             // Vanilla will set drunknessFilter.weight and gasImageAlpha.alpha on the next Update anyway. | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private static void ClearCondensation() | ||||||
|  |         { | ||||||
|  |             helmetCondensationDrops = 0f; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         [HarmonyPatch(typeof(HUDManager))] | ||||||
|  |         internal static class HUDManagerScreenFiltersPatch | ||||||
|  |         { | ||||||
|  |             [HarmonyPatch(nameof(HUDManager.SetScreenFilters))] | ||||||
|  |             [HarmonyPostfix] | ||||||
|  |             static void SetScreenFiltersPostfix(HUDManager __instance) | ||||||
|  |             { | ||||||
|  |                 Update(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|     "name": "MuzikaGromche", |     "name": "MuzikaGromche", | ||||||
|     "version_number": "1337.420.9003", |     "version_number": "1337.420.9004", | ||||||
|     "author": "Ratijas", |     "author": "Ratijas", | ||||||
|     "description": "Add some content to your inverse teleporter experience on Titan!", |     "description": "Add some content to your inverse teleporter experience on Titan!", | ||||||
|     "website_url": "https://git.vilunov.me/ratijas/muzika-gromche", |     "website_url": "https://git.vilunov.me/ratijas/muzika-gromche", | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue