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();
+ }
+ }
+ }
+}