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