forked from nikita/muzika-gromche
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))
|
||||
{
|
||||
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