diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc4201..dba29a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## MuzikaGromche 1337.420.9004 +- Override Death Screen / Game Over text in certain cases. ## MuzikaGromche 1337.420.9003 - Lights Out Edition diff --git a/MuzikaGromche/DeathScreenManager.cs b/MuzikaGromche/DeathScreenManager.cs new file mode 100644 index 0000000..cb5291c --- /dev/null +++ b/MuzikaGromche/DeathScreenManager.cs @@ -0,0 +1,72 @@ +using System.Collections; +using HarmonyLib; +using TMPro; +using UnityEngine; + +namespace MuzikaGromche +{ + static class DeathScreenGameOverTextManager + { + private const string GameOverTextVanilla = "[LIFE SUPPORT: OFFLINE]"; + + public const string GameOverTextModdedDefault = "[ MUZIKA: GROMCHE ]"; + + public static void Clear() + { + SetTextImpl(GameOverTextVanilla); + } + + public static void SetText(string? text) + { + SetTextImpl(text ?? GameOverTextModdedDefault); + } + + public static IEnumerator SetTextAndClear(string? text) + { + SetText(text); + // Game Over animation duration is about 4.25 seconds + yield return new WaitForSeconds(5f); + Clear(); + } + + private static void SetTextImpl(string text) + { + GameObject textGameObject = GameObject.Find("Systems/UI/Canvas/DeathScreen/GameOverText"); + if (textGameObject == null) + { + return; + } + TextMeshProUGUI tmp = textGameObject.GetComponent(); + if (tmp == null) + { + return; + } + var transform = textGameObject.GetComponent(); + if (transform == null) + { + return; + } + // Default transform width is 645.8 which only fit the default message and no extra characters + Vector2 size = transform.sizeDelta; + if (Mathf.Approximately(size.x, 645.8f)) + { + size.x += 100f; + transform.sizeDelta = size; + } + tmp.text = text; + } + } + + [HarmonyPatch(typeof(RoundManager))] + static class DeathScreenGameOverTextResetPatch + { + [HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))] + [HarmonyPatch(nameof(RoundManager.OnDestroy))] + [HarmonyPrefix] + static void OnDestroy(RoundManager __instance) + { + var _ = __instance; + DeathScreenGameOverTextManager.Clear(); + } + } +} diff --git a/MuzikaGromche/MuzikaGromche.csproj b/MuzikaGromche/MuzikaGromche.csproj index 8748250..85b5c8e 100644 --- a/MuzikaGromche/MuzikaGromche.csproj +++ b/MuzikaGromche/MuzikaGromche.csproj @@ -52,6 +52,12 @@ $(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Netcode.Runtime.dll + + $(LethalCompanyDir)Lethal Company_Data\Managed\Unity.TextMeshPro.dll + + + $(LethalCompanyDir)Lethal Company_Data\Managed\UnityEngine.UI.dll + diff --git a/MuzikaGromche/Plugin.cs b/MuzikaGromche/Plugin.cs index d9c7ade..702f93d 100644 --- a/MuzikaGromche/Plugin.cs +++ b/MuzikaGromche/Plugin.cs @@ -260,6 +260,7 @@ namespace MuzikaGromche FadeOutDuration = 4, FlickerLightsTimeSeries = [-5, 31], Lyrics = [], + GameOverText = "[MUZIKA GROMCHE: K-POP]", }, new SelectableAudioTrack { @@ -278,6 +279,7 @@ namespace MuzikaGromche FadeOutDuration = 4, FlickerLightsTimeSeries = [-5], Lyrics = [], + GameOverText = "[COULD'VE BEEN: IMMORTAL]", }, new SelectableAudioTrack { @@ -296,6 +298,7 @@ namespace MuzikaGromche FadeOutDuration = 4, FlickerLightsTimeSeries = [-5.5f, 31, 63.9f], Lyrics = [], + GameOverText = "[ HEY, YOUNG BLOOD ]", }, new SelectableAudioTrack { @@ -443,6 +446,7 @@ namespace MuzikaGromche (96, $"\t\t\tresolving ur private IP\n/{PwnLyricsVariants[^1]}"), (98, $"\t\t\tresolving ur private IP\nP_WNED"), ], + GameOverText = "[HACK3D BY: RUSSI4NS]", }, new SelectableAudioTrack { @@ -467,6 +471,7 @@ namespace MuzikaGromche FadeOutDuration = 6, FlickerLightsTimeSeries = [-120.5f, -105, -89, -8, 44, 45], Lyrics = [], + GameOverText = "[DIDN'T PUMP IT: LOUDER]", }, new SelectableAudioTrack { @@ -680,6 +685,7 @@ namespace MuzikaGromche harmony.PatchAll(typeof(DiscoBallTilePatch)); harmony.PatchAll(typeof(DiscoBallDespawnPatch)); harmony.PatchAll(typeof(SpawnRatePatch)); + harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch)); NetcodePatcher(); Compatibility.Register(this); } @@ -882,6 +888,8 @@ namespace MuzikaGromche public string[] LyricsLines { get; } public Palette Palette { get; } + + public string? GameOverText { get => null; } } // A proxy audio track with default implementation for every IAudioTrack method that simply forwards requests to the inner IAudioTrack. @@ -907,6 +915,7 @@ namespace MuzikaGromche float[] IAudioTrack.LyricsTimeSeries => Track.LyricsTimeSeries; string[] IAudioTrack.LyricsLines => Track.LyricsLines; Palette IAudioTrack.Palette => Track.Palette; + string? IAudioTrack.GameOverText => Track.GameOverText; } // Core audio track implementation with some defaults and config overrides. @@ -982,6 +991,8 @@ namespace MuzikaGromche } public Palette Palette { get; set; } = Palette.DEFAULT; + + public string? GameOverText { get; init; } = null; } // Standalone, top-level, selectable audio track @@ -1881,6 +1892,7 @@ namespace MuzikaGromche SetupEntriesToSkipWinding(configFile); SetupEntriesForPaletteOverride(configFile); SetupEntriesForTimingsOverride(configFile); + SetupEntriesForGameOverText(configFile); #endif var chanceRange = new AcceptableValueRange(0, 100); @@ -2182,6 +2194,33 @@ namespace MuzikaGromche } } } + + private void SetupEntriesForGameOverText(ConfigFile configFile) + { + const string section = "Game Over"; + var gameOverTextConfigEntry = configFile.Bind(section, "Game Over Text", DeathScreenGameOverTextManager.GameOverTextModdedDefault, + new ConfigDescription("Custom Game Over text to show.")); + LethalConfigManager.AddConfigItem(new GenericButtonConfigItem(section, "Game Over Animation", + "Run Death Screen / Game Over animation 3 times.", "Trigger", () => + { + HUDManager.Instance.StartCoroutine(AnimateGameOverText(gameOverTextConfigEntry.Value)); + })); + LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(gameOverTextConfigEntry, requiresRestart: false)); + } + + static IEnumerator AnimateGameOverText(string text) + { + yield return new WaitForSeconds(1f); + for (int i = 0; i < 3; i++) + { + DeathScreenGameOverTextManager.SetText(text); + HUDManager.Instance.gameOverAnimator.SetTrigger("gameOver"); + yield return new WaitForSeconds(5f); + HUDManager.Instance.gameOverAnimator.SetTrigger("revive"); + yield return new WaitForSeconds(1f); + } + DeathScreenGameOverTextManager.Clear(); + } #endif private T Default(T options) where T : BaseOptions @@ -2335,6 +2374,16 @@ namespace MuzikaGromche LoopAudioSource.PlayScheduled(loopStartDspTime); Debug.Log($"{nameof(MuzikaGromche)} Play Intro: dspTime={AudioSettings.dspTime:N4}, intro.time={IntroAudioSource.time:N4}/{IntroAudioSource.clip.length:N4}, scheduled Loop={loopStartDspTime}"); } + + public void OverrideDeathScreenGameOverText() + { + if (CurrentTrack == null) + { + // Playing as a client with a host who doesn't have the mod + return; + } + StartCoroutine(DeathScreenGameOverTextManager.SetTextAndClear(CurrentTrack.GameOverText)); + } } [HarmonyPatch(typeof(JesterAI))] @@ -2484,6 +2533,18 @@ namespace MuzikaGromche } } } + + [HarmonyPatch(nameof(JesterAI.killPlayerAnimation))] + [HarmonyPrefix] + static void JesterKillPlayerAnimationPrefix(JesterAI __instance, int playerId) + { + // Note on cast to int: base game already downcasts ulong to int anyway + if (playerId == (int)GameNetworkManager.Instance.localPlayerController.playerClientId) + { + var behaviour = __instance.GetComponent(); + behaviour.OverrideDeathScreenGameOverText(); + } + } } [HarmonyPatch(typeof(EnemyAI))] @@ -2499,8 +2560,9 @@ namespace MuzikaGromche { PoweredLightsBehaviour.Instance.ResetLightColor(); DiscoBallManager.Disable(); + DeathScreenGameOverTextManager.Clear(); // Just in case if players have spawned multiple Jesters, - // Don't reset Plugin.CurrentTrack to null, + // Don't reset Config.CurrentTrack to null, // so that the latest chosen track remains set. } }