From aea755361bb922de22aad626b50b036b4cbfc407 Mon Sep 17 00:00:00 2001 From: ivan tkachenko Date: Fri, 26 Sep 2025 18:07:11 +0300 Subject: [PATCH] Added new track AttentionPls, implement HUD effects as a time series / timeline --- Assets/AttentionPls1Intro.ogg | 3 + Assets/AttentionPls2Intro.ogg | 3 + Assets/AttentionPlsLoop.ogg | 3 + CHANGELOG.md | 1 + MuzikaGromche/MuzikaGromche.csproj | 3 + MuzikaGromche/Plugin.cs | 360 ++++++++++++++++++++++++-- MuzikaGromche/ScreenFiltersManager.cs | 146 +++++++++++ 7 files changed, 502 insertions(+), 17 deletions(-) create mode 100644 Assets/AttentionPls1Intro.ogg create mode 100644 Assets/AttentionPls2Intro.ogg create mode 100644 Assets/AttentionPlsLoop.ogg create mode 100644 MuzikaGromche/ScreenFiltersManager.cs diff --git a/Assets/AttentionPls1Intro.ogg b/Assets/AttentionPls1Intro.ogg new file mode 100644 index 0000000..78542ad --- /dev/null +++ b/Assets/AttentionPls1Intro.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620dfa9dda5b6d3c7f7ef79b80065508b6d33e503ce01c83c4094ae73e894664 +size 679926 diff --git a/Assets/AttentionPls2Intro.ogg b/Assets/AttentionPls2Intro.ogg new file mode 100644 index 0000000..1fa916a --- /dev/null +++ b/Assets/AttentionPls2Intro.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3aefdd95fc2ac7efba0820f79b2c026998e525d0fec98e117d9bcaa3e50e40f4 +size 682839 diff --git a/Assets/AttentionPlsLoop.ogg b/Assets/AttentionPlsLoop.ogg new file mode 100644 index 0000000..590a254 --- /dev/null +++ b/Assets/AttentionPlsLoop.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d96d2f258196913a9afc3bcfeb65f69c95ac41d9ff73dca6bca977d43bd48e05 +size 278640 diff --git a/CHANGELOG.md b/CHANGELOG.md index dba29a9..bcfb162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/MuzikaGromche/MuzikaGromche.csproj b/MuzikaGromche/MuzikaGromche.csproj index 85b5c8e..a51d880 100644 --- a/MuzikaGromche/MuzikaGromche.csproj +++ b/MuzikaGromche/MuzikaGromche.csproj @@ -58,6 +58,9 @@ $(LethalCompanyDir)Lethal Company_Data\Managed\UnityEngine.UI.dll + + $(LethalCompanyDir)Lethal Company_Data\Managed\Unity.RenderPipelines.Core.Runtime.dll + diff --git a/MuzikaGromche/Plugin.cs b/MuzikaGromche/Plugin.cs index 702f93d..36bc451 100644 --- a/MuzikaGromche/Plugin.cs +++ b/MuzikaGromche/Plugin.cs @@ -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 + { + 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(); + 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)}([{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 DrunknessLoopOffsetTimeSeries { get; } + public TimeSeries 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 IAudioTrack.DrunknessLoopOffsetTimeSeries => Track.DrunknessLoopOffsetTimeSeries; + TimeSeries IAudioTrack.CondensationLoopOffsetTimeSeries => Track.CondensationLoopOffsetTimeSeries; Palette IAudioTrack.Palette => Track.Palette; string? IAudioTrack.GameOverText => Track.GameOverText; } @@ -990,6 +1083,9 @@ namespace MuzikaGromche } } + public TimeSeries DrunknessLoopOffsetTimeSeries { get; init; } = new(); + public TimeSeries 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 GetEvents(BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp) + private List GetEvents(BeatTimestamp loopOffsetTimestamp, BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp) { List 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 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? DrunknessLoopOffsetTimeSeriesOverride = null; + private static TimeSeries? 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 IAudioTrack.DrunknessLoopOffsetTimeSeries => DrunknessLoopOffsetTimeSeriesOverride ?? Track.DrunknessLoopOffsetTimeSeries; + TimeSeries 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(0, 100); @@ -2083,6 +2296,8 @@ namespace MuzikaGromche ConfigEntry fadeOutDurationEntry = null!; ConfigEntry flickerLightsTimeSeriesEntry = null!; ConfigEntry lyricsTimeSeriesEntry = null!; + ConfigEntry drunknessTimeSeriesEntry = null!; + ConfigEntry condensationTimeSeriesEntry = null!; ConfigEntry beatsOffsetEntry = null!; ConfigEntry colorTransitionInEntry = null!; ConfigEntry colorTransitionOutEntry = null!; @@ -2103,9 +2318,13 @@ namespace MuzikaGromche fadeOutDurationEntry = configFile.Bind(section, "Fade Out Duration", 0f, new ConfigDescription("Duration of fading out", new AcceptableValueRange(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(-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(ConfigEntry entry, Func> getter, Action?> setter, Func parser, Func 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? parseTimeSeries(string str, Func parser) + { + try + { + if (string.IsNullOrWhiteSpace(str)) + { + return null; + } + + List beats = []; + List 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(beats.ToArray(), values.ToArray()); + return ts; + } + catch (Exception e) + { + Debug.Log($"{nameof(MuzikaGromche)} Unable to parse time series: {e}"); + return null; + } + } + string formatTimeSeries(TimeSeries ts, Func 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(string str, Func 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 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. diff --git a/MuzikaGromche/ScreenFiltersManager.cs b/MuzikaGromche/ScreenFiltersManager.cs new file mode 100644 index 0000000..2f31370 --- /dev/null +++ b/MuzikaGromche/ScreenFiltersManager.cs @@ -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(); + } + } + } +}