1
0
Fork 0

Added new track AttentionPls, implement HUD effects as a time series / timeline

This commit is contained in:
ivan tkachenko 2025-09-26 18:07:11 +03:00
parent e67c72951e
commit aea755361b
7 changed files with 502 additions and 17 deletions

BIN
Assets/AttentionPls1Intro.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Assets/AttentionPls2Intro.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Assets/AttentionPlsLoop.ogg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -3,6 +3,7 @@
## MuzikaGromche 1337.420.9004 ## MuzikaGromche 1337.420.9004
- Override Death Screen / Game Over text in certain cases. - Override Death Screen / Game Over text in certain cases.
- Added a new track AttentionPls featuring multiple intro variants and new visual effects.
## MuzikaGromche 1337.420.9003 - Lights Out Edition ## MuzikaGromche 1337.420.9003 - Lights Out Edition

View File

@ -58,6 +58,9 @@
<Reference Include="UnityEngine.UI" Publicize="true" Private="False"> <Reference Include="UnityEngine.UI" Publicize="true" Private="False">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\UnityEngine.UI.dll</HintPath> <HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\UnityEngine.UI.dll</HintPath>
</Reference> </Reference>
<Reference Include="Unity.RenderPipelines.Core.Runtime" Publicize="true" Private="False">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.RenderPipelines.Core.Runtime.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'"> <ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'">

View File

