Added new track AttentionPls, implement HUD effects as a time series / timeline
This commit is contained in:
		
							parent
							
								
									e67c72951e
								
							
						
					
					
						commit
						aea755361b
					
				
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -3,6 +3,7 @@ | |||
| ## MuzikaGromche 1337.420.9004 | ||||
| 
 | ||||
| - 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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -58,6 +58,9 @@ | |||
|         <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 Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'"> | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ using System.Net.NetworkInformation; | |||
| using System.Net.Sockets; | ||||
| using System.Reflection; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using Unity.Netcode; | ||||
| using UnityEngine; | ||||
| using UnityEngine.Networking; | ||||
|  | @ -56,6 +57,9 @@ namespace MuzikaGromche | |||
|                 ColorTransitionOut = 0.25f, | ||||
|                 ColorTransitionEasing = Easing.OutExpo, | ||||
|                 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"]), | ||||
|                 Lyrics = [ | ||||
|                     (-68, "Devchata pljashut pod spidami"), | ||||
|  | @ -132,6 +136,9 @@ namespace MuzikaGromche | |||
|                 ColorTransitionOut = 0.25f, | ||||
|                 ColorTransitionEasing = Easing.OutExpo, | ||||
|                 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"]), | ||||
|                 Lyrics = [ | ||||
|                     (-111, "Deploy Destroy, porjadok eto otstoj"), | ||||
|  | @ -583,6 +590,57 @@ namespace MuzikaGromche | |||
|                 FlickerLightsTimeSeries = [-68.5f, -16.5f, 30.5f], | ||||
|                 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() | ||||
|  | @ -686,6 +744,7 @@ namespace MuzikaGromche | |||
|                 harmony.PatchAll(typeof(DiscoBallDespawnPatch)); | ||||
|                 harmony.PatchAll(typeof(SpawnRatePatch)); | ||||
|                 harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch)); | ||||
|                 harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch)); | ||||
|                 NetcodePatcher(); | ||||
|                 Compatibility.Register(this); | ||||
|             } | ||||
|  | @ -795,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 | ||||
|     // can be selected using weighted random from a list of selectable tracks. | ||||
|     public interface ISelectableTrack | ||||
|  | @ -887,6 +975,9 @@ namespace MuzikaGromche | |||
|         // If the chosen alternative is an empty string, lyrics event shall be skipped. | ||||
|         public string[] LyricsLines { get; } | ||||
| 
 | ||||
|         public TimeSeries<float> DrunknessLoopOffsetTimeSeries { get; } | ||||
|         public TimeSeries<float> CondensationLoopOffsetTimeSeries { get; } | ||||
| 
 | ||||
|         public Palette Palette { get; } | ||||
| 
 | ||||
|         public string? GameOverText { get => null; } | ||||
|  | @ -914,6 +1005,8 @@ namespace MuzikaGromche | |||
|         float[] IAudioTrack.FlickerLightsTimeSeries => Track.FlickerLightsTimeSeries; | ||||
|         float[] IAudioTrack.LyricsTimeSeries => Track.LyricsTimeSeries; | ||||
|         string[] IAudioTrack.LyricsLines => Track.LyricsLines; | ||||
|         TimeSeries<float> IAudioTrack.DrunknessLoopOffsetTimeSeries => Track.DrunknessLoopOffsetTimeSeries; | ||||
|         TimeSeries<float> IAudioTrack.CondensationLoopOffsetTimeSeries => Track.CondensationLoopOffsetTimeSeries; | ||||
|         Palette IAudioTrack.Palette => Track.Palette; | ||||
|         string? IAudioTrack.GameOverText => Track.GameOverText; | ||||
|     } | ||||
|  | @ -990,6 +1083,9 @@ namespace MuzikaGromche | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public TimeSeries<float> DrunknessLoopOffsetTimeSeries { get; init; } = new(); | ||||
|         public TimeSeries<float> CondensationLoopOffsetTimeSeries { get; init; } = new(); | ||||
| 
 | ||||
|         public Palette Palette { get; set; } = Palette.DEFAULT; | ||||
| 
 | ||||
|         public string? GameOverText { get; init; } = null; | ||||
|  | @ -1198,9 +1294,19 @@ namespace MuzikaGromche | |||
|             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; | ||||
|             } | ||||
|  | @ -1455,8 +1561,8 @@ namespace MuzikaGromche | |||
| 
 | ||||
