From 95819cf83ae72aa6e021e7be6bc76d0b0ccdfc12 Mon Sep 17 00:00:00 2001
From: Atony <75191600+AtonyGit@users.noreply.github.com>
Date: Fri, 14 Aug 2026 22:29:40 -0500
Subject: [PATCH] Add (unsynced) in-game timebar + adjust ui pos
---
MiraAPI/GameModes/AbstractGameMode.cs | 60 +++++
MiraAPI/GameModes/HideAndSeekMode.cs | 68 +++++-
.../HnsReimplemented/HideAndSeekHudHelper.cs | 227 ++++++++++++++++++
.../Patches/GameModes/GameManagerPatches.cs | 25 ++
.../Patches/GameModes/ProgressTrackerPatch.cs | 17 ++
MiraAPI/Patches/GameModes/TaskPanelPatch.cs | 17 ++
6 files changed, 410 insertions(+), 4 deletions(-)
create mode 100644 MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs
create mode 100644 MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs
create mode 100644 MiraAPI/Patches/GameModes/TaskPanelPatch.cs
diff --git a/MiraAPI/GameModes/AbstractGameMode.cs b/MiraAPI/GameModes/AbstractGameMode.cs
index efb7384..6780efb 100644
--- a/MiraAPI/GameModes/AbstractGameMode.cs
+++ b/MiraAPI/GameModes/AbstractGameMode.cs
@@ -218,6 +218,20 @@ public abstract class AbstractGameMode : IOptionable
/// True if the sabotage map should be shown.
public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true;
+ ///
+ /// Gets the to display on the map.
+ ///
+ /// The for the gamemode.
+ public virtual MapOptions GetMapOptions()
+ {
+ return new MapOptions
+ {
+ Mode = PlayerControl.LocalPlayer.Data.Role.IsImpostor && !MeetingHud.Instance
+ ? MapOptions.Modes.Sabotage
+ : MapOptions.Modes.Normal,
+ };
+ }
+
///
/// Can a player vent in this gamemode.
///
@@ -226,6 +240,52 @@ public abstract class AbstractGameMode : IOptionable
/// True if venting is enabled in this mode.
public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true;
+ ///
+ /// Does the task bar appear in the gamemode.
+ ///
+ /// True if the task bar is enabled in this mode.
+ public virtual bool ShowTaskBar => true;
+
+ public virtual void UpdateTaskPanel(TaskPanelBehaviour instance)
+ {
+ instance.background.transform.localScale = (instance.taskText.textBounds.size.x > 0f)
+ ? new Vector3(instance.taskText.textBounds.size.x + 0.2f, instance.taskText.textBounds.size.y + 0.2f, 1f)
+ : Vector3.zero;
+ Vector3 vector = instance.background.sprite.bounds.extents;
+ vector.y = -vector.y;
+ vector = vector.Mul(instance.background.transform.localScale);
+ instance.background.transform.localPosition = vector;
+ Vector3 vector2 = instance.tab.sprite.bounds.extents;
+ vector2 = vector2.Mul(instance.tab.transform.localScale);
+ vector2.y = -vector2.y;
+ vector2.x += vector.x * 2f;
+ instance.tab.transform.localPosition = vector2;
+ if (GameManager.Instance == null)
+ {
+ return;
+ }
+
+ var yPos = 0.6f;
+ var xPos = -instance.background.sprite.bounds.size.x * instance.background.transform.localScale.x;
+ instance.closedPosition = new Vector3(xPos, yPos, instance.closedPosition.z);
+ instance.openPosition = new Vector3(instance.openPosition.x, yPos, instance.openPosition.z);
+ if (instance.open)
+ {
+ instance.timer = Mathf.Min(1f, instance.timer + Time.deltaTime / instance.animationTimeSeconds);
+ }
+ else
+ {
+ instance.timer = Mathf.Max(0f, instance.timer - Time.deltaTime / instance.animationTimeSeconds);
+ }
+
+ Vector3 relativePos = new(
+ Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer),
+ yPos,
+ instance.openPosition.z);
+ instance.transform.localPosition =
+ AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos);
+ }
+
///
public override string ToString() => Name;
}
diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs
index f1b5953..2c0f38e 100644
--- a/MiraAPI/GameModes/HideAndSeekMode.cs
+++ b/MiraAPI/GameModes/HideAndSeekMode.cs
@@ -7,6 +7,7 @@ using AmongUs.GameOptions;
using HarmonyLib;
using InnerNet;
using MiraAPI.GameOptions;
+using MiraAPI.HnsReimplemented;
using MiraAPI.HnsReimplemented.Options;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
@@ -39,6 +40,7 @@ public class HideAndSeekMode : AbstractGameMode
public override bool ShowNormalRoleSettings => false;
public override float DefaultImpostorKillCooldown => 1f;
+ public override bool ShowTaskBar => false;
public static int ImpostorPlayerID()
{
return OptionGroupSingleton.Instance.SelectedSeeker.Value;
@@ -73,7 +75,7 @@ public class HideAndSeekMode : AbstractGameMode
}
IGameOptions currentGameOptions = GameOptionsManager.Instance.CurrentGameOptions;
int adjustedNumImpostors = GameOptionsManager.Instance.CurrentGameOptions.GetAdjustedNumImpostors(list2.Count);
- AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, adjustedNumImpostors, RoleTypes.Impostor);
+ AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Impostor, Math.Max(adjustedNumImpostors, 1), RoleTypes.Impostor);
AssignRolesForTeam(list2, currentGameOptions, RoleTeamTypes.Crewmate, int.MaxValue, RoleTypes.Engineer);
}
@@ -84,7 +86,7 @@ public class HideAndSeekMode : AbstractGameMode
int teamMax,
RoleTypes defaultRole)
{
- Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}");
+ Error($"Hide And Seek Mode - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}");
int num = 0;
IRoleOptionsCollection roleOptions = opts.RoleOptions;
var source = RoleManager.Instance.AllRoles.ToArray()
@@ -400,6 +402,25 @@ public class HideAndSeekMode : AbstractGameMode
deadPlayerCount = 0;
ShipStatus.Instance.BreakEmergencyButton();
PlayerControl.LocalPlayer.SetKillTimer(0.01f);
+ if (HudManager.InstanceExists)
+ {
+ HudManager.Instance.gameObject.AddComponent();
+ }
+ }
+ public override MapOptions GetMapOptions()
+ {
+ MapOptions mapOptions = new MapOptions
+ {
+ Mode = MapOptions.Modes.Normal,
+ };
+ if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && HideAndSeekHudHelper.Instance.SeekerAdminMapEnabled(PlayerControl.LocalPlayer))
+ {
+ mapOptions.Mode = MapOptions.Modes.CountOverlay;
+ mapOptions.AllowMovementWhileMapOpen = true;
+ mapOptions.IncludeDeadBodies = false;
+ mapOptions.ShowLivePlayerPosition = false;
+ }
+ return mapOptions;
}
public override void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance)
@@ -411,10 +432,10 @@ public class HideAndSeekMode : AbstractGameMode
}
if (Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor) != 0)
{
- /*if (instance.AllTimersExpired())
+ if (HideAndSeekHudHelper.Instance.AllTimersExpired())
{
instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_CrewmatesByTimer, !DataManager.Player.Ads.HasPurchasedAdRemoval);
- }*/
+ }
return;
}
instance.Manager.RpcEndGame(GameOverReason.HideAndSeek_ImpostorsByKills, !DataManager.Player.Ads.HasPurchasedAdRemoval);
@@ -459,6 +480,45 @@ public class HideAndSeekMode : AbstractGameMode
return PlayerBodyTypes.Normal;
}
}
+ public override void UpdateTaskPanel(TaskPanelBehaviour instance)
+ {
+ instance.background.transform.localScale = (instance.taskText.textBounds.size.x > 0f)
+ ? new Vector3(instance.taskText.textBounds.size.x + 0.2f, instance.taskText.textBounds.size.y + 0.2f, 1f)
+ : Vector3.zero;
+ Vector3 vector = instance.background.sprite.bounds.extents;
+ vector.y = -vector.y;
+ vector = vector.Mul(instance.background.transform.localScale);
+ instance.background.transform.localPosition = vector;
+ Vector3 vector2 = instance.tab.sprite.bounds.extents;
+ vector2 = vector2.Mul(instance.tab.transform.localScale);
+ vector2.y = -vector2.y;
+ vector2.x += vector.x * 2f;
+ instance.tab.transform.localPosition = vector2;
+ if (GameManager.Instance == null)
+ {
+ return;
+ }
+
+ var yPos = 1.6f;
+ var xPos = -instance.background.sprite.bounds.size.x * instance.background.transform.localScale.x;
+ instance.closedPosition = new Vector3(xPos, yPos, instance.closedPosition.z);
+ instance.openPosition = new Vector3(instance.openPosition.x, yPos, instance.openPosition.z);
+ if (instance.open)
+ {
+ instance.timer = Mathf.Min(1f, instance.timer + Time.deltaTime / instance.animationTimeSeconds);
+ }
+ else
+ {
+ instance.timer = Mathf.Max(0f, instance.timer - Time.deltaTime / instance.animationTimeSeconds);
+ }
+
+ Vector3 relativePos = new(
+ Mathf.SmoothStep(instance.closedPosition.x, instance.openPosition.x, instance.timer),
+ yPos,
+ instance.openPosition.z);
+ instance.transform.localPosition =
+ AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos);
+ }
private int deadPlayerCount;
public override void OnPlayerDeath(PlayerControl player, bool assignGhostRole)
{
diff --git a/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs
new file mode 100644
index 0000000..329c8dc
--- /dev/null
+++ b/MiraAPI/HnsReimplemented/HideAndSeekHudHelper.cs
@@ -0,0 +1,227 @@
+using System.Collections;
+using System.Linq;
+using BepInEx.Unity.IL2CPP.Utils.Collections;
+using MiraAPI.GameOptions;
+using MiraAPI.HnsReimplemented.Options;
+using MiraAPI.Utilities;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace MiraAPI.HnsReimplemented;
+
+[RegisterInIl2Cpp]
+public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr)
+{
+ public static HideAndSeekHudHelper Instance { get; private set; }
+
+ public AudioClip FinalHideAlertSfx;
+
+ public AudioClip FinalHideCountdownSfx;
+
+ private AudioClip TaskFinishedSound;
+
+ private const int SECONDS_TO_BEEP = 10;
+
+ private const float SECONDS_TO_SET_DIRTY = 1f;
+
+ private HideAndSeekTimerBar timerBar;
+
+ private float totalHideTime = float.MaxValue;
+
+ private float currentHideTime = float.MaxValue;
+
+ private float totalFinalHideTime = float.MaxValue;
+
+ private float currentFinalHideTime = float.MaxValue;
+
+ private float secondsSinceLastSetDirty;
+ private Coroutine beepCoroutine;
+
+ private void Awake()
+ {
+ Instance = this;
+ }
+
+ public bool IsFinalCountdown
+ {
+ get { return currentHideTime <= 0f; }
+ }
+
+ public bool SeekerAdminMapEnabled(PlayerControl player)
+ {
+ int item = Helpers.GetAlivePlayers().Count(x => !x.Data.Role.IsImpostor);
+ return !player.inVent && player.Data != null && player.Data.Role != null &&
+ ((!player.inVent && player.Data.Role.IsImpostor && IsFinalCountdown &&
+ OptionGroupSingleton.Instance.FinalHideSeekMap.Value) ||
+ (player.Data.Role.IsImpostor &&
+ item <=
+ (GameData.Instance.PlayerCount - 1) / 3));
+ }
+
+ public void OnTaskComplete(float timeDeduction)
+ {
+ if (timerBar != null)
+ {
+ timerBar.TaskComplete();
+ }
+
+ AdjustEscapeTimer(timeDeduction, true);
+ }
+
+ public float GetTotalRoundTime()
+ {
+ float escapeTime = OptionGroupSingleton.Instance.HidingTime.Value;
+ float finalCountdownTime = OptionGroupSingleton.Instance.FinalHideTime.Value;
+ return escapeTime + finalCountdownTime;
+ }
+
+ public float GetTotalTimeRemaining()
+ {
+ return currentHideTime + currentFinalHideTime;
+ }
+
+ public float GetRoundTimeElapsed()
+ {
+ return GetTotalRoundTime() - GetTotalTimeRemaining();
+ }
+
+ public void Start()
+ {
+ totalHideTime = OptionGroupSingleton.Instance.HidingTime.Value;
+ currentHideTime = totalHideTime;
+ totalFinalHideTime = OptionGroupSingleton.Instance.FinalHideTime.Value;
+ currentFinalHideTime = totalFinalHideTime;
+ if (timerBar != null)
+ {
+ Object.Destroy(timerBar);
+ }
+
+ FinalHideAlertSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideAlertSFX;
+ FinalHideCountdownSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideCountdownSFX;
+ TaskFinishedSound = GameManagerCreator.Instance.HideAndSeekManagerPrefab.TaskFinishedSound;
+ timerBar = Object.Instantiate(
+ GameManagerCreator.Instance.HideAndSeekManagerPrefab.TimerBarPrefab,
+ HudManager.Instance.transform.parent);
+ }
+
+ public void OnGameEnd()
+ {
+ if (timerBar != null)
+ {
+ Object.Destroy(timerBar.gameObject);
+ }
+
+ if (beepCoroutine != null)
+ {
+ StopCoroutine(beepCoroutine);
+ }
+
+ beepCoroutine = null!;
+ }
+
+ public void FixedUpdate()
+ {
+ secondsSinceLastSetDirty += Time.fixedDeltaTime;
+ if (IsFinalCountdown)
+ {
+ AdjustFinalEscapeTimer(Time.fixedDeltaTime);
+ return;
+ }
+
+ AdjustEscapeTimer(Time.fixedDeltaTime, false);
+ }
+
+ public void OnDestroy()
+ {
+ if (timerBar != null)
+ {
+ Object.Destroy(timerBar);
+ }
+ }
+
+ private void OnFinalCountdownTriggered()
+ {
+ foreach (PlayerControl playerControl in PlayerControl.AllPlayerControls)
+ {
+ if (!playerControl.Data.Role.IsImpostor && !playerControl.Data.IsDead)
+ {
+ playerControl.ClearTasks();
+ PlayerTask.GetOrCreateTask(playerControl, 0).Text =
+ DestroyableSingleton.Instance.GetString(StringNames.HideActionButton);
+ }
+ }
+
+ if (!PlayerControl.LocalPlayer.Data.IsDead && Minigame.Instance != null)
+ {
+ Minigame instance = Minigame.Instance;
+ if (instance != null)
+ {
+ instance.ForceClose();
+ }
+ }
+
+ timerBar.StartFinalHide();
+ SoundManager.Instance.PlaySound(FinalHideAlertSfx, false, 1f, null);
+ DestroyableSingleton.Instance.SetAlertOverlay(true);
+ }
+
+ private void AdjustEscapeTimer(float timeDeduction, bool forceDirty)
+ {
+ float num = currentHideTime;
+ currentHideTime -= timeDeduction;
+ currentHideTime = Mathf.Max(currentHideTime, 0f);
+ if (currentHideTime <= 10f && beepCoroutine == null)
+ {
+ beepCoroutine = StartCoroutine(BeepAlmostEverySecond().WrapToIl2Cpp());
+ }
+
+ if (num > 0f && currentHideTime <= 0f)
+ {
+ OnFinalCountdownTriggered();
+ }
+
+ timerBar.UpdateTimer(currentHideTime, totalHideTime);
+ if (forceDirty || secondsSinceLastSetDirty > 1f)
+ {
+ secondsSinceLastSetDirty = 0f;
+ }
+ }
+
+ private IEnumerator BeepAlmostEverySecond()
+ {
+ while (!IsFinalCountdown)
+ {
+ float num = currentHideTime / 10f;
+ float pitch = 1.5f - num / 2f;
+ SoundManager.Instance.PlaySoundImmediate(FinalHideCountdownSfx, false, 1f, pitch, null);
+ yield return new WaitForSeconds(1f);
+ }
+
+ yield return Effects.Wait(currentFinalHideTime - 10f);
+ while (currentFinalHideTime > 0f)
+ {
+ float num2 = currentFinalHideTime / 10f;
+ float pitch2 = 1.5f - num2 / 2f;
+ SoundManager.Instance.PlaySoundImmediate(FinalHideCountdownSfx, false, 1f, pitch2, null);
+ yield return new WaitForSeconds(1f);
+ }
+
+ yield break;
+ }
+
+ private void AdjustFinalEscapeTimer(float timeDeduction)
+ {
+ currentFinalHideTime -= timeDeduction;
+ currentFinalHideTime = Mathf.Max(currentFinalHideTime, 0f);
+ timerBar.UpdateTimer(currentFinalHideTime, totalFinalHideTime);
+ if (secondsSinceLastSetDirty > 1f)
+ {
+ secondsSinceLastSetDirty = 0f;
+ }
+ }
+
+ public bool AllTimersExpired()
+ {
+ return currentHideTime <= 0f && currentFinalHideTime <= 0f;
+ }
+}
diff --git a/MiraAPI/Patches/GameModes/GameManagerPatches.cs b/MiraAPI/Patches/GameModes/GameManagerPatches.cs
index fca4f75..3f56e82 100644
--- a/MiraAPI/Patches/GameModes/GameManagerPatches.cs
+++ b/MiraAPI/Patches/GameModes/GameManagerPatches.cs
@@ -28,4 +28,29 @@ internal static class GameManagerPatches
return true;
}
+ [HarmonyPatch(typeof(NormalGameManager), nameof(NormalGameManager.GetMapOptions))]
+ [HarmonyPrefix]
+ public static bool GetMapOptions(ref MapOptions __result)
+ {
+ if (CustomGameModeManager.ActiveMode == null)
+ {
+ return true;
+ }
+
+ __result = CustomGameModeManager.ActiveMode.GetMapOptions();
+ return false;
+ }
}
+
+[HarmonyPatch(typeof(GameStartManager))]
+internal static class GameStartManagerPatches
+{
+ [HarmonyPrefix, HarmonyPatch(nameof(GameStartManager.Start))]
+ public static void StartPatch(GameStartManager __instance)
+ {
+ if (MiraApiPlugin.IsDevBuild)
+ {
+ __instance.MinPlayers = 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs b/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs
new file mode 100644
index 0000000..53620e0
--- /dev/null
+++ b/MiraAPI/Patches/GameModes/ProgressTrackerPatch.cs
@@ -0,0 +1,17 @@
+using HarmonyLib;
+using MiraAPI.GameModes;
+
+namespace MiraAPI.Patches.GameModes;
+[HarmonyPatch(typeof(ProgressTracker), nameof(ProgressTracker.Start))]
+internal static class ProgressTrackerPatch
+{
+ public static bool Prefix(ProgressTracker __instance)
+ {
+ if (CustomGameModeManager.ActiveMode != null && !CustomGameModeManager.ActiveMode.ShowTaskBar)
+ {
+ __instance.gameObject.SetActive(false);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/MiraAPI/Patches/GameModes/TaskPanelPatch.cs b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs
new file mode 100644
index 0000000..f312a9c
--- /dev/null
+++ b/MiraAPI/Patches/GameModes/TaskPanelPatch.cs
@@ -0,0 +1,17 @@
+using HarmonyLib;
+using MiraAPI.GameModes;
+
+namespace MiraAPI.Patches.GameModes;
+[HarmonyPatch(typeof(TaskPanelBehaviour), nameof(TaskPanelBehaviour.Update))]
+internal static class TaskPanelPatch
+{
+ public static bool Prefix(TaskPanelBehaviour __instance)
+ {
+ if (CustomGameModeManager.ActiveMode != null)
+ {
+ CustomGameModeManager.ActiveMode.UpdateTaskPanel(__instance);
+ return false;
+ }
+ return true;
+ }
+}