@ -13,6 +13,7 @@ using System.Net.NetworkInformation;
using System.Net.Sockets; using System.Net.Sockets;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using Unity.Netcode; using Unity.Netcode;
using UnityEngine; using UnityEngine;
using UnityEngine.Networking; using UnityEngine.Networking;
@ -56,6 +57,9 @@ namespace MuzikaGromche
ColorTransitionOut = 0.25f, ColorTransitionOut = 0.25f,
ColorTransitionEasing = Easing.OutExpo, ColorTransitionEasing = Easing.OutExpo,
FlickerLightsTimeSeries = [-5, 29, 61], FlickerLightsTimeSeries = [-5, 29, 61],
DrunknessLoopOffsetTimeSeries = new(
[-2f, 0.0f, 1.0f, 03f, 30f, 32f, 33f, 35f, 62f],
[ 0f, 0.4f, 0.6f, 0f, 0f, 0.5f, 0.7f, 0f, 0f]),
Palette = Palette.Parse(["#B300FF", "#FFF100", "#00FF51", "#474747", "#FF00B3", "#0070FF"]), Palette = Palette.Parse(["#B300FF", "#FFF100", "#00FF51", "#474747", "#FF00B3", "#0070FF"]),
Lyrics = [ Lyrics = [
(-68, "Devchata pljashut pod spidami"), (-68, "Devchata pljashut pod spidami"),
@ -132,6 +136,9 @@ namespace MuzikaGromche
ColorTransitionOut = 0.25f, ColorTransitionOut = 0.25f,
ColorTransitionEasing = Easing.OutExpo, ColorTransitionEasing = Easing.OutExpo,
FlickerLightsTimeSeries = [-101, -93, -77, -61, -37, -5, 27], FlickerLightsTimeSeries = [-101, -93, -77, -61, -37, -5, 27],
DrunknessLoopOffsetTimeSeries = new(
[-48f, -46f, -42f, 16f, 19f, 23f],
[ 0f, 0.7f, 0f, 0f, 0.3f, 0f]),
Palette = Palette.Parse(["#217F87", "#BAFF00", "#73BE25", "#78AB4E", "#FFFF00"]), Palette = Palette.Parse(["#217F87", "#BAFF00", "#73BE25", "#78AB4E", "#FFFF00"]),
Lyrics = [ Lyrics = [
(-111, "Deploy Destroy, porjadok eto otstoj"), (-111, "Deploy Destroy, porjadok eto otstoj"),
@ -583,6 +590,57 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-68.5f, -16.5f, 30.5f], FlickerLightsTimeSeries = [-68.5f, -16.5f, 30.5f],
Lyrics = [], Lyrics = [],
}, },
new SelectableTracksGroup
{
Name = "AttentionPls",
Language = Language.RUSSIAN,
IsExplicit = true,
Tracks =
[
new SelectableAudioTrack
{
Name = "AttentionPls1",
FileNameLoop = "AttentionPlsLoop.ogg",
AudioType = AudioType.OGGVORBIS,
Language = Language.RUSSIAN,
WindUpTimer = 39.19f,
Bars = 8,
BeatsOffset = 0.3f,
ColorTransitionIn = 0.4f,
ColorTransitionOut = 0.4f,
ColorTransitionEasing = Easing.OutExpo,
Palette = Palette.Parse(["#FCEB3C", "#FC3C9D", "#65C7FA", "#89FC8F", "#FEE9E9", "#FCEB3C", "#89FC8F", "#FC3C9D"]),
LoopOffset = 0,
FadeOutBeat = -6,
FadeOutDuration = 5,
FlickerLightsTimeSeries = [-8, 31],
Lyrics = [],
DrunknessLoopOffsetTimeSeries = new([7f, 12f, 15f], [0f, 0.90f, 0f]),
CondensationLoopOffsetTimeSeries = new([23f, 28f, 31f], [0f, 0.4f, 0f]),
},
new SelectableAudioTrack
{
Name = "AttentionPls2",
FileNameLoop = "AttentionPlsLoop.ogg",
AudioType = AudioType.OGGVORBIS,
Language = Language.RUSSIAN,
WindUpTimer = 39.19f,
Bars = 8,
BeatsOffset = 0.3f,
ColorTransitionIn = 0.4f,
ColorTransitionOut = 0.4f,
ColorTransitionEasing = Easing.OutExpo,
Palette = Palette.Parse(["#FCEB3C", "#FC3C9D", "#65C7FA", "#89FC8F", "#FEE9E9", "#FCEB3C", "#89FC8F", "#FC3C9D"]),
LoopOffset = 0,
FadeOutBeat = -6,
FadeOutDuration = 5,
FlickerLightsTimeSeries = [-8, 31],
Lyrics = [],
DrunknessLoopOffsetTimeSeries = new([7f, 12f, 15f], [0f, 0.90f, 0f]),
CondensationLoopOffsetTimeSeries = new([23f, 28f, 31f], [0f, 0.4f, 0f]),
},
],
},
]; ];
public static ISelectableTrack ChooseTrack() public static ISelectableTrack ChooseTrack()
@ -686,6 +744,7 @@ namespace MuzikaGromche
harmony.PatchAll(typeof(DiscoBallDespawnPatch)); harmony.PatchAll(typeof(DiscoBallDespawnPatch));
harmony.PatchAll(typeof(SpawnRatePatch)); harmony.PatchAll(typeof(SpawnRatePatch));
harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch)); harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
NetcodePatcher(); NetcodePatcher();
Compatibility.Register(this); 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 // An instance of a track which appears as a configuration entry and
// can be selected using weighted random from a list of selectable tracks. // can be selected using weighted random from a list of selectable tracks.
public interface ISelectableTrack public interface ISelectableTrack
@ -887,6 +975,9 @@ namespace MuzikaGromche
// If the chosen alternative is an empty string, lyrics event shall be skipped. // If the chosen alternative is an empty string, lyrics event shall be skipped.
public string[] LyricsLines { get; } public string[] LyricsLines { get; }
public TimeSeries<float> DrunknessLoopOffsetTimeSeries { get; }
public TimeSeries<float> CondensationLoopOffsetTimeSeries { get; }
public Palette Palette { get; } public Palette Palette { get; }
public string? GameOverText { get => null; } public string? GameOverText { get => null; }
@ -914,6 +1005,8 @@ namespace MuzikaGromche
float[] IAudioTrack.FlickerLightsTimeSeries => Track.FlickerLightsTimeSeries; float[] IAudioTrack.FlickerLightsTimeSeries => Track.FlickerLightsTimeSeries;
float[] IAudioTrack.LyricsTimeSeries => Track.LyricsTimeSeries; float[] IAudioTrack.LyricsTimeSeries => Track.LyricsTimeSeries;
string[] IAudioTrack.LyricsLines => Track.LyricsLines; string[] IAudioTrack.LyricsLines => Track.LyricsLines;
TimeSeries<float> IAudioTrack.DrunknessLoopOffsetTimeSeries => Track.DrunknessLoopOffsetTimeSeries;
TimeSeries<float> IAudioTrack.CondensationLoopOffsetTimeSeries => Track.CondensationLoopOffsetTimeSeries;
Palette IAudioTrack.Palette => Track.Palette; Palette IAudioTrack.Palette => Track.Palette;
string? IAudioTrack.GameOverText => Track.GameOverText; 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 Palette Palette { get; set; } = Palette.DEFAULT;
public string? GameOverText { get; init; } = null; public string? GameOverText { get; init; } = null;
@ -1198,9 +1294,19 @@ namespace MuzikaGromche
return null; return null;
} }
public readonly float Duration() public readonly float Duration(bool longest = false)
{ {
if (IsEmpty()) if (longest)
{
var to = BeatToInclusive;
if (BeatFromExclusive >= 0f && BeatToInclusive >= 0f && to < BeatFromExclusive)
{
// wrapped
to += LoopBeats;
}
return Mathf.Max(0f, to - BeatFromExclusive);
}
else if (IsEmpty())
{ {
return 0f; return 0f;
} }
@ -1455,8 +1561,8 @@ namespace MuzikaGromche
if (AudioState.HasStarted) if (AudioState.HasStarted)
{ {
var loopTimestamp = Update(LoopLoopingState); var loopOffsetTimestamp = Update(LoopLoopingState);
var loopOffsetSpan = BeatTimeSpan.Between(LastKnownLoopOffsetBeat, loopTimestamp); var loopOffsetSpan = BeatTimeSpan.Between(LastKnownLoopOffsetBeat, loopOffsetTimestamp);
// Do not go back in time // Do not go back in time
if (!loopOffsetSpan.IsEmpty()) if (!loopOffsetSpan.IsEmpty())
@ -1467,8 +1573,8 @@ namespace MuzikaGromche
} }
var windUpOffsetTimestamp = Update(WindUpLoopingState); var windUpOffsetTimestamp = Update(WindUpLoopingState);
LastKnownLoopOffsetBeat = loopTimestamp.Beat; LastKnownLoopOffsetBeat = loopOffsetTimestamp.Beat;
var events = GetEvents(loopOffsetSpan, windUpOffsetTimestamp); var events = GetEvents(loopOffsetTimestamp, loopOffsetSpan, windUpOffsetTimestamp);
#if DEBUG #if DEBUG
Debug.Log($"{nameof(MuzikaGromche)} looping? {(LoopLoopingState.IsLooping ? 'X' : '_')}{(WindUpLoopingState.IsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} Time={Time.realtimeSinceStartup:N4} events={string.Join(",", events)}"); Debug.Log($"{nameof(MuzikaGromche)} looping? {(LoopLoopingState.IsLooping ? 'X' : '_')}{(WindUpLoopingState.IsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} Time={Time.realtimeSinceStartup:N4} events={string.Join(",", events)}");
#endif #endif
@ -1484,13 +1590,13 @@ namespace MuzikaGromche
return loopingState.Update(AudioState.Time, AudioState.IsExtrapolated, AdditionalOffset()); return loopingState.Update(AudioState.Time, AudioState.IsExtrapolated, AdditionalOffset());
} }
// Timings that may be changes through config // Timings that may be changed through config
private float AdditionalOffset() private float AdditionalOffset()
{ {
return Config.AudioOffset.Value + track.BeatsOffsetInSeconds; return Config.AudioOffset.Value + track.BeatsOffsetInSeconds;
} }
private List<BaseEvent> GetEvents(BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp) private List<BaseEvent> GetEvents(BeatTimestamp loopOffsetTimestamp, BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp)
{ {
List<BaseEvent> events = []; List<BaseEvent> events = [];
@ -1511,7 +1617,6 @@ namespace MuzikaGromche
} }
// TODO: quick editor // TODO: quick editor
// loopOffsetSpan.GetLastIndex(Config.LyricsTimeSeries)
if (Config.DisplayLyrics.Value) if (Config.DisplayLyrics.Value)
{ {
var index = loopOffsetSpan.GetLastIndex(track.LyricsTimeSeries); var index = loopOffsetSpan.GetLastIndex(track.LyricsTimeSeries);
@ -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; 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; abstract class BaseEvent;
@ -1706,6 +1899,20 @@ namespace MuzikaGromche
public override string ToString() => "WindUp"; public override string ToString() => "WindUp";
} }
abstract class HUDEvent : BaseEvent;
class DrunkEvent(float drunkness) : HUDEvent
{
public readonly float Drunkness = drunkness;
public override string ToString() => $"Drunk({Drunkness:N2})";
}
class CondensationEvent(float condensation) : HUDEvent
{
public readonly float Condensation = condensation;
public override string ToString() => $"Condensation({Condensation:N2})";
}
// Default C#/.NET remainder operator % returns negative result for negative input // Default C#/.NET remainder operator % returns negative result for negative input
// which is unsuitable as an index for an array. // which is unsuitable as an index for an array.
static class Mod static class Mod
@ -1840,6 +2047,8 @@ namespace MuzikaGromche
private static string? ColorTransitionEasingOverride = null; private static string? ColorTransitionEasingOverride = null;
private static float[]? FlickerLightsTimeSeriesOverride = null; private static float[]? FlickerLightsTimeSeriesOverride = null;
private static float[]? LyricsTimeSeriesOverride = null; private static float[]? LyricsTimeSeriesOverride = null;
private static TimeSeries<float>? DrunknessLoopOffsetTimeSeriesOverride = null;
private static TimeSeries<float>? CondensationLoopOffsetTimeSeriesOverride = null;
private static Palette? PaletteOverride = null; private static Palette? PaletteOverride = null;
private class AudioTrackWithConfigOverride(IAudioTrack track) : ProxyAudioTrack(track), IAudioTrack private class AudioTrackWithConfigOverride(IAudioTrack track) : ProxyAudioTrack(track), IAudioTrack
@ -1864,6 +2073,9 @@ namespace MuzikaGromche
float[] IAudioTrack.LyricsTimeSeries => LyricsTimeSeriesOverride ?? Track.LyricsTimeSeries; float[] IAudioTrack.LyricsTimeSeries => LyricsTimeSeriesOverride ?? Track.LyricsTimeSeries;
TimeSeries<float> IAudioTrack.DrunknessLoopOffsetTimeSeries => DrunknessLoopOffsetTimeSeriesOverride ?? Track.DrunknessLoopOffsetTimeSeries;
TimeSeries<float> IAudioTrack.CondensationLoopOffsetTimeSeries => CondensationLoopOffsetTimeSeriesOverride ?? Track.CondensationLoopOffsetTimeSeries;
Palette IAudioTrack.Palette => PaletteOverride ?? Track.Palette; Palette IAudioTrack.Palette => PaletteOverride ?? Track.Palette;
} }
#endif #endif
@ -1888,11 +2100,12 @@ namespace MuzikaGromche
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions()))); LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions())));
#if DEBUG #if DEBUG
SetupEntriesForGameOverText(configFile);
SetupEntriesForScreenFilters(configFile);
SetupEntriesForExtrapolation(configFile); SetupEntriesForExtrapolation(configFile);
SetupEntriesToSkipWinding(configFile); SetupEntriesToSkipWinding(configFile);
SetupEntriesForPaletteOverride(configFile); SetupEntriesForPaletteOverride(configFile);
SetupEntriesForTimingsOverride(configFile); SetupEntriesForTimingsOverride(configFile);
SetupEntriesForGameOverText(configFile);
#endif #endif
var chanceRange = new AcceptableValueRange<int>(0, 100); var chanceRange = new AcceptableValueRange<int>(0, 100);
@ -2083,6 +2296,8 @@ namespace MuzikaGromche
ConfigEntry<float> fadeOutDurationEntry = null!; ConfigEntry<float> fadeOutDurationEntry = null!;
ConfigEntry<string> flickerLightsTimeSeriesEntry = null!; ConfigEntry<string> flickerLightsTimeSeriesEntry = null!;
ConfigEntry<string> lyricsTimeSeriesEntry = null!; ConfigEntry<string> lyricsTimeSeriesEntry = null!;
ConfigEntry<string> drunknessTimeSeriesEntry = null!;
ConfigEntry<string> condensationTimeSeriesEntry = null!;
ConfigEntry<float> beatsOffsetEntry = null!; ConfigEntry<float> beatsOffsetEntry = null!;
ConfigEntry<float> colorTransitionInEntry = null!; ConfigEntry<float> colorTransitionInEntry = null!;
ConfigEntry<float> colorTransitionOutEntry = null!; ConfigEntry<float> colorTransitionOutEntry = null!;
@ -2103,9 +2318,13 @@ namespace MuzikaGromche
fadeOutDurationEntry = configFile.Bind(section, "Fade Out Duration", 0f, fadeOutDurationEntry = configFile.Bind(section, "Fade Out Duration", 0f,
new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 10))); new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 10)));
flickerLightsTimeSeriesEntry = configFile.Bind(section, "Flicker Lights Time Series", "", flickerLightsTimeSeriesEntry = configFile.Bind(section, "Flicker Lights Time Series", "",
new ConfigDescription("Time series of beat offsets when to flicker the lights.")); new ConfigDescription("Time series of loop offset beats when to flicker the lights."));
lyricsTimeSeriesEntry = configFile.Bind(section, "Lyrics Time Series", "", lyricsTimeSeriesEntry = configFile.Bind(section, "Lyrics Time Series", "",
new ConfigDescription("Time series of beat offsets when to show lyrics lines.")); new ConfigDescription("Time series of loop offset beats when to show lyrics lines."));
drunknessTimeSeriesEntry = configFile.Bind(section, "Drunkness", "",
new ConfigDescription("Time series of loop offset beats which are keyframes for the drunkness effect. Format: 'time1: value1, time2: value2"));
condensationTimeSeriesEntry = configFile.Bind(section, "Helmet Condensation Drops", "",
new ConfigDescription("Time series of loop offset beats which are keyframes for the Helmet Condensation Drops effect. Format: 'time1: value1, time2: value2"));
beatsOffsetEntry = configFile.Bind(section, "Beats Offset", 0f, beatsOffsetEntry = configFile.Bind(section, "Beats Offset", 0f,
new ConfigDescription("How much to offset the whole beat. More is later", new AcceptableValueRange<float>(-0.5f, 0.5f))); new ConfigDescription("How much to offset the whole beat. More is later", new AcceptableValueRange<float>(-0.5f, 0.5f)));
colorTransitionInEntry = configFile.Bind(section, "Color Transition In", 0.25f, colorTransitionInEntry = configFile.Bind(section, "Color Transition In", 0.25f,
@ -2120,6 +2339,8 @@ namespace MuzikaGromche
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutDurationEntry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutDurationEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(flickerLightsTimeSeriesEntry, Default(new TextInputFieldOptions()))); LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(flickerLightsTimeSeriesEntry, Default(new TextInputFieldOptions())));
LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(lyricsTimeSeriesEntry, Default(new TextInputFieldOptions()))); LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(lyricsTimeSeriesEntry, Default(new TextInputFieldOptions())));
LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(drunknessTimeSeriesEntry, Default(new TextInputFieldOptions())));
LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(condensationTimeSeriesEntry, Default(new TextInputFieldOptions())));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(beatsOffsetEntry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(beatsOffsetEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionInEntry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionInEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutEntry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutEntry, floatSliderOptions));
@ -2129,6 +2350,8 @@ namespace MuzikaGromche
registerStruct(fadeOutDurationEntry, t => t.FadeOutDuration, x => FadeOutDurationOverride = x); registerStruct(fadeOutDurationEntry, t => t.FadeOutDuration, x => FadeOutDurationOverride = x);
registerArray(flickerLightsTimeSeriesEntry, t => t.FlickerLightsTimeSeries, xs => FlickerLightsTimeSeriesOverride = xs, float.Parse, sort: true); registerArray(flickerLightsTimeSeriesEntry, t => t.FlickerLightsTimeSeries, xs => FlickerLightsTimeSeriesOverride = xs, float.Parse, sort: true);
registerArray(lyricsTimeSeriesEntry, t => t.LyricsTimeSeries, xs => LyricsTimeSeriesOverride = xs, float.Parse, sort: true); registerArray(lyricsTimeSeriesEntry, t => t.LyricsTimeSeries, xs => LyricsTimeSeriesOverride = xs, float.Parse, sort: true);
registerTimeSeries(drunknessTimeSeriesEntry, t => t.DrunknessLoopOffsetTimeSeries, xs => DrunknessLoopOffsetTimeSeriesOverride = xs, float.Parse, f => f.ToString());
registerTimeSeries(condensationTimeSeriesEntry, t => t.CondensationLoopOffsetTimeSeries, xs => CondensationLoopOffsetTimeSeriesOverride = xs, float.Parse, f => f.ToString());
registerStruct(beatsOffsetEntry, t => t.BeatsOffset, x => BeatsOffsetOverride = x); registerStruct(beatsOffsetEntry, t => t.BeatsOffset, x => BeatsOffsetOverride = x);
registerStruct(colorTransitionInEntry, t => t.ColorTransitionIn, x => ColorTransitionInOverride = x); registerStruct(colorTransitionInEntry, t => t.ColorTransitionIn, x => ColorTransitionInOverride = x);
registerStruct(colorTransitionOutEntry, t => t.ColorTransitionOut, x => ColorTransitionOutOverride = x); registerStruct(colorTransitionOutEntry, t => t.ColorTransitionOut, x => ColorTransitionOutOverride = x);
@ -2162,7 +2385,76 @@ namespace MuzikaGromche
} }
setter.Invoke(overrideTimingsEntry.Value ? values : null); setter.Invoke(overrideTimingsEntry.Value ? values : null);
}); });
void registerTimeSeries<T>(ConfigEntry<string> entry, Func<IAudioTrack, TimeSeries<T>> getter, Action<TimeSeries<T>?> setter, Func<string, T> parser, Func<T, string> formatter) =>
register(entry,
(track) =>
{
var ts = getter(track);
return formatTimeSeries(ts, formatter);
},
() =>
{
var ts = parseTimeSeries(entry.Value, parser);
if (ts is { } timeSeries)
{
entry.Value = formatTimeSeries(timeSeries, formatter);
}
setter.Invoke(overrideTimingsEntry.Value ? ts : null);
});
// current restriction is that formatted value can not contain commas or semicolons.
TimeSeries<T>? parseTimeSeries<T>(string str, Func<string, T> parser)
{
try
{
if (string.IsNullOrWhiteSpace(str))
{
return null;
}
List<float> beats = [];
List<T> values = [];
foreach (var pair in str.Split(","))
{
if (string.IsNullOrWhiteSpace(pair))
{
continue;
}
var keyvalue = pair.Split(":");
if (keyvalue.Length != 2)
{
throw new FormatException($"Pair must be separated by exactly one semicolon: '{pair}'");
}
var beat = float.Parse(keyvalue[0].Trim());
var val = parser(keyvalue[1].Trim());
beats.Add(beat);
values.Add(val);
}
var ts = new TimeSeries<T>(beats.ToArray(), values.ToArray());
return ts;
}
catch (Exception e)
{
Debug.Log($"{nameof(MuzikaGromche)} Unable to parse time series: {e}");
return null;
}
}
string formatTimeSeries<T>(TimeSeries<T> ts, Func<T, string> formatter)
{
StringBuilder strings = new();
for (int i = 0; i < ts.Length; i++)
{
var beat = ts.Beats[i];
var value = formatter(ts.Values[i]);
strings.Append($"{beat}: {value}");
if (i != ts.Length - 1)
{
strings.Append(", ");
}
}
Debug.Log($"{nameof(MuzikaGromche)} format time series {ts} {strings}");
return strings.ToString();
}
T[]? parseStringArray<T>(string str, Func<string, T> parser, bool sort = false) where T : struct T[]? parseStringArray<T>(string str, Func<string, T> parser, bool sort = false) where T : struct
{ {
try try
@ -2221,6 +2513,32 @@ namespace MuzikaGromche
} }
DeathScreenGameOverTextManager.Clear(); DeathScreenGameOverTextManager.Clear();
} }
private void SetupEntriesForScreenFilters(ConfigFile configFile)
{
const string section = "Screen Filters";
var drunkConfigEntry = configFile.Bind(section, "Drunkness Level", 0f,
new ConfigDescription("Override drunkness level in Screen Filters Manager."));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(drunkConfigEntry, requiresRestart: false));
drunkConfigEntry.SettingChanged += (sender, args) =>
{
ScreenFiltersManager.Drunkness = drunkConfigEntry.Value;
};
var condensationConfigEntry = configFile.Bind(section, "Condensation Level", 0f,
new ConfigDescription("Override drunkness level in Screen Filters Manager."));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(condensationConfigEntry, new FloatSliderOptions()
{
Min = 0f,
Max = 0.27f,
RequiresRestart = false,
}));
condensationConfigEntry.SettingChanged += (sender, args) =>
{
ScreenFiltersManager.HelmetCondensationDrops = condensationConfigEntry.Value;
};
}
#endif #endif
private T Default<T>(T options) where T : BaseOptions private T Default<T>(T options) where T : BaseOptions
@ -2498,6 +2816,7 @@ namespace MuzikaGromche
{ {
PoweredLightsBehaviour.Instance.ResetLightColor(); PoweredLightsBehaviour.Instance.ResetLightColor();
DiscoBallManager.Disable(); DiscoBallManager.Disable();
ScreenFiltersManager.Clear();
// Rotate track groups // Rotate track groups
behaviour.ChooseTrackServerRpc(); behaviour.ChooseTrackServerRpc();
behaviour.BeatTimeState = null; behaviour.BeatTimeState = null;
@ -2507,6 +2826,7 @@ namespace MuzikaGromche
else if ((__instance.previousState == 1 || __instance.previousState == 2) && behaviour.BeatTimeState is { } beatTimeState) else if ((__instance.previousState == 1 || __instance.previousState == 2) && behaviour.BeatTimeState is { } beatTimeState)
{ {
var events = beatTimeState.Update(introAudioSource, loopAudioSource); var events = beatTimeState.Update(introAudioSource, loopAudioSource);
var localPlayerCanHearMusic = Plugin.LocalPlayerCanHearMusic(__instance);
foreach (var ev in events) foreach (var ev in events)
{ {
switch (ev) switch (ev)
@ -2523,11 +2843,16 @@ namespace MuzikaGromche
RoundManager.Instance.FlickerLights(true); RoundManager.Instance.FlickerLights(true);
break; break;
case LyricsEvent e: case LyricsEvent e when localPlayerCanHearMusic:
if (Plugin.LocalPlayerCanHearMusic(__instance)) Plugin.DisplayLyrics(e.Text);
{ break;
Plugin.DisplayLyrics(e.Text);
} case DrunkEvent e when localPlayerCanHearMusic:
ScreenFiltersManager.Drunkness = e.Drunkness;
break;
case CondensationEvent e when localPlayerCanHearMusic:
ScreenFiltersManager.HelmetCondensationDrops = e.Condensation;
break; break;
} }
} }
@ -2561,6 +2886,7 @@ namespace MuzikaGromche
PoweredLightsBehaviour.Instance.ResetLightColor(); PoweredLightsBehaviour.Instance.ResetLightColor();
DiscoBallManager.Disable(); DiscoBallManager.Disable();
DeathScreenGameOverTextManager.Clear(); DeathScreenGameOverTextManager.Clear();
ScreenFiltersManager.Clear();
// Just in case if players have spawned multiple Jesters, // Just in case if players have spawned multiple Jesters,
// Don't reset Config.CurrentTrack to null, // Don't reset Config.CurrentTrack to null,
// so that the latest chosen track remains set. // so that the latest chosen track remains set.

View File

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