|             if (AudioState.HasStarted) | ||||
|             { | ||||
|                 var loopTimestamp = Update(LoopLoopingState); | ||||
|                 var loopOffsetSpan = BeatTimeSpan.Between(LastKnownLoopOffsetBeat, loopTimestamp); | ||||
|                 var loopOffsetTimestamp = Update(LoopLoopingState); | ||||
|                 var loopOffsetSpan = BeatTimeSpan.Between(LastKnownLoopOffsetBeat, loopOffsetTimestamp); | ||||
| 
 | ||||
|                 // Do not go back in time | ||||
|                 if (!loopOffsetSpan.IsEmpty()) | ||||
|  | @ -1467,8 +1573,8 @@ namespace MuzikaGromche | |||
|                     } | ||||
| 
 | ||||
|                     var windUpOffsetTimestamp = Update(WindUpLoopingState); | ||||
|                     LastKnownLoopOffsetBeat = loopTimestamp.Beat; | ||||
|                     var events = GetEvents(loopOffsetSpan, windUpOffsetTimestamp); | ||||
|                     LastKnownLoopOffsetBeat = loopOffsetTimestamp.Beat; | ||||
|                     var events = GetEvents(loopOffsetTimestamp, loopOffsetSpan, windUpOffsetTimestamp); | ||||
| #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)}"); | ||||
| #endif | ||||
|  | @ -1484,13 +1590,13 @@ namespace MuzikaGromche | |||
|             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() | ||||
|         { | ||||
|             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 = []; | ||||
| 
 | ||||
|  | @ -1511,7 +1617,6 @@ namespace MuzikaGromche | |||
|             } | ||||
| 
 | ||||
|             // TODO: quick editor | ||||
|             // loopOffsetSpan.GetLastIndex(Config.LyricsTimeSeries) | ||||
|             if (Config.DisplayLyrics.Value) | ||||
|             { | ||||
|                 var index = loopOffsetSpan.GetLastIndex(track.LyricsTimeSeries); | ||||
|  | @ -1528,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; | ||||
|         } | ||||
| 
 | ||||
|  | @ -1631,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; | ||||
|  | @ -1706,6 +1899,20 @@ namespace MuzikaGromche | |||
|         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 | ||||
|     // which is unsuitable as an index for an array. | ||||
|     static class Mod | ||||
|  | @ -1840,6 +2047,8 @@ namespace MuzikaGromche | |||
|         private static string? ColorTransitionEasingOverride = null; | ||||
|         private static float[]? FlickerLightsTimeSeriesOverride = null; | ||||
|         private static float[]? LyricsTimeSeriesOverride = null; | ||||
|         private static TimeSeries<float>? DrunknessLoopOffsetTimeSeriesOverride = null; | ||||
|         private static TimeSeries<float>? CondensationLoopOffsetTimeSeriesOverride = null; | ||||
|         private static Palette? PaletteOverride = null; | ||||
| 
 | ||||
|         private class AudioTrackWithConfigOverride(IAudioTrack track) : ProxyAudioTrack(track), IAudioTrack | ||||
|  | @ -1864,6 +2073,9 @@ namespace MuzikaGromche | |||
| 
 | ||||
|             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; | ||||
|         } | ||||
| #endif | ||||
|  | @ -1888,11 +2100,12 @@ namespace MuzikaGromche | |||
|             LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions()))); | ||||
| 
 | ||||
| #if DEBUG | ||||
|             SetupEntriesForGameOverText(configFile); | ||||
|             SetupEntriesForScreenFilters(configFile); | ||||
|             SetupEntriesForExtrapolation(configFile); | ||||
|             SetupEntriesToSkipWinding(configFile); | ||||
|             SetupEntriesForPaletteOverride(configFile); | ||||
|             SetupEntriesForTimingsOverride(configFile); | ||||
|             SetupEntriesForGameOverText(configFile); | ||||
| #endif | ||||
| 
 | ||||
