diff --git a/MiraAPI/GameOptions/AbstractOptionGroup.cs b/MiraAPI/GameOptions/AbstractOptionGroup.cs
index 45f9bd4..db2b93c 100644
--- a/MiraAPI/GameOptions/AbstractOptionGroup.cs
+++ b/MiraAPI/GameOptions/AbstractOptionGroup.cs
@@ -35,7 +35,7 @@ public abstract class AbstractOptionGroup
///
/// Gets the group color. This is used to color the group in the options menu.
///
- public virtual Color GroupColor => Color.clear;
+ public virtual Color GroupColor => MiraApiPlugin.DefaultHeaderColor;
///
/// Gets the group priority. This is used to determine the order in which groups are displayed in the options menu.
diff --git a/MiraAPI/Hud/CustomActionButton.cs b/MiraAPI/Hud/CustomActionButton.cs
index 8e5815c..6a721da 100644
--- a/MiraAPI/Hud/CustomActionButton.cs
+++ b/MiraAPI/Hud/CustomActionButton.cs
@@ -168,7 +168,7 @@ public abstract class CustomActionButton
/// Allows you to change the button's location.
///
/// The new location.
- /// Whether the button's position should change ingame.
+ /// Whether the button's position should change in-game.
public virtual void SetButtonLocation(ButtonLocation location, bool moveButton = true)
{
if (!HudManager.InstanceExists || Button == null)
@@ -221,7 +221,6 @@ public abstract class CustomActionButton
/// A utility function to change the outline color of the button's text.
///
/// The new color.
- /// The thickness of the outline. Set to 0 if you want to remove it.
public virtual void SetTextOutline(Color color)
{
Button?.buttonLabelText.SetOutlineColor(color);
@@ -364,7 +363,7 @@ public abstract class CustomActionButton
/// A value that represents whether the button should light up or not.
public virtual bool CanUse()
{
- return !LimitedUses || UsesLeft > 0;
+ return PlayerControl.LocalPlayer.moveable && (!LimitedUses || UsesLeft > 0);
}
///
diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs
index 3380820..46f0a16 100644
--- a/MiraAPI/MiraApiPlugin.cs
+++ b/MiraAPI/MiraApiPlugin.cs
@@ -1,12 +1,19 @@
-using BepInEx;
+using System;
+using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
+using Il2CppInterop.Runtime;
using MiraAPI.PluginLoading;
+using MiraAPI.Roles;
+using MonoMod.Utils;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
using Reactor.Utilities;
+using TMPro;
using UnityEngine;
+using UnityEngine.ProBuilder;
+using Delegate = Il2CppSystem.Delegate;
namespace MiraAPI;
@@ -20,6 +27,7 @@ namespace MiraAPI;
public partial class MiraApiPlugin : BasePlugin
{
public static Color MiraColor { get; } = new Color32(238, 154, 112, 255);
+ public static Color DefaultHeaderColor = new Color32(77, 77, 77, 255);
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
diff --git a/MiraAPI/Modifiers/ModifierExtensions.cs b/MiraAPI/Modifiers/ModifierExtensions.cs
index 06d9f06..ca49808 100644
--- a/MiraAPI/Modifiers/ModifierExtensions.cs
+++ b/MiraAPI/Modifiers/ModifierExtensions.cs
@@ -180,6 +180,30 @@ public static class ModifierExtensions
return player.GetModifierComponent().HasModifier(typeId, predicate);
}
+ ///
+ /// Clears all modifiers from a player.
+ ///
+ /// The player you want to clear modifiers for.
+ public static void ClearModifiers(this PlayerControl plr)
+ {
+ foreach (var mod in plr.GetModifierComponent().ActiveModifiers)
+ {
+ plr.RemoveModifier(mod);
+ }
+ }
+
+ ///
+ /// Clears all modifiers from a player.
+ ///
+ /// The player you want to clear modifiers for.
+ public static void RpcClearModifiers(this PlayerControl plr)
+ {
+ foreach (var mod in plr.GetModifierComponent().ActiveModifiers)
+ {
+ plr.RpcRemoveModifier(mod.UniqueId);
+ }
+ }
+
///
/// Checks if the player has a specific modifier by its GUID.
///
diff --git a/MiraAPI/Modifiers/ModifierUtils.cs b/MiraAPI/Modifiers/ModifierUtils.cs
new file mode 100644
index 0000000..b69e7bc
--- /dev/null
+++ b/MiraAPI/Modifiers/ModifierUtils.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MiraAPI.Modifiers;
+
+///
+/// Utilities to make handling modifiers in-game easier.
+///
+public static class ModifierUtils
+{
+ ///
+ /// Gets a list of all active in-game modifiers.
+ ///
+ /// Select if modifier is valid to be added to list.
+ /// The modifier type.
+ /// A list of modifiers.
+ public static IEnumerable GetActiveModifiers(Func? predicate = null) where T : BaseModifier
+ {
+ return PlayerControl.AllPlayerControls.ToArray().SelectMany(x => x.GetModifiers(predicate)).OfType();
+ }
+
+ ///
+ /// Gets all players with a certain modifier.
+ ///
+ /// Select if modifier is valid.
+ /// The modifier type.
+ /// A list of players with that modifier.
+ public static IEnumerable GetPlayersWithModifier(Func? predicate = null) where T : BaseModifier
+ {
+ return PlayerControl.AllPlayerControls.ToArray().Where(x => x.HasModifier(predicate));
+ }
+}
diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
index b792767..05de7f6 100644
--- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
+++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs
@@ -42,7 +42,7 @@ public static class GameOptionsMenuPatch
{
var filteredGroups =
GameSettingMenuPatches.SelectedMod?.OptionGroups
- .Where(x => x.OptionableType == null) ?? [];
+ .Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
foreach (var group in filteredGroups)
{
@@ -144,7 +144,7 @@ public static class GameOptionsMenuPatch
}
var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups
- .Where(x => x.OptionableType == null) ?? [];
+ .Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
foreach (var group in filteredGroups)
{
@@ -198,16 +198,16 @@ public static class GameOptionsMenuPatch
menu.settingsContainer);
categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20);
- if (group.GroupColor != Color.clear)
- {
- categoryHeaderMasked.Background.color = group.GroupColor;
- categoryHeaderMasked.Divider.color = group.GroupColor;
- categoryHeaderMasked.Title.color = group.GroupColor.FindAlternateColor();
- }
+ categoryHeaderMasked.Background.color = group.GroupColor;
+ categoryHeaderMasked.Divider.color = group.GroupColor;
+ categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) ? Color.white : group.GroupColor.FindAlternateColor();
+ categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset();
+ categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.55f, -0.1833f, 0);
categoryHeaderMasked.Background.size = new Vector2(
categoryHeaderMasked.Background.size.x + 1.5f,
categoryHeaderMasked.Background.size.y);
+
categoryHeaderMasked.gameObject.SetActive(false);
group.Header = categoryHeaderMasked;
@@ -230,7 +230,7 @@ public static class GameOptionsMenuPatch
SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true);
foreach (var renderer in componentsInChildren)
{
- if (group.GroupColor != Color.clear)
+ if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor)
{
renderer.color = group.GroupColor.FindAlternateColor();
if (renderer.transform.parent.TryGetComponent(out var btn))
@@ -245,7 +245,7 @@ public static class GameOptionsMenuPatch
foreach (var textMeshPro in newOpt.GetComponentsInChildren(true))
{
- if (group.GroupColor != Color.clear)
+ if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor)
{
textMeshPro.color = group.GroupColor;
}
@@ -257,12 +257,11 @@ public static class GameOptionsMenuPatch
if (newOpt is ToggleOption toggle)
{
toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset();
- toggle.CheckMark.color =
- group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal;
+ toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor ? group.GroupColor : MiraAssets.AcceptedTeal;
var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite")
.GetComponent();
rend.sprite = MiraAssets.CheckmarkBox.LoadAsset();
- rend.color = group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal;
+ rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor ? group.GroupColor : MiraAssets.AcceptedTeal;
}
menu.Children.Add(newOpt);
diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
index 89a6a24..43c5719 100644
--- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
+++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs
@@ -330,8 +330,8 @@ public static class LobbyViewPanePatches
numPerGame,
chancePerGame,
61,
- customRole.OptionsMenuColor,
- customRole.Configuration.Icon.LoadAsset(),
+ customRole.RoleColor,
+ customRole.Configuration.Icon?.LoadAsset() ?? MiraAssets.Empty.LoadAsset(),
true);
viewSettingsInfoPanelRoleVariant.iconSprite.transform.localScale = new Vector3(0.365f, 0.365f, 1f);
viewSettingsInfoPanelRoleVariant.iconSprite.transform.localPosition = new Vector3(0.7144f, -0.028f, -2);
@@ -340,7 +340,7 @@ public static class LobbyViewPanePatches
viewSettingsInfoPanelRoleVariant.chanceTitle.color =
viewSettingsInfoPanelRoleVariant.chanceBackground.color =
viewSettingsInfoPanelRoleVariant.background.color =
- customRole.OptionsMenuColor.FindAlternateColor();
+ customRole.RoleColor.GetAlternateColor();
instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject);
num -= 0.664f;
}
@@ -418,7 +418,7 @@ public static class LobbyViewPanePatches
role.StringName,
maskLayer,
role.TeamType == RoleTeamTypes.Crewmate,
- customRole.Configuration.Icon.LoadAsset());
+ customRole.Configuration.Icon != null ? customRole.Configuration.Icon.LoadAsset() : MiraAssets.Empty.LoadAsset());
viewPanel.header.icon.transform.localScale = new Vector3(0.465f, 0.465f, 1f);
viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
index 011857d..e4984fd 100644
--- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
+++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
@@ -127,32 +127,34 @@ public static class RoleSettingMenuPatches
chanceLabel.Destroy();
countLabel.Destroy();
- if (name is StringNames.CrewmateRolesHeader)
- {
- categoryHeaderMasked.Title.color = Palette.CrewmateRoleHeaderTextBlue;
- categoryHeaderMasked.Background.color = Palette.CrewmateRoleHeaderBlue;
- }
- else if (name is StringNames.ImpostorRolesHeader)
- {
- categoryHeaderMasked.Title.color = Palette.ImpostorRoleHeaderTextRed;
- categoryHeaderMasked.Background.color = Palette.ImpostorRoleHeaderRed;
- }
- else
- {
- categoryHeaderMasked.Title.color = group.Color;
- categoryHeaderMasked.Divider.color = group.Color;
- categoryHeaderMasked.Background.color = group.Color;
- }
+ categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset();
+ categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.55f, -0.1833f, 0);
- categoryHeaderMasked.Title.color = categoryHeaderMasked.Title.color.IsColorDark() ?
- categoryHeaderMasked.Title.color.LightenColor(0.3f) : categoryHeaderMasked.Title.color.LightenColor(0.1f);
+ switch (name)
+ {
+ case StringNames.CrewmateRolesHeader:
+ categoryHeaderMasked.Title.color = Palette.CrewmateRoleHeaderTextBlue;
+ categoryHeaderMasked.Background.color = Palette.CrewmateRoleHeaderBlue;
+ break;
+ case StringNames.ImpostorRolesHeader:
+ categoryHeaderMasked.Title.color = Palette.ImpostorRoleHeaderTextRed;
+ categoryHeaderMasked.Background.color = Palette.ImpostorRoleHeaderRed;
+ break;
+ default:
+ categoryHeaderMasked.Title.color = group.Color.Equals(MiraApiPlugin.DefaultHeaderColor) ? Color.white : group.Color.FindAlternateColor();
+ categoryHeaderMasked.Divider.color = group.Color;
+ categoryHeaderMasked.Background.color = group.Color;
+ break;
+ }
categoryHeaderMasked.Title.fontStyle = __instance.categoryHeaderEditRoleOrigin.Title.fontStyle;
categoryHeaderMasked.Title.font = __instance.categoryHeaderEditRoleOrigin.Title.font;
categoryHeaderMasked.Title.fontMaterial = __instance.categoryHeaderEditRoleOrigin.Title.fontMaterial;
categoryHeaderMasked.Divider.color = categoryHeaderMasked.Background.color;
+ categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.55f, -0.1833f, 0);
categoryHeaderMasked.Background.size = new Vector2(categoryHeaderMasked.Background.size.x + 1.5f, categoryHeaderMasked.Background.size.y);
+
categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f;
categoryHeaderMasked.transform.localPosition = new Vector3(-0.44f, ScrollerNum, -2f);
categoryHeaderMasked.gameObject.SetActive(true);
diff --git a/MiraAPI/Patches/Stubs/MinigameStubs.cs b/MiraAPI/Patches/Stubs/MinigameStubs.cs
index 3818d09..11e085e 100644
--- a/MiraAPI/Patches/Stubs/MinigameStubs.cs
+++ b/MiraAPI/Patches/Stubs/MinigameStubs.cs
@@ -33,4 +33,15 @@ public static class MinigameStubs
{
// nothing needed
}
+
+ ///
+ /// Fixes the error when creating custom minigames. Credit to Submerged for discovering this fix.
+ ///
+ /// The Minigame instance.
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(Minigame), nameof(Minigame.Begin))]
+ public static void MinigameErrorFixPatch(Minigame __instance)
+ {
+ __instance.logger ??= new Logger("Minigame", Logger.Level.Info, Logger.Category.Gameplay);
+ }
}
diff --git a/MiraAPI/Resources/CategoryHeader.png b/MiraAPI/Resources/CategoryHeader.png
new file mode 100644
index 0000000..3d2a4bf
Binary files /dev/null and b/MiraAPI/Resources/CategoryHeader.png differ
diff --git a/MiraAPI/Roles/CustomRoleConfiguration.cs b/MiraAPI/Roles/CustomRoleConfiguration.cs
index efaf444..cf714fd 100644
--- a/MiraAPI/Roles/CustomRoleConfiguration.cs
+++ b/MiraAPI/Roles/CustomRoleConfiguration.cs
@@ -1,6 +1,8 @@
using AmongUs.GameOptions;
+using Il2CppSystem.Web.Util;
using MiraAPI.Utilities.Assets;
using UnityEngine;
+using Helpers = MiraAPI.Utilities.Helpers;
namespace MiraAPI.Roles;
@@ -32,7 +34,6 @@ public record struct CustomRoleConfiguration
TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate;
HideSettings = roleBehaviour?.IsDead == true;
ShowInFreeplay = roleBehaviour?.IsDead == false;
- OptionsScreenshot = null;
}
///
@@ -58,12 +59,17 @@ public record struct CustomRoleConfiguration
///
/// Gets or sets the Sprite used for the Role Options menu screenshot.
///
- public LoadableAsset? OptionsScreenshot { get; set; }
+ public LoadableAsset? OptionsScreenshot { get; set; } = null;
///
/// Gets or sets the Sprite used for the Role Icon.
///
- public LoadableAsset Icon { get; set; } = MiraAssets.Empty;
+ public LoadableAsset? Icon { get; set; } = null;
+
+ ///
+ /// Gets or sets the Intro sound for the Role.
+ ///
+ public LoadableAsset? IntroSound { get; set; } = null;
///
/// Gets or sets a value indicating whether the role is affected by light affectors on Airship.
diff --git a/MiraAPI/Roles/CustomRoleManager.cs b/MiraAPI/Roles/CustomRoleManager.cs
index a778a09..0c2c4ad 100644
--- a/MiraAPI/Roles/CustomRoleManager.cs
+++ b/MiraAPI/Roles/CustomRoleManager.cs
@@ -13,6 +13,7 @@ using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
+using UnityEngine.TextCore;
using Object = UnityEngine.Object;
namespace MiraAPI.Roles;
@@ -93,6 +94,21 @@ public static class CustomRoleManager
roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount;
roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot?.LoadAsset();
+ if (customRole.Configuration.Icon != null)
+ {
+ var asset = customRole.Configuration.Icon.LoadAsset();
+ if (asset != null)
+ {
+ roleBehaviour.RoleIconSolid = asset;
+ roleBehaviour.RoleIconWhite = asset;
+ }
+ }
+
+ if (customRole.Configuration.IntroSound != null)
+ {
+ roleBehaviour.IntroSound = customRole.Configuration.IntroSound.LoadAsset();
+ }
+
if (roleBehaviour.IsDead)
{
RoleManager.GhostRoles.Add(roleBehaviour.Role);
diff --git a/MiraAPI/Roles/CustomRoleUtils.cs b/MiraAPI/Roles/CustomRoleUtils.cs
new file mode 100644
index 0000000..8f2432d
--- /dev/null
+++ b/MiraAPI/Roles/CustomRoleUtils.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using AmongUs.GameOptions;
+using UnityEngine;
+
+namespace MiraAPI.Roles;
+
+///
+/// Utilities to make handling roles in-game easier.
+///
+public static class CustomRoleUtils
+{
+ ///
+ /// Gets all active in-game roles.
+ ///
+ /// A list of roles.
+ // ReSharper disable once MemberCanBePrivate.Global
+ public static IEnumerable GetActiveRoles() => PlayerControl.AllPlayerControls.ToArray().Select(x => x.Data.Role);
+
+ ///
+ /// Gets all active in-game roles in a certain team.
+ ///
+ /// The team you would like to check for.
+ /// A list of roles with the team.
+ public static IEnumerable GetActiveRolesOfTeam(ModdedRoleTeams team) => GetActiveRoles().Where(x => x is ICustomRole customRole && customRole.Team == team);
+
+ ///
+ /// Gets all active in-game roles of a certain type.
+ ///
+ /// The role Type you would like to check for. Must be a RoleBehaviour.
+ /// A list of roles with that specific type.
+ public static IEnumerable GetActiveRolesOfType() where T : RoleBehaviour => GetActiveRoles().OfType();
+
+ ///
+ /// Creates a string builder for the Role Tab.
+ ///
+ /// The ICustomRole object.
+ /// A StringBuilder.
+ public static StringBuilder CreateForRole(ICustomRole role)
+ {
+ var taskStringBuilder = new StringBuilder();
+ taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}You are a {role.RoleName}.");
+ taskStringBuilder.Append("");
+ taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}");
+ return taskStringBuilder;
+ }
+}
diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs
index a1c0e5d..1784702 100644
--- a/MiraAPI/Roles/ICustomRole.cs
+++ b/MiraAPI/Roles/ICustomRole.cs
@@ -206,7 +206,7 @@ public interface ICustomRole : IOptionable
/// Get the custom Role Tab text for this role.
///
/// A StringBuilder with the role tab text.
- StringBuilder SetTabText() => Helpers.CreateForRole(this);
+ StringBuilder SetTabText() => CustomRoleUtils.CreateForRole(this);
///
/// Determine whether a given modifier can be applied to this role.
diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs
index 514bd25..d2e855f 100644
--- a/MiraAPI/Utilities/Assets/MiraAssets.cs
+++ b/MiraAPI/Utilities/Assets/MiraAssets.cs
@@ -10,6 +10,7 @@ public static class MiraAssets
public static readonly LoadableResourceAsset NextButtonActive = new("MiraAPI.Resources.NextButtonActive.png");
public static readonly LoadableResourceAsset Cog = new("MiraAPI.Resources.Cog.png");
public static readonly LoadableResourceAsset Checkmark = new("MiraAPI.Resources.Checkmark.png");
+ public static readonly LoadableResourceAsset CategoryHeader = new("MiraAPI.Resources.CategoryHeader.png");
public static readonly LoadableResourceAsset CheckmarkBox = new("MiraAPI.Resources.CheckMarkBox.png");
public static readonly Color32 AcceptedTeal = new(43, 233, 198, 255);
}
diff --git a/MiraAPI/Utilities/Assets/SpriteTools.cs b/MiraAPI/Utilities/Assets/SpriteTools.cs
index fa49f12..6858dcc 100644
--- a/MiraAPI/Utilities/Assets/SpriteTools.cs
+++ b/MiraAPI/Utilities/Assets/SpriteTools.cs
@@ -1,4 +1,5 @@
using System;
+using System.IO;
using System.Reflection;
using Reactor.Utilities.Extensions;
using UnityEngine;
@@ -30,6 +31,9 @@ public static class SpriteTools
throw new ArgumentException($"Resource not found: {resourcePath}");
}
- return Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit);
+ tex.name = resourcePath;
+ var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit);
+ sprite.name = resourcePath;
+ return sprite;
}
}
diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs
index b2f5c18..4139aee 100644
--- a/MiraAPI/Utilities/Extensions.cs
+++ b/MiraAPI/Utilities/Extensions.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
+using System.Text.RegularExpressions;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Networking;
@@ -40,6 +41,30 @@ public static class Extensions
BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value)));
}
+ ///
+ /// Gets a PlayerControl from their PlayerVoteArea in a meeting.
+ ///
+ /// The vote area.
+ /// The player's PlayerControl.
+ public static PlayerControl? GetPlayer(this PlayerVoteArea state) => GameData.Instance.GetPlayerById(state.TargetPlayerId)?.Object;
+
+ ///
+ /// Gets an int representing the amount of tasks a player has left.
+ ///
+ /// The player.
+ /// A count of how many tasks the player has left.
+ public static int GetTasksLeft(this PlayerControl player) => player.Data.Tasks.ToArray().Count(x => !x.Complete);
+
+ ///
+ /// Checks if a PlayerControl is the game's host.
+ ///
+ /// The player you're checking for.
+ /// If the player is the host, true, else false.
+ public static bool IsHost(this PlayerControl playerControl)
+ {
+ return TutorialManager.InstanceExists || AmongUsClient.Instance.HostId == playerControl.OwnerId;
+ }
+
///
/// Determines if a float is an integer.
///
@@ -131,6 +156,17 @@ public static class Extensions
.FirstOrDefault();
}
+ ///
+ /// Gets a proper string for an enum. (with spaces).
+ ///
+ /// The enum you would like to change.
+ /// A proper string for the enum.
+ public static string ToDisplayString(this Enum @enum)
+ {
+ var regex = new Regex(@"([^\^])([A-Z][a-z$])");
+ return regex.Replace(@enum.ToString(), m => $"{m.Groups[1].Value} {m.Groups[2].Value}");
+ }
+
///
/// Gets the constructors of a type that match the specified arguments.
///
diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs
index 6b3b79f..a54b6cf 100644
--- a/MiraAPI/Utilities/Helpers.cs
+++ b/MiraAPI/Utilities/Helpers.cs
@@ -1,11 +1,17 @@
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Text;
+using AmongUs.GameOptions;
+using HarmonyLib;
using MiraAPI.Roles;
+using Reactor.Utilities;
+using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
+using UnityEngine.TextCore;
using Object = UnityEngine.Object;
namespace MiraAPI.Utilities;
@@ -24,6 +30,123 @@ public static class Helpers
return PlayerControl.AllPlayerControls.ToArray().Where(x => !x.Data.IsDead).ToList();
}
+ ///
+ /// Determines whether a given probability check succeeds.
+ ///
+ /// An integer value representing the success probability (0-100).
+ /// True if the number falls in the range, false if not.
+ public static bool CheckChance(int probability)
+ {
+ switch (probability)
+ {
+ case 0:
+ return false;
+ case 100:
+ return true;
+ default:
+ {
+ var num = Random.RandomRangeInt(1, 101);
+ return num <= probability;
+ }
+ }
+ }
+
+ ///
+ /// Retrieves a vent from the ID.
+ ///
+ /// The vent ID.
+ /// The vent.
+ public static Vent? GetVentById(int id)
+ {
+ return ShipStatus.Instance.AllVents.FirstOrDefault(vent => vent.Id == id);
+ }
+
+ ///
+ /// Creates an arrow.
+ ///
+ /// The arrow gameObject's parent.
+ /// The color of the arrow.
+ /// The created ArrowBehaviour.
+ public static ArrowBehaviour CreateArrow(Transform parent, Color color)
+ {
+ var prefab = Object.FindObjectOfType(true);
+ var arrow = Object.Instantiate(prefab, parent);
+ arrow.image = arrow.gameObject.GetComponent();
+ arrow.image.color = color;
+ arrow.gameObject.layer = 5;
+ arrow.gameObject.SetActive(true);
+ return arrow;
+ }
+
+ ///
+ /// Get the closest object of a specific type.
+ ///
+ /// A list of all the objects you'd like to check the distance for.
+ /// The position of where you want to check from. For example: PlayerControl.LocalPlayer.transform.position.
+ /// The object type.
+ /// The closest object.
+ public static T? FindClosestObjectOfType(List objectList, Vector3 position) where T : MonoBehaviour
+ {
+ T? closest = null;
+ var closestDistanceSqr = Mathf.Infinity;
+
+ foreach (var obj in objectList)
+ {
+ if (obj == null)
+ {
+ continue;
+ }
+
+ var sqrDistance = (obj.transform.position - position).sqrMagnitude;
+ if (sqrDistance < closestDistanceSqr)
+ {
+ closestDistanceSqr = sqrDistance;
+ closest = obj;
+ }
+ }
+
+ return closest;
+ }
+
+ ///
+ /// Creates and shows a notification.
+ ///
+ /// The text you want to display.
+ /// The color of the text and image.
+ /// The sound you want to play with the notification.
+ /// The sprite beside the notification.
+ /// The created notification.
+ public static LobbyNotificationMessage CreateAndShowNotification(string text, Color color, AudioClip? clip = null, Sprite? spr = null)
+ {
+ return CreateAndShowNotification(text, color, new Vector3(0f, 0f, -2f), clip, spr);
+ }
+
+ ///
+ /// Creates and shows a notification.
+ ///
+ /// The text you want to display.
+ /// The color of the text and image.
+ /// The position of the notification.
+ /// The sound you want to play with the notification.
+ /// The sprite beside the notification.
+ /// The created notification.
+ public static LobbyNotificationMessage CreateAndShowNotification(string text, Color color, Vector3 localPos, AudioClip? clip = null, Sprite? spr = null)
+ {
+ var popper = HudManager.Instance.Notifier;
+ var newMessage = Object.Instantiate(popper.notificationMessageOrigin, Vector3.zero, Quaternion.identity, popper.transform);
+ newMessage.transform.localPosition = localPos;
+ newMessage.SetUp(text, spr ?? null, color, new System.Action(() => popper.OnMessageDestroy(newMessage)));
+ popper.lastMessageKey = -1;
+ popper.ShiftMessages();
+ popper.AddMessageToQueue(newMessage);
+
+ if (clip == null) return newMessage;
+ SoundManager.Instance.StopSound(clip);
+ SoundManager.Instance.PlaySound(clip, false, 2f);
+
+ return newMessage;
+ }
+
///
/// Returns an empty coroutine.
///
@@ -265,16 +388,24 @@ public static class Helpers
}
///
- /// Creates a string builder for the Role Tab.
+ /// Converts the first letter of a string to uppercase.
///
- /// The ICustomRole object.
- /// A StringBuilder.
- public static StringBuilder CreateForRole(ICustomRole role)
+ /// The string.
+ /// The fixed string.
+ public static string FirstLetterToUpper(string str)
{
- var taskStringBuilder = new StringBuilder();
- taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}You are a {role.RoleName}.");
- taskStringBuilder.Append("");
- taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}");
- return taskStringBuilder;
+ return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower(CultureInfo.InvariantCulture));
+ }
+
+ ///
+ /// Gets a random string based on characters.
+ ///
+ /// The length of the string.
+ /// The characters in the random string.
+ /// The random string.
+ public static string RandomString(int length, string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+ {
+ return new string(Enumerable.Repeat(chars, length)
+ .Select(s => s[Random.RandomRangeInt(0, s.Length)]).ToArray());
}
}