diff --git a/MiraAPI.Example/Modifiers/CaptainModifier.cs b/MiraAPI.Example/Modifiers/CaptainModifier.cs index 63b7c41..5845e5a 100644 --- a/MiraAPI.Example/Modifiers/CaptainModifier.cs +++ b/MiraAPI.Example/Modifiers/CaptainModifier.cs @@ -5,14 +5,4 @@ namespace MiraAPI.Example.Modifiers; public class CaptainModifier : GameModifier { public override string ModifierName => "Captain"; - - public override int GetAmountPerGame() - { - return 1; - } - - public override int GetAssignmentChance() - { - return 100; - } } diff --git a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs index c640ed8..1d1ddd5 100644 --- a/MiraAPI.Example/Modifiers/HighPriorityModifier.cs +++ b/MiraAPI.Example/Modifiers/HighPriorityModifier.cs @@ -5,15 +5,6 @@ namespace MiraAPI.Example.Modifiers; public class HighPriorityModifier : GameModifier { public override string ModifierName => "High Priority"; - public override int GetAssignmentChance() - { - return 100; - } - - public override int GetAmountPerGame() - { - return 1; - } public override int Priority() { diff --git a/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs new file mode 100644 index 0000000..2ae297b --- /dev/null +++ b/MiraAPI.Example/Options/Modifiers/CaptainModifierSettings.cs @@ -0,0 +1,13 @@ +using MiraAPI.Example.Modifiers; +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; + +namespace MiraAPI.Example.Options.Modifiers; + +public class CaptainModifierSettings : AbstractOptionGroup +{ + public override string GroupName => "Captain Settings"; + + [ModdedNumberOption("Uses", 1, 5)] + public float NumUses { get; set; } = 3; +} diff --git a/MiraAPI/GameOptions/ModifierOptionGroup.cs b/MiraAPI/GameOptions/ModifierOptionGroup.cs new file mode 100644 index 0000000..f845952 --- /dev/null +++ b/MiraAPI/GameOptions/ModifierOptionGroup.cs @@ -0,0 +1,15 @@ +using System.Linq; + +namespace MiraAPI.GameOptions; + +internal class ModifierOptionGroup : AbstractOptionGroup +{ + public override string GroupName { get; } + + public ModifierOptionGroup(string name, IModdedOption[] options, params AbstractOptionGroup[] others) + { + GroupName = name; + Options.AddRange(options); + Options.AddRange(others.SelectMany(x=>x.Options)); + } +} diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs index 0174326..4d38c04 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedNumberOption.cs @@ -75,11 +75,11 @@ public class ModdedNumberOption : ModdedOption data.Increment = Increment; data.ValidRange = new FloatRange(Min, Max); data.FormatString = formatString ?? - (!Mathf.Approximately(defaultValue, 0) || - !Mathf.Approximately(Increment % 1, 0) || - !Mathf.Approximately(Value % 1, 0) || - !Mathf.Approximately(Min % 1, 0) || - !Mathf.Approximately(Max % 1, 0) ? "0.0" : "0"); + (defaultValue.IsInteger() && + Increment.IsInteger() && + Value.IsInteger() && + Min.IsInteger() && + Max.IsInteger() ? "0" : "0.0"); data.ZeroIsInfinity = ZeroInfinity; data.SuffixType = (NumberSuffixes)SuffixType; diff --git a/MiraAPI/Modifiers/AssignmentConfiguration.cs b/MiraAPI/Modifiers/AssignmentConfiguration.cs new file mode 100644 index 0000000..fd4f3ba --- /dev/null +++ b/MiraAPI/Modifiers/AssignmentConfiguration.cs @@ -0,0 +1,10 @@ +namespace MiraAPI.Modifiers; + +/// +/// Represents the assignment configuration for a game modifier. +/// +/// The default amount of players that can have this modifier in a game. +/// The default chance of the modifier being assigned to a player. +/// Whether Mira should create an amount option for the modifier. +/// Whether Mira should create a chance option for the modifier. +public record struct AssignmentConfiguration(int DefaultAmount, int DefaultChance, bool CreateAmountOption, bool CreateChanceOption); diff --git a/MiraAPI/Modifiers/BaseModifier.cs b/MiraAPI/Modifiers/BaseModifier.cs index 473c78f..1bd1082 100644 --- a/MiraAPI/Modifiers/BaseModifier.cs +++ b/MiraAPI/Modifiers/BaseModifier.cs @@ -1,11 +1,13 @@ using System; +using MiraAPI.GameOptions; +using MiraAPI.PluginLoading; namespace MiraAPI.Modifiers; /// /// Base class for all modifiers. /// -public abstract class BaseModifier +public abstract class BaseModifier : IOptionable { /// /// Gets the player that the modifier is attached to. @@ -32,6 +34,16 @@ public abstract class BaseModifier /// public uint TypeId => ModifierManager.GetModifierTypeId(GetType()) ?? throw new InvalidOperationException("Modifier is not registered."); + /// + /// Gets the parent mod of the modifier. + /// + public MiraPluginInfo ParentMod => Array.Find( + MiraPluginManager.Instance.RegisteredPlugins(), + x => x.Modifiers.Exists(y => y.TypeId == TypeId) + ) ?? throw new InvalidOperationException("Modifier is not registered."); + + internal ModifierOptionGroup? ModifierOptionsGroup { get; set; } + /// /// Gets the modifier name. /// diff --git a/MiraAPI/Modifiers/ModifierManager.cs b/MiraAPI/Modifiers/ModifierManager.cs index 4285a73..fee96ab 100644 --- a/MiraAPI/Modifiers/ModifierManager.cs +++ b/MiraAPI/Modifiers/ModifierManager.cs @@ -71,7 +71,23 @@ public static class ModifierManager return true; } - var mod = Activator.CreateInstance(modifierType) as GameModifier; + if (modifierType.GetConstructor(Type.EmptyTypes) == null) + { + Logger.Error($"Modifier {modifierType.FullName} does not have a parameterless constructor!"); + return false; + } + + if (Activator.CreateInstance(modifierType) is not GameModifier mod) + { + Logger.Error($"Modifier {modifierType.FullName} could not be cast to GameModifier!"); + return false; + } + + info.GameModifiers.Add(mod); + + info.PluginConfig.Bind(mod.AmountDefinition, 0); + info.PluginConfig.Bind(mod.ChanceDefinition, 0); + var priority = mod!.Priority(); if (!PrioritiesToIdsMap.TryGetValue(priority, out var list)) diff --git a/MiraAPI/Modifiers/Types/GameModifier.cs b/MiraAPI/Modifiers/Types/GameModifier.cs index abcd1f7..7e4cccb 100644 --- a/MiraAPI/Modifiers/Types/GameModifier.cs +++ b/MiraAPI/Modifiers/Types/GameModifier.cs @@ -1,4 +1,10 @@ -namespace MiraAPI.Modifiers.Types; +using System; +using BepInEx.Configuration; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; +using Reactor.Utilities; + +namespace MiraAPI.Modifiers.Types; /// /// The base class for a game modifier. Game modifiers are applied at the start of the game on top of the player's role. @@ -8,17 +14,72 @@ public abstract class GameModifier : BaseModifier /// public override bool ShowInFreeplay => true; + /// + /// Gets the assignment configuration for the modifier. + /// + public virtual AssignmentConfiguration AssignmentConfiguration => new(0, 0, true, true); + + internal ConfigDefinition AmountDefinition => new("Modifiers", $"Amount {GetType().FullName}"); + + internal ConfigDefinition ChanceDefinition => new("Modifiers", $"Chance {GetType().FullName}"); + + internal ModdedNumberOption AmountOption { get; } = new("Amount", 0, 0, 15, 1, MiraNumberSuffixes.None); + + internal ModdedNumberOption ChanceOption { get; } = new("Chance", 0, 0, 100, 1, MiraNumberSuffixes.Percent); + /// /// Gets the chance of the modifier being assigned to a player. /// /// An int value between 0 and 100 representing percent. - public abstract int GetAssignmentChance(); + public virtual int GetAssignmentChance() + { + return AssignmentConfiguration.CreateChanceOption && + ParentMod.PluginConfig.TryGetEntry(ChanceDefinition, out ConfigEntry entry) + ? entry.Value + : AssignmentConfiguration.DefaultChance; + } /// /// Gets the amount of players that can have this modifier in a game. /// /// An int value greater than or equal to zero. - public abstract int GetAmountPerGame(); + public virtual int GetAmountPerGame() + { + return AssignmentConfiguration.CreateAmountOption && + ParentMod.PluginConfig.TryGetEntry(AmountDefinition, out ConfigEntry entry) + ? entry.Value + : AssignmentConfiguration.DefaultAmount; + } + + /// + /// Sets the chance of the modifier being assigned to a player. + /// + /// The chance between 0 and 100. + public virtual void SetAssignmentChance(int chance) + { + if (ParentMod.PluginConfig.TryGetEntry(ChanceDefinition, out ConfigEntry entry)) + { + entry.Value = Math.Clamp(chance, 0, 100); + return; + } + + Logger.Error($"Error getting chance configuration for modifier: {ModifierName}"); + } + + /// + /// Sets the amount of players that can have this modifier in a game. + /// + /// The amount of players between zero and the max player count. + public virtual void SetAmountPerGame(int amount) + { + if (ParentMod.PluginConfig.TryGetEntry(AmountDefinition, out ConfigEntry entry)) + { + entry.Value = Math.Max(amount, 0); + return; + } + + Logger.Error($"Error getting amount configuration for modifier: {ModifierName}"); + } /// /// Gets the priority at which the modifier will spawn. The higher the value, the higher up on the assignment list. diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index c0aef14..7b24619 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -1,14 +1,18 @@ -using System.Linq; +using System; +using System.Linq; using HarmonyLib; using MiraAPI.GameOptions; +using MiraAPI.Modifiers.Types; using MiraAPI.Roles; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Localization.Utilities; +using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.Events; +using Object = UnityEngine.Object; namespace MiraAPI.Patches.Options; @@ -32,64 +36,105 @@ public static class GameOptionsMenuPatch } var num = 2.1f; - var filteredGroups = - GameSettingMenuPatches.SelectedMod?.OptionGroups - .Where(x=> x is not IOptionableGroup) ?? []; - - foreach (var group in filteredGroups) + if (__instance.name == "MODIFIERS TAB") { - if (group.Options.Count == 0 || group.Header is null) + ModifiersUpdate(ref num); + } + else + { + var filteredGroups = + GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x is not IOptionableGroup) ?? []; + + foreach (var group in filteredGroups) { - continue; - } - - if (!group.GroupVisible.Invoke()) - { - group.Header.gameObject.SetActive(false); - foreach (var option in group.Options) - { - option.OptionBehaviour?.gameObject.SetActive(false); - } - continue; - } - - group.Header.gameObject.SetActive(true); - group.Header.transform.localScale = Vector3.one * 0.63f; - group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); - - num -= 0.58f; - - foreach (var opt in group.Options) - { - var newOpt = opt.OptionBehaviour; - - if (newOpt is null) - { - continue; - } - - if (!opt.Visible.Invoke()) - { - newOpt.gameObject.SetActive(false); - continue; - } - - if (!group.AllOptionsHidden) - { - newOpt.gameObject.SetActive(true); - newOpt.transform.localPosition = new Vector3(0.952f, num, -2f); - num -= 0.45f; - } - else - { - newOpt.gameObject.SetActive(false); - } + UpdateGroup(group, ref num); } } __instance.scrollBar.SetYBoundsMax(-num - 1.65f); } + private static void UpdateGroup(AbstractOptionGroup group, ref float num) + { + if (group.Options.Count == 0 || group.Header is null) + { + return; + } + + if (!group.GroupVisible.Invoke()) + { + group.Header.gameObject.SetActive(false); + foreach (var option in group.Options) + { + option.OptionBehaviour?.gameObject.SetActive(false); + } + + return; + } + + group.Header.gameObject.SetActive(true); + group.Header.transform.localScale = Vector3.one * 0.63f; + group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f); + + num -= 0.58f; + + foreach (var opt in group.Options) + { + var newOpt = opt.OptionBehaviour; + + if (newOpt is null) + { + continue; + } + + if (!opt.Visible.Invoke()) + { + newOpt.gameObject.SetActive(false); + continue; + } + + if (!group.AllOptionsHidden) + { + newOpt.gameObject.SetActive(true); + newOpt.transform.localPosition = new Vector3(0.952f, num, -2f); + num -= 0.45f; + } + else + { + newOpt.gameObject.SetActive(false); + } + } + } + + private static void ModifiersUpdate(ref float num) + { + var modifiers = GameSettingMenuPatches.SelectedMod?.Modifiers.Select(x=>x.ModifierOptionsGroup) ?? []; + + foreach (var modGroup in modifiers) + { + if (modGroup != null) + { + UpdateGroup(modGroup, ref num); + } + } + } + + private static void ModifiersCreate(GameOptionsMenu menu) + { + var modifiers = GameSettingMenuPatches.SelectedMod?.GameModifiers ?? []; + + var optLookup = GameSettingMenuPatches.SelectedMod?.OptionGroups + .Where(x => x is IOptionableGroup group && group.OptionableType.IsAssignableTo(typeof(GameModifier))) + .ToLookup(x => (x as IOptionableGroup)!.OptionableType); + + foreach (var mod in modifiers) + { + mod.ModifierOptionsGroup = new ModifierOptionGroup(mod.ModifierName, [mod.AmountOption, mod.ChanceOption], [.. optLookup?[mod.GetType()] ?? []]); + CreateGroup(menu, mod.ModifierOptionsGroup); + } + } + [HarmonyPrefix] [HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))] public static bool SettingsPatch(GameOptionsMenu __instance) @@ -101,107 +146,19 @@ public static class GameOptionsMenuPatch __instance.MapPicker.gameObject.SetActive(false); + if (__instance.name == "MODIFIERS TAB") + { + Logger.Error("Creating modifiers"); + ModifiersCreate(__instance); + return false; + } + var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups .Where(x => x is not IOptionableGroup) ?? []; foreach (var group in filteredGroups) { - var categoryHeaderMasked = Object.Instantiate( - __instance.categoryHeaderOrigin, - Vector3.zero, - Quaternion.identity, - __instance.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.GetAlternateColor(); - } - - categoryHeaderMasked.Background.size = new Vector2( - categoryHeaderMasked.Background.size.x + 1.5f, - categoryHeaderMasked.Background.size.y); - categoryHeaderMasked.gameObject.SetActive(false); - group.Header = categoryHeaderMasked; - - var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); - newText.text = "(Click to close)"; - newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); - newText.gameObject.GetComponent().Destroy(); - - var options = group.Options.Select( - opt => opt.CreateOption( - __instance.checkboxOrigin, - __instance.numberOptionOrigin, - __instance.stringOptionOrigin, - __instance.settingsContainer)); - - foreach (var newOpt in options) - { - newOpt.SetClickMask(__instance.ButtonClickMask); - - SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); - foreach (var renderer in componentsInChildren) - { - if (group.GroupColor != Color.clear) - { - renderer.color = group.GroupColor.GetAlternateColor(); - if (renderer.transform.parent.TryGetComponent(out var btn)) - { - btn.interactableColor = group.GroupColor.GetAlternateColor(); - btn.interactableHoveredColor = Color.white; - } - } - - renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); - } - - foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) - { - if (group.GroupColor != Color.clear) - { - textMeshPro.color = group.GroupColor; - } - - textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); - textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); - } - - if (newOpt is ToggleOption toggle) - { - toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); - toggle.CheckMark.color = - group.GroupColor != Color.clear ? 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; - } - - __instance.Children.Add(newOpt); - - newOpt.Initialize(); - newOpt.gameObject.SetActive(false); - } - - var boxCol = categoryHeaderMasked.gameObject.AddComponent(); - boxCol.size = new Vector2(7, 0.7f); - boxCol.offset = new Vector2(1.5f, -0.3f); - - var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); - headerBtn.ClickSound = __instance.BackButton.GetComponent().ClickSound; - headerBtn.OnMouseOver = new UnityEvent(); - headerBtn.OnMouseOut = new UnityEvent(); - headerBtn.OnClick.AddListener( - (UnityAction)(() => - { - group.AllOptionsHidden = !group.AllOptionsHidden; - newText.text = group.AllOptionsHidden - ? "(Click to open)" - : "(Click to close)"; - })); - headerBtn.SetButtonEnableState(true); + CreateGroup(__instance, group); } return false; @@ -241,4 +198,105 @@ public static class GameOptionsMenuPatch return false; } + + private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group) + { + var categoryHeaderMasked = Object.Instantiate( + menu.categoryHeaderOrigin, + Vector3.zero, + Quaternion.identity, + 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.GetAlternateColor(); + } + + categoryHeaderMasked.Background.size = new Vector2( + categoryHeaderMasked.Background.size.x + 1.5f, + categoryHeaderMasked.Background.size.y); + categoryHeaderMasked.gameObject.SetActive(false); + group.Header = categoryHeaderMasked; + + var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform); + newText.text = "(Click to close)"; + newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f); + newText.gameObject.GetComponent().Destroy(); + + var options = group.Options.Select( + opt => opt.CreateOption( + menu.checkboxOrigin, + menu.numberOptionOrigin, + menu.stringOptionOrigin, + menu.settingsContainer)); + + foreach (var newOpt in options) + { + newOpt.SetClickMask(menu.ButtonClickMask); + + SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren(true); + foreach (var renderer in componentsInChildren) + { + if (group.GroupColor != Color.clear) + { + renderer.color = group.GroupColor.GetAlternateColor(); + if (renderer.transform.parent.TryGetComponent(out var btn)) + { + btn.interactableColor = group.GroupColor.GetAlternateColor(); + btn.interactableHoveredColor = Color.white; + } + } + + renderer.material.SetInt(PlayerMaterial.MaskLayer, 20); + } + + foreach (var textMeshPro in newOpt.GetComponentsInChildren(true)) + { + if (group.GroupColor != Color.clear) + { + textMeshPro.color = group.GroupColor; + } + + textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f); + textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20); + } + + if (newOpt is ToggleOption toggle) + { + toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset(); + toggle.CheckMark.color = + group.GroupColor != Color.clear ? 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; + } + + menu.Children.Add(newOpt); + + newOpt.Initialize(); + newOpt.gameObject.SetActive(false); + } + + var boxCol = categoryHeaderMasked.gameObject.AddComponent(); + boxCol.size = new Vector2(7, 0.7f); + boxCol.offset = new Vector2(1.5f, -0.3f); + + var headerBtn = categoryHeaderMasked.gameObject.AddComponent(); + headerBtn.ClickSound = menu.BackButton.GetComponent().ClickSound; + headerBtn.OnMouseOver = new UnityEvent(); + headerBtn.OnMouseOut = new UnityEvent(); + headerBtn.OnClick.AddListener( + (UnityAction)(() => + { + group.AllOptionsHidden = !group.AllOptionsHidden; + newText.text = group.AllOptionsHidden + ? "(Click to open)" + : "(Click to close)"; + })); + headerBtn.SetButtonEnableState(true); + } } diff --git a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs index 4f87780..ab3ead7 100644 --- a/MiraAPI/Patches/Options/GameSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/GameSettingMenuPatches.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using HarmonyLib; using MiraAPI.PluginLoading; using MiraAPI.Utilities.Assets; @@ -12,26 +11,41 @@ using Object = UnityEngine.Object; namespace MiraAPI.Patches.Options; -/// -/// Patches for the to add support for custom options. -/// [HarmonyPatch(typeof(GameSettingMenu))] -public static class GameSettingMenuPatches +internal static class GameSettingMenuPatches { - /// - /// Gets the currently selected mod index. - /// public static int SelectedModIdx { get; private set; } - /// - /// Gets the currently selected mod. - /// public static MiraPluginInfo? SelectedMod { get; private set; } private static TextMeshPro? _text; private static Vector3 _roleBtnOgPos; + private static GameOptionsMenu? _modifiersTab; + private static PassiveButton? _modifiersButton; + private static PassiveButton? _smallRolesButton; + + [HarmonyPostfix] + [HarmonyPatch(nameof(GameSettingMenu.ChangeTab))] + public static void ChangeTabPostfix(GameSettingMenu __instance, int tabNum, bool previewOnly) + { + if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly) + { + _modifiersTab?.gameObject.SetActive(tabNum == 3); + _modifiersButton?.SelectButton(tabNum == 3); + _smallRolesButton?.SelectButton(tabNum == 2); + } + + if (previewOnly) + { + return; + } + + _modifiersButton?.SelectButton(tabNum == 3); + _smallRolesButton?.SelectButton(tabNum == 2); + } + /// /// Prefix for the method. Sets up the custom options. /// @@ -54,8 +68,6 @@ public static class GameSettingMenuPatches _text.fontSizeMax = 3.2f; _text.overflowMode = TextOverflowModes.Overflow; - UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); - _text.alignment = TextAlignmentOptions.Center; var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject; @@ -91,6 +103,61 @@ public static class GameSettingMenuPatches } UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); })); + + // clone game settings tab for modifiers + _modifiersTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent); + _modifiersTab.name = "MODIFIERS TAB"; + + // create button for modifiers + __instance.RoleSettingsButton.gameObject.SetActive(false); + _smallRolesButton = Object.Instantiate(__instance.RoleSettingsButton, __instance.RoleSettingsButton.transform.parent); + var pos = new Vector3(-3.65f, _smallRolesButton.transform.localPosition.y, _smallRolesButton.transform.localPosition.z); + _smallRolesButton.transform.localPosition = pos; + _smallRolesButton.OnClick.AddListener((UnityAction)(() => + { + __instance.ChangeTab(2, false); + })); + _smallRolesButton.OnMouseOver.AddListener((UnityAction)(() => + { + __instance.ChangeTab(2, true); + })); + + var roleText = _smallRolesButton.buttonText; + roleText.text = "Roles"; + roleText.GetComponent().Destroy(); + roleText.alignment = TextAlignmentOptions.Center; + roleText.transform.parent.localPosition = new Vector3(-.525f, roleText.transform.parent.localPosition.y, roleText.transform.parent.localPosition.z); + + foreach (var collider in _smallRolesButton.Colliders) + { + if (collider.TryCast() is { } col) + { + col.size = new Vector2(col.size.x / 2, col.size.y); + } + } + foreach (var rend in _smallRolesButton.GetComponentsInChildren(true)) + { + rend.size = new Vector2(rend.size.x / 2, rend.size.y); + } + + _modifiersButton = Object.Instantiate(_smallRolesButton, _smallRolesButton.transform.parent); + _modifiersButton.OnClick = new ButtonClickedEvent(); + _modifiersButton.OnClick.AddListener((UnityAction)(() => + { + __instance.ChangeTab(3, false); + })); + _modifiersButton.OnMouseOver = new UnityEvent(); + _modifiersButton.OnMouseOver.AddListener((UnityAction)(() => + { + __instance.ChangeTab(3, true); + })); + + _modifiersButton.buttonText.text = "Modifiers"; + pos.x = -2.27f; + _modifiersButton.transform.localPosition = pos; + _modifiersButton.name = "ModifiersButton"; + + UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab); } private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles) @@ -109,9 +176,26 @@ public static class GameSettingMenuPatches _text.text = name[..Math.Min(name.Length, 25)]; } - menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos; - menu.GameSettingsButton.gameObject.SetActive(true); - menu.RoleSettingsButton.gameObject.SetActive(true); + _modifiersButton.gameObject.SetActive(false); + _smallRolesButton.gameObject.SetActive(false); + menu.RoleSettingsButton.gameObject.SetActive(false); + if (SelectedModIdx != 0 && SelectedMod?.GameModifiers.Count != 0) + { + _modifiersButton.gameObject.SetActive(true); + _smallRolesButton.gameObject.SetActive(true); + } + else + { + menu.RoleSettingsButton.gameObject.SetActive(true); + if (SelectedModIdx == 0 && _modifiersTab.gameObject.active) + { + menu.ChangeTab(0, false); + } + } + + //menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos; + //menu.GameSettingsButton.gameObject.SetActive(true); + //menu.RoleSettingsButton.gameObject.SetActive(true); // TODO: reimplement this /* diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 20ffa18..13c190f 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -504,7 +504,7 @@ public static class RoleSettingMenuPatches if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1) { - ScrollerNum += -0.43f; + ScrollerNum -= 0.43f; } return roleOptionSetting; diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 68d3ff0..2dfe479 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -6,6 +6,7 @@ using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Hud; using MiraAPI.Modifiers; +using MiraAPI.Modifiers.Types; namespace MiraAPI.PluginLoading; @@ -73,6 +74,8 @@ public class MiraPluginInfo internal List Modifiers { get; } = []; + internal List GameModifiers { get; } = []; + internal Dictionary CustomRoles { get; } = []; internal Dictionary GameModes { get; } = []; diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index ecb4326..27df3cd 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -47,7 +47,7 @@ public interface ICustomRole : IOptionable /// /// Gets the role options group. /// - public RoleOptionsGroup RoleOptionsGroup => Team switch + RoleOptionsGroup RoleOptionsGroup => Team switch { ModdedRoleTeams.Crewmate => RoleOptionsGroup.Crewmate, ModdedRoleTeams.Impostor => RoleOptionsGroup.Impostor, @@ -58,7 +58,7 @@ public interface ICustomRole : IOptionable /// /// Gets the role's TeamIntroCutscene configuration. /// - public TeamIntroConfiguration? IntroConfiguration => Team switch + TeamIntroConfiguration? IntroConfiguration => Team switch { ModdedRoleTeams.Custom => TeamIntroConfiguration.Neutral, _ => null, diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 0a4fee3..a85da1d 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -39,6 +39,16 @@ public static class Extensions BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value))); } + /// + /// Determines if a float is an integer. + /// + /// The float number. + /// True if the float is an integer, false otherwise. + public static bool IsInteger(this float number) + { + return Mathf.Approximately(number, Mathf.Round(number)); + } + /// /// Gets the best constructor for a type based on the specified arguments. ///