|             var chanceRange = new AcceptableValueRange<int>(0, 100); | ||||
|  | @ -2083,6 +2296,8 @@ namespace MuzikaGromche | |||
|             ConfigEntry<float> fadeOutDurationEntry = null!; | ||||
|             ConfigEntry<string> flickerLightsTimeSeriesEntry = null!; | ||||
|             ConfigEntry<string> lyricsTimeSeriesEntry = null!; | ||||
|             ConfigEntry<string> drunknessTimeSeriesEntry = null!; | ||||
|             ConfigEntry<string> condensationTimeSeriesEntry = null!; | ||||
|             ConfigEntry<float> beatsOffsetEntry = null!; | ||||
|             ConfigEntry<float> colorTransitionInEntry = null!; | ||||
|             ConfigEntry<float> colorTransitionOutEntry = null!; | ||||
|  | @ -2103,9 +2318,13 @@ namespace MuzikaGromche | |||
|             fadeOutDurationEntry = configFile.Bind(section, "Fade Out Duration", 0f, | ||||
|                 new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 10))); | ||||
|             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", "", | ||||
|                 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, | ||||
|                 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, | ||||
|  | @ -2120,6 +2339,8 @@ namespace MuzikaGromche | |||
|             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutDurationEntry, floatSliderOptions)); | ||||
|             LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(flickerLightsTimeSeriesEntry, 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(colorTransitionInEntry, floatSliderOptions)); | ||||
|             LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutEntry, floatSliderOptions)); | ||||
|  | @ -2129,6 +2350,8 @@ namespace MuzikaGromche | |||
|             registerStruct(fadeOutDurationEntry, t => t.FadeOutDuration, x => FadeOutDurationOverride = x); | ||||
|             registerArray(flickerLightsTimeSeriesEntry, t => t.FlickerLightsTimeSeries, xs => FlickerLightsTimeSeriesOverride = 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(colorTransitionInEntry, t => t.ColorTransitionIn, x => ColorTransitionInOverride = x); | ||||
|             registerStruct(colorTransitionOutEntry, t => t.ColorTransitionOut, x => ColorTransitionOutOverride = x); | ||||
|  | @ -2162,7 +2385,76 @@ namespace MuzikaGromche | |||
|                         } | ||||
|                         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 | ||||
|             { | ||||
|                 try | ||||
|  | @ -2221,6 +2513,32 @@ namespace MuzikaGromche | |||
|             } | ||||
|             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 | ||||
| 
 | ||||
|         private T Default<T>(T options) where T : BaseOptions | ||||
|  | @ -2498,6 +2816,7 @@ namespace MuzikaGromche | |||
|             { | ||||
|                 PoweredLightsBehaviour.Instance.ResetLightColor(); | ||||
|                 DiscoBallManager.Disable(); | ||||
|                 ScreenFiltersManager.Clear(); | ||||
|                 // Rotate track groups | ||||
|                 behaviour.ChooseTrackServerRpc(); | ||||
|                 behaviour.BeatTimeState = null; | ||||
|  | @ -2507,6 +2826,7 @@ namespace MuzikaGromche | |||
|             else if ((__instance.previousState == 1 || __instance.previousState == 2) && behaviour.BeatTimeState is { } beatTimeState) | ||||
|             { | ||||
|                 var events = beatTimeState.Update(introAudioSource, loopAudioSource); | ||||
|                 var localPlayerCanHearMusic = Plugin.LocalPlayerCanHearMusic(__instance); | ||||
|                 foreach (var ev in events) | ||||
|                 { | ||||
|                     switch (ev) | ||||
|  | @ -2523,11 +2843,16 @@ namespace MuzikaGromche | |||
|                             RoundManager.Instance.FlickerLights(true); | ||||
|                             break; | ||||
| 
 | ||||
|                         case LyricsEvent e: | ||||
|                             if (Plugin.LocalPlayerCanHearMusic(__instance)) | ||||
|                             { | ||||
|                                 Plugin.DisplayLyrics(e.Text); | ||||
|                             } | ||||
|                         case LyricsEvent e when localPlayerCanHearMusic: | ||||
|                             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; | ||||
|                     } | ||||
|                 } | ||||
|  | @ -2561,6 +2886,7 @@ namespace MuzikaGromche | |||
|                 PoweredLightsBehaviour.Instance.ResetLightColor(); | ||||
|                 DiscoBallManager.Disable(); | ||||
|                 DeathScreenGameOverTextManager.Clear(); | ||||
|                 ScreenFiltersManager.Clear(); | ||||
|                 // Just in case if players have spawned multiple Jesters, | ||||
|                 // Don't reset Config.CurrentTrack to null, | ||||
|                 // 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(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue