mirror of
https://github.com/All-Of-Us-Mods/MiraAPI.git
synced 2026-10-20 01:46:15 +00:00
Merge remote-tracking branch 'origin/dev' into cleanup
This commit is contained in:
7 files changed
+992
-91
No files matched your search
@@ -0,0 +1,218 @@
|
|||||||
|
using MiraAPI.Patches.Stubs;
|
||||||
|
using MiraAPI.Utilities.Assets;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
|
||||||
|
namespace MiraAPI.Hud;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multi-select <see cref="CustomPhoneMenu"/> using the <see cref="ShapeshifterPanel"/> as a base.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEntry">The type of object each entry represents.</typeparam>
|
||||||
|
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity convention.")]
|
||||||
|
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
|
||||||
|
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")]
|
||||||
|
public abstract class CustomMultiSelectMenu<TEntry> : CustomPhoneMenu<CustomMultiSelectMenu<TEntry>.MenuEntry> where TEntry : class
|
||||||
|
{
|
||||||
|
private int totalSelections;
|
||||||
|
private bool shouldConfirm;
|
||||||
|
private bool canRepeat;
|
||||||
|
private readonly List<MenuEntry> selectedEntries = [];
|
||||||
|
|
||||||
|
private LoadableAsset<Sprite>? hoverSelectSprite;
|
||||||
|
private LoadableAsset<Sprite>? hoverDeselectSprite;
|
||||||
|
private Color? activeColor;
|
||||||
|
private Color? hoverSelectColor;
|
||||||
|
private Color? hoverDeselectColor;
|
||||||
|
|
||||||
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
[SuppressMessage("Minor Code Smell", "S1104:Fields should not have public accessibility", Justification = "Unity convention.")]
|
||||||
|
public UiElement confirmButton;
|
||||||
|
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
|
||||||
|
private Action<List<TEntry>>? onSelection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Menu Entry used when multiple entries are at play.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Panel">The panel of the entry.</param>
|
||||||
|
/// <param name="Entry">The entry itself.</param>
|
||||||
|
public record MenuEntry(ShapeshifterPanel Panel, TEntry Entry) : IMenuEntry;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <typeparamref name="TMenu"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TMenu">The type of <see cref="CustomMultiSelectMenu{TEntry}"/>.</typeparam>
|
||||||
|
/// <param name="activeColor">The <see cref="Color"/> to use when an entry is selected but not hovered over.</param>
|
||||||
|
/// <param name="hoverSelectSprite">The <see cref="Sprite"/> to use when an entry is hovered over while not selected.</param>
|
||||||
|
/// <param name="hoverSelectColor">The <see cref="Color"/> to use when an entry is hovered over while not selected.</param>
|
||||||
|
/// <param name="hoverDeselectSprite">The <see cref="Sprite"/> to use when an entry is hovered over while selected.</param>
|
||||||
|
/// <param name="hoverDeselectColor">The <see cref="Color"/> to use when an entry is hovered over while selected.</param>
|
||||||
|
/// <param name="onMouseOut">Function that can optionally be run when the mouse is moved outside a menu panel.</param>
|
||||||
|
/// <param name="onMouseOver">Function that can optionally be run when the mouse is moved over a menu panel.</param>
|
||||||
|
/// <returns>New <typeparamref name="TMenu"/> object.</returns>
|
||||||
|
protected static TMenu Create<TMenu>(
|
||||||
|
Color? activeColor = null,
|
||||||
|
LoadableAsset<Sprite>? hoverSelectSprite = null,
|
||||||
|
Color? hoverSelectColor = null,
|
||||||
|
LoadableAsset<Sprite>? hoverDeselectSprite = null,
|
||||||
|
Color? hoverDeselectColor = null,
|
||||||
|
PanelButtonOnMouse? onMouseOut = null,
|
||||||
|
PanelButtonOnMouse? onMouseOver = null
|
||||||
|
) where TMenu : CustomMultiSelectMenu<TEntry>, new()
|
||||||
|
{
|
||||||
|
TMenu customMenu = Create<TMenu>(onMouseOut, onMouseOver);
|
||||||
|
|
||||||
|
customMenu.confirmButton = null!; // TODO: create/add confirm button
|
||||||
|
|
||||||
|
var button = customMenu.confirmButton.GetComponent<PassiveButton>();
|
||||||
|
button.OnClick.RemoveAllListeners();
|
||||||
|
button.OnClick.AddListener((UnityAction)customMenu.OnCompleteSelection);
|
||||||
|
|
||||||
|
customMenu.activeColor = activeColor;
|
||||||
|
|
||||||
|
customMenu.hoverSelectSprite = hoverSelectSprite;
|
||||||
|
customMenu.hoverSelectColor = hoverSelectColor;
|
||||||
|
|
||||||
|
customMenu.hoverDeselectSprite = hoverDeselectSprite ?? hoverSelectSprite;
|
||||||
|
customMenu.hoverDeselectColor = hoverDeselectColor ?? hoverSelectColor;
|
||||||
|
|
||||||
|
return customMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Setup the given <paramref name="panel"/> once created, given its index, entry, and onClick action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="panel">The panel to configure.</param>
|
||||||
|
/// <param name="i">The <paramref name="panel"/>'s index.</param>
|
||||||
|
/// <param name="entry">The <paramref name="panel"/>'s <typeparamref name="TEntry"/> entry.</param>
|
||||||
|
/// <param name="onClick">Action to perform when the <paramref name="panel"/> is clicked on.</param>
|
||||||
|
protected abstract void SetupPanelEntry(ShapeshifterPanel panel, int i, TEntry entry, Action onClick);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begins/opens the custom multi-select menu.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entries">All entries to give the custom menu.</param>
|
||||||
|
/// <param name="onClick">Function called when all selections are made/confirmed.</param>
|
||||||
|
/// <param name="totalSelections">The number of selections required.</param>
|
||||||
|
/// <param name="shouldConfirm">Whether the entire selection should be confirmed manually.</param>
|
||||||
|
/// <param name="canRepeat">If the same entry can be selected multiple times, else unselect entry on click.</param>
|
||||||
|
protected void Begin(IEnumerable<TEntry> entries, Action<List<TEntry>?> onClick, int totalSelections, bool shouldConfirm, bool canRepeat = false)
|
||||||
|
{
|
||||||
|
MinigameStubs.Begin(Component, null);
|
||||||
|
|
||||||
|
this.totalSelections = totalSelections;
|
||||||
|
this.shouldConfirm = shouldConfirm;
|
||||||
|
this.canRepeat = canRepeat;
|
||||||
|
this.onSelection = onClick;
|
||||||
|
|
||||||
|
var back = backButton.GetComponent<PassiveButton>();
|
||||||
|
back.OnClick.AddListener((UnityAction)(() =>
|
||||||
|
{
|
||||||
|
selectedEntries.Clear();
|
||||||
|
onClick(null);
|
||||||
|
}));
|
||||||
|
|
||||||
|
DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, Component.TaskType);
|
||||||
|
var list2 = new Il2CppSystem.Collections.Generic.List<UiElement>();
|
||||||
|
RegisterPanels(
|
||||||
|
entries,
|
||||||
|
(shapeshifterPanel, i, entry) =>
|
||||||
|
{
|
||||||
|
var num = i % 3;
|
||||||
|
var num2 = i / 3;
|
||||||
|
shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
|
||||||
|
SetupPanelEntry(shapeshifterPanel, i, entry, () => OnEntryClick(entry));
|
||||||
|
list2.Add(shapeshifterPanel.Button);
|
||||||
|
},
|
||||||
|
(p, e) => new MenuEntry(p, e));
|
||||||
|
ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begins/opens the custom menu, only requiring a single selection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entries">All entries to give the custom menu.</param>
|
||||||
|
/// <param name="onClick">Function called when the selection is made.</param>
|
||||||
|
/// <param name="shouldConfirm">Whether the set of both selections should be confirmed manually.</param>
|
||||||
|
protected void Begin(IEnumerable<TEntry> entries, Action<TEntry?> onClick, bool shouldConfirm)
|
||||||
|
{
|
||||||
|
Begin(entries, list => onClick(list?[0]), 1, shouldConfirm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begins/opens the custom menu, requiring two selections.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entries">All entries to give the custom menu.</param>
|
||||||
|
/// <param name="onClick">Function called when both selections are made.</param>
|
||||||
|
/// <param name="shouldConfirm">Whether the set of both selections should be confirmed manually.</param>
|
||||||
|
/// <param name="canRepeat">If the same entry can be selected both times, else unselect entry on click.</param>
|
||||||
|
protected void Begin(IEnumerable<TEntry> entries, Action<TEntry?, TEntry?> onClick, bool shouldConfirm, bool canRepeat = false)
|
||||||
|
{
|
||||||
|
Begin(entries, list => onClick(list?[0], list?[1]), 2, shouldConfirm, canRepeat);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEntryClick(TEntry entry)
|
||||||
|
{
|
||||||
|
MenuEntry menuEntry = MenuEntries.First(e => e.Entry == entry);
|
||||||
|
|
||||||
|
if (!canRepeat && selectedEntries.Remove(menuEntry)) // Unselect previous choice
|
||||||
|
{
|
||||||
|
// TODO: if confirm button is enabled, disable it
|
||||||
|
SetNameplateAppearance(menuEntry, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedEntries.Count >= totalSelections) // Do not go past total selections.
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedEntries.Add(menuEntry);
|
||||||
|
if (selectedEntries.Count <= totalSelections) // Add choice to list of selections
|
||||||
|
{
|
||||||
|
SetNameplateAppearance(menuEntry, true);
|
||||||
|
}
|
||||||
|
if (selectedEntries.Count < totalSelections)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total selections reached
|
||||||
|
if (shouldConfirm)
|
||||||
|
{
|
||||||
|
// TODO: enable confirm button
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnCompleteSelection();
|
||||||
|
selectedEntries.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCompleteSelection()
|
||||||
|
{
|
||||||
|
onSelection!([.. selectedEntries.Select(se => se.Entry)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override bool IsEntrySelected(IMenuEntry entry)
|
||||||
|
{
|
||||||
|
return selectedEntries.Any(e => e.Panel == entry.Panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="CustomPhoneMenu.SetNameplateAppearance(IMenuEntry, LoadableAsset{Sprite}?, Color?, Color?)"/>
|
||||||
|
/// <param name="menuEntry"></param>
|
||||||
|
/// <param name="isSelected">Whether the menu entry is actively selected.</param>
|
||||||
|
protected void SetNameplateAppearance(IMenuEntry menuEntry, bool isSelected)
|
||||||
|
{
|
||||||
|
SetNameplateAppearance(
|
||||||
|
menuEntry,
|
||||||
|
isSelected ? hoverDeselectSprite : hoverSelectSprite,
|
||||||
|
isSelected ? hoverDeselectColor : hoverSelectColor,
|
||||||
|
isSelected ? activeColor : Color.clear);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
using MiraAPI.Patches.Stubs;
|
||||||
|
using MiraAPI.Utilities.Assets;
|
||||||
|
using Reactor.Utilities;
|
||||||
|
using Reactor.Utilities.Extensions;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using CppCollections = Il2CppSystem.Collections.Generic;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
namespace MiraAPI.Hud;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Paginable <see cref="CustomPhoneMenu"/> using the <see cref="ShapeshifterPanel"/> as a base.
|
||||||
|
/// </summary>
|
||||||
|
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity convention.")]
|
||||||
|
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
|
||||||
|
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")]
|
||||||
|
public abstract class CustomPaginableMenu : CustomPhoneMenu<CustomPaginableMenu.MenuEntry>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Menu Entry used for when multiple pages are needed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Panel">The panel.</param>
|
||||||
|
/// <param name="SortKey">The key by which the panels are sorted.</param>
|
||||||
|
public record MenuEntry(ShapeshifterPanel Panel, string SortKey) : IMenuEntry;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the menu.
|
||||||
|
/// </summary>
|
||||||
|
protected abstract string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the prefab used to generate the search field.
|
||||||
|
/// </summary>
|
||||||
|
protected abstract TextBoxTMP? PrefabTextbox { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override float MenuDepth => -60f;
|
||||||
|
|
||||||
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
protected int currentPage;
|
||||||
|
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
|
||||||
|
private TextBoxTMP? searchTextbox;
|
||||||
|
private string searchText = string.Empty;
|
||||||
|
private TextMeshPro? noResultsText;
|
||||||
|
|
||||||
|
private const int ItemsPerPage = 15;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <typeparamref name="TMenu"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TMenu">The type of <see cref="CustomPaginableMenu"/>.</typeparam>
|
||||||
|
/// <param name="onMouseOut">Function that can optionally be run when the mouse is moved outside a menu panel.</param>
|
||||||
|
/// <param name="onMouseOver">Function that can optionally be run when the mouse is moved over a menu panel.</param>
|
||||||
|
/// <returns>New <typeparamref name="TMenu"/> object.</returns>
|
||||||
|
protected static new TMenu Create<TMenu>(PanelButtonOnMouse? onMouseOut = null, PanelButtonOnMouse? onMouseOver = null) where TMenu : CustomPaginableMenu, new()
|
||||||
|
{
|
||||||
|
TMenu customMenu = CustomPhoneMenu.Create<TMenu>(onMouseOut, onMouseOver);
|
||||||
|
|
||||||
|
var nextButton = Object.Instantiate(customMenu.backButton, customMenu.transform).gameObject;
|
||||||
|
nextButton.transform.localPosition = new Vector3(1.85f, -2.185f, customMenu.MenuDepth);
|
||||||
|
nextButton.transform.localScale = new Vector3(0.65f, 0.65f, 1);
|
||||||
|
nextButton.name = "RightArrowButton";
|
||||||
|
nextButton.GetComponent<SpriteRenderer>().sprite = MiraAssets.NextButton.LoadAsset();
|
||||||
|
nextButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
|
||||||
|
|
||||||
|
var passiveButton = nextButton.gameObject.GetComponent<PassiveButton>();
|
||||||
|
passiveButton.OnClick = new Button.ButtonClickedEvent();
|
||||||
|
passiveButton.OnClick.AddListener((UnityAction)customMenu.NextPage);
|
||||||
|
|
||||||
|
var backButton = Object.Instantiate(nextButton, customMenu.transform).gameObject;
|
||||||
|
backButton.transform.localPosition = new Vector3(-1.85f, -2.185f, customMenu.MenuDepth);
|
||||||
|
backButton.name = "LeftArrowButton";
|
||||||
|
backButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().Destroy();
|
||||||
|
backButton.GetComponent<SpriteRenderer>().flipX = true;
|
||||||
|
var prevPassive = backButton.gameObject.GetComponent<PassiveButton>();
|
||||||
|
prevPassive.OnClick.AddListener((UnityAction)customMenu.PreviousPage);
|
||||||
|
|
||||||
|
customMenu.PhoneUI.GetChild(0).GetComponent<SpriteRenderer>().material =
|
||||||
|
PlayerControl.LocalPlayer.cosmetics.currentBodySprite.BodySprite.material;
|
||||||
|
customMenu.PhoneUI.GetChild(1).GetComponent<SpriteRenderer>().material =
|
||||||
|
PlayerControl.LocalPlayer.cosmetics.currentBodySprite.BodySprite.material;
|
||||||
|
|
||||||
|
return customMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeForSearch(string? text)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(text)
|
||||||
|
? string.Empty
|
||||||
|
: text.Trim().ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MenuEntry> GetFilteredEntries()
|
||||||
|
{
|
||||||
|
var query = NormalizeForSearch(searchText);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(query))
|
||||||
|
return MenuEntries;
|
||||||
|
|
||||||
|
return [.. MenuEntries
|
||||||
|
.Where(e => e.SortKey.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderByDescending(e => e.SortKey.Equals(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ThenByDescending(e => e.SortKey.StartsWith(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ThenByDescending(e => e.SortKey.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ThenBy(e => e.SortKey, StringComparer.OrdinalIgnoreCase)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetTotalPages(int itemCount)
|
||||||
|
{
|
||||||
|
return Mathf.Max(1, Mathf.CeilToInt(itemCount / (float)ItemsPerPage));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshControllerOverlay(CppCollections.List<UiElement> list)
|
||||||
|
{
|
||||||
|
if (ControllerManager.Instance && backButton != null)
|
||||||
|
{
|
||||||
|
ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator CoRestoreFocus()
|
||||||
|
{
|
||||||
|
yield return null;
|
||||||
|
searchTextbox?.GiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NextPage()
|
||||||
|
{
|
||||||
|
var filtered = GetFilteredEntries();
|
||||||
|
var pages = GetTotalPages(filtered.Count);
|
||||||
|
currentPage = (currentPage + 1) % pages;
|
||||||
|
var list = ShowPage();
|
||||||
|
RefreshControllerOverlay(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PreviousPage()
|
||||||
|
{
|
||||||
|
var filtered = GetFilteredEntries();
|
||||||
|
var pages = GetTotalPages(filtered.Count);
|
||||||
|
currentPage = (currentPage - 1 + pages) % pages;
|
||||||
|
var list = ShowPage();
|
||||||
|
RefreshControllerOverlay(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows the current page.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The Il2CPP list of ui elements displayed on the page.</returns>
|
||||||
|
public CppCollections.List<UiElement> ShowPage()
|
||||||
|
{
|
||||||
|
foreach (var entry in MenuEntries)
|
||||||
|
{
|
||||||
|
entry.Panel.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var filtered = GetFilteredEntries();
|
||||||
|
noResultsText?.gameObject.SetActive(filtered.Count == 0 && !string.IsNullOrWhiteSpace(searchText));
|
||||||
|
var totalPages = GetTotalPages(filtered.Count);
|
||||||
|
currentPage = Mathf.Clamp(currentPage, 0, totalPages - 1);
|
||||||
|
|
||||||
|
var list = filtered.Skip(currentPage * ItemsPerPage).Take(ItemsPerPage).ToList();
|
||||||
|
var list2 = new CppCollections.List<UiElement>();
|
||||||
|
|
||||||
|
for (var i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
var entry = list[i];
|
||||||
|
var num = i % 3;
|
||||||
|
var num2 = i / 3 % 5;
|
||||||
|
entry.Panel.transform.localPosition =
|
||||||
|
new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
|
||||||
|
entry.Panel.gameObject.SetActive(true);
|
||||||
|
list2.Add(entry.Panel.Button);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures that the search UI exists.
|
||||||
|
/// </summary>
|
||||||
|
protected void EnsureSearchUi()
|
||||||
|
{
|
||||||
|
if (searchTextbox != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gridCenterX = xStart + xOffset;
|
||||||
|
var desiredSearchBarCenterY = yStart + 0.55f;
|
||||||
|
|
||||||
|
if (PrefabTextbox == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchRoot = PrefabTextbox.transform.parent != null ? PrefabTextbox.transform.parent.gameObject : PrefabTextbox.gameObject;
|
||||||
|
var searchObj = Object.Instantiate(searchRoot, transform);
|
||||||
|
searchObj.name = $"{Name}SearchBar";
|
||||||
|
|
||||||
|
foreach (var aspect in searchObj.GetComponentsInChildren<AspectPosition>(true))
|
||||||
|
{
|
||||||
|
aspect.DestroyImmediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTextbox = searchObj.GetComponentInChildren<TextBoxTMP>(true);
|
||||||
|
if (searchTextbox == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var placeholder = searchTextbox.transform.parent.GetChild(2).GetComponent<TextMeshPro>();
|
||||||
|
placeholder?.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
foreach (var tmp in searchObj.GetComponentsInChildren<TextMeshPro>(true))
|
||||||
|
{
|
||||||
|
if (tmp != searchTextbox.outputText &&
|
||||||
|
(tmp.text.Contains("Search", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
tmp.text.Contains("Here", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
tmp.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchObj.transform.localScale = new Vector3(0.7f, 0.7f, 1f);
|
||||||
|
searchObj.transform.localPosition = new Vector3(gridCenterX, desiredSearchBarCenterY, -1f);
|
||||||
|
|
||||||
|
var searchBounds = CalcSpriteBoundsInParentSpace(transform, searchObj);
|
||||||
|
var deltaX = gridCenterX - searchBounds.center.x;
|
||||||
|
var deltaY = desiredSearchBarCenterY - searchBounds.center.y;
|
||||||
|
searchObj.transform.localPosition += new Vector3(deltaX, deltaY, 0f);
|
||||||
|
searchBounds = CalcSpriteBoundsInParentSpace(transform, searchObj);
|
||||||
|
|
||||||
|
var wikiClickSound = HudManager.Instance?.MapButton?.ClickSound;
|
||||||
|
|
||||||
|
var searchFocusButton = searchTextbox.gameObject.GetComponent<PassiveButton>()
|
||||||
|
?? searchTextbox.gameObject.AddComponent<PassiveButton>();
|
||||||
|
if (wikiClickSound != null)
|
||||||
|
{
|
||||||
|
searchFocusButton.ClickSound = wikiClickSound;
|
||||||
|
}
|
||||||
|
searchFocusButton.OnClick.RemoveAllListeners();
|
||||||
|
searchFocusButton.OnClick.AddListener((UnityAction)(Action)(() =>
|
||||||
|
{
|
||||||
|
searchTextbox.GiveFocus();
|
||||||
|
}));
|
||||||
|
|
||||||
|
searchTextbox.SetText(string.Empty);
|
||||||
|
searchTextbox.OnChange.RemoveAllListeners();
|
||||||
|
searchTextbox.OnChange.AddListener((UnityAction)(Action)(() =>
|
||||||
|
{
|
||||||
|
searchText = searchTextbox.outputText.text ?? string.Empty;
|
||||||
|
currentPage = 0;
|
||||||
|
var list = ShowPage();
|
||||||
|
RefreshControllerOverlay(list);
|
||||||
|
|
||||||
|
if (searchTextbox != null)
|
||||||
|
{
|
||||||
|
Coroutines.Start(CoRestoreFocus());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
var label = Object.Instantiate(HudManager.Instance?.TaskPanel.taskText, transform);
|
||||||
|
if (label != null)
|
||||||
|
{
|
||||||
|
label.name = $"{Name}SearchLabel";
|
||||||
|
label.text = "Search";
|
||||||
|
label.alignment = TextAlignmentOptions.Center;
|
||||||
|
label.fontSize = label.fontSizeMin = label.fontSizeMax = 2.1f;
|
||||||
|
label.color = Color.white;
|
||||||
|
label.transform.localPosition = new Vector3(gridCenterX, searchBounds.max.y + 0.18f, -1f);
|
||||||
|
|
||||||
|
if (searchTextbox.outputText != null)
|
||||||
|
{
|
||||||
|
label.font = searchTextbox.outputText.font;
|
||||||
|
label.fontMaterial = searchTextbox.outputText.fontMaterial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noResultsText = Object.Instantiate(HudManager.Instance?.TaskPanel.taskText, transform);
|
||||||
|
if (noResultsText != null)
|
||||||
|
{
|
||||||
|
noResultsText.name = $"{Name}NoResultsText";
|
||||||
|
noResultsText.text = "No results";
|
||||||
|
noResultsText.alignment = TextAlignmentOptions.Center;
|
||||||
|
noResultsText.fontSize = noResultsText.fontSizeMin = noResultsText.fontSizeMax = 2.25f;
|
||||||
|
noResultsText.color = Color.white;
|
||||||
|
noResultsText.transform.localPosition = new Vector3(gridCenterX, yStart + 0.1f, -1f);
|
||||||
|
noResultsText.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backButton == null || searchTextbox == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var clearButtonX = searchBounds.max.x + 0.2f;
|
||||||
|
var clearButtonY = searchBounds.center.y;
|
||||||
|
|
||||||
|
var clearObj = Object.Instantiate(backButton.gameObject, transform);
|
||||||
|
clearObj.name = "ClearSearchButton";
|
||||||
|
clearObj.transform.localScale = new Vector3(0.35f, 0.35f, 1f);
|
||||||
|
clearObj.transform.localPosition = new Vector3(clearButtonX, clearButtonY, -1f);
|
||||||
|
|
||||||
|
clearObj.GetComponent<CloseButtonConsoleBehaviour>()?.DestroyImmediate();
|
||||||
|
clearObj.GetComponent<AspectPosition>()?.DestroyImmediate();
|
||||||
|
|
||||||
|
var clearSearchButton = clearObj.GetComponent<PassiveButton>();
|
||||||
|
if (clearSearchButton == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (wikiClickSound != null)
|
||||||
|
{
|
||||||
|
clearSearchButton.ClickSound = wikiClickSound;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSearchButton.OnClick.RemoveAllListeners();
|
||||||
|
clearSearchButton.OnClick = new Button.ButtonClickedEvent();
|
||||||
|
clearSearchButton.OnClick.AddListener((UnityAction)(() =>
|
||||||
|
{
|
||||||
|
if (searchTextbox == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTextbox.SetText(string.Empty);
|
||||||
|
searchText = string.Empty;
|
||||||
|
currentPage = 0;
|
||||||
|
var list = ShowPage();
|
||||||
|
RefreshControllerOverlay(list);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bounds CalcSpriteBoundsInParentSpace(Transform parent, GameObject root)
|
||||||
|
{
|
||||||
|
var first = true;
|
||||||
|
var bounds = default(Bounds);
|
||||||
|
|
||||||
|
foreach (var r in root.GetComponentsInChildren<SpriteRenderer>(true))
|
||||||
|
{
|
||||||
|
if (r == null || r.sprite == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var b = r.bounds;
|
||||||
|
var c = b.center;
|
||||||
|
var e = b.extents;
|
||||||
|
|
||||||
|
var p1 = parent.InverseTransformPoint(new Vector3(c.x - e.x, c.y - e.y, c.z));
|
||||||
|
var p2 = parent.InverseTransformPoint(new Vector3(c.x - e.x, c.y + e.y, c.z));
|
||||||
|
var p3 = parent.InverseTransformPoint(new Vector3(c.x + e.x, c.y - e.y, c.z));
|
||||||
|
var p4 = parent.InverseTransformPoint(new Vector3(c.x + e.x, c.y + e.y, c.z));
|
||||||
|
|
||||||
|
if (first)
|
||||||
|
{
|
||||||
|
bounds = new Bounds(p1, Vector3.zero);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds.Encapsulate(p1);
|
||||||
|
bounds.Encapsulate(p2);
|
||||||
|
bounds.Encapsulate(p3);
|
||||||
|
bounds.Encapsulate(p4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begins/opens the custom player menu. After registering panels, it will prepare the search, pages, and open the menu.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registerEntryPanels">Function where all panels should be registered.</param>
|
||||||
|
protected void Begin(Action registerEntryPanels)
|
||||||
|
{
|
||||||
|
MinigameStubs.Begin(Component, null);
|
||||||
|
|
||||||
|
searchText = string.Empty;
|
||||||
|
currentPage = 0;
|
||||||
|
|
||||||
|
registerEntryPanels();
|
||||||
|
|
||||||
|
EnsureSearchUi();
|
||||||
|
|
||||||
|
var list2 = ShowPage();
|
||||||
|
|
||||||
|
ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
using AmongUs.GameOptions;
|
||||||
|
using MiraAPI.Utilities.Assets;
|
||||||
|
using Reactor.Utilities.Attributes;
|
||||||
|
using Reactor.Utilities.Extensions;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
namespace MiraAPI.Hud;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component registered in Il2Cpp domain to handle Unity events for custom menus.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterInIl2Cpp]
|
||||||
|
public class CustomPhoneMenuComponent(IntPtr cppPtr) : Minigame(cppPtr)
|
||||||
|
{
|
||||||
|
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")]
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
if (ControllerManager.Instance)
|
||||||
|
{
|
||||||
|
ControllerManager.Instance.CloseOverlayMenu(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("Should not be called, see the respective Begin methods instead.", true)]
|
||||||
|
[SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Retained.")]
|
||||||
|
#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member (Justification: Intentional to avoid mods calling Begin when they are not supposed to.)
|
||||||
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Base game code.)
|
||||||
|
public override void Begin(PlayerTask task)
|
||||||
|
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Should not be called, see the respective Begin methods instead.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines an entry in a <see cref="CustomPhoneMenu"/> with a Panel.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMenuEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the base Panel of the menu entry.
|
||||||
|
/// </summary>
|
||||||
|
ShapeshifterPanel Panel { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines a custom menu with a list of <see cref="ICustomMenu"/> entries.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICustomMenu
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all registered <see cref="IMenuEntry"/>s.
|
||||||
|
/// </summary>
|
||||||
|
List<IMenuEntry> MenuEntries { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines a custom menu with a list of only <typeparamref name="TMenu"/> entries.
|
||||||
|
/// <para/>
|
||||||
|
/// Only implement this if you want to define a single new type of menu entries explicitly.
|
||||||
|
/// Must reference the member of the <see cref="CustomPhoneMenu"/> superclass that is being hidden by this one to work.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TMenu">The type of menu entry.</typeparam>
|
||||||
|
public interface ICustomMenu<TMenu> : ICustomMenu where TMenu : IMenuEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all registered <typeparamref name="TMenu"/>s.
|
||||||
|
/// </summary>
|
||||||
|
new List<TMenu> MenuEntries { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom Phone Menu logic using the <see cref="ShapeshifterPanel"/> as a base.
|
||||||
|
/// </summary>
|
||||||
|
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention.")]
|
||||||
|
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
|
||||||
|
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")]
|
||||||
|
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Proxy for Unity properties.")]
|
||||||
|
public abstract class CustomPhoneMenu : ICustomMenu
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Menu Entry used when specifically only the Panel itself is required.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Panel">The <see cref="ShapeshifterPanel"/> instance.</param>
|
||||||
|
protected sealed record class BasicEntry(ShapeshifterPanel Panel) : IMenuEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implicitly converts to <see cref="ShapeshifterPanel"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entry">The entry that contains the reference to the panel.</param>
|
||||||
|
public static implicit operator ShapeshifterPanel(BasicEntry entry)
|
||||||
|
{
|
||||||
|
return entry.Panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implicitly converts to an instance of <see cref="BasicEntry"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="panel">The panel instance to make an entry off of.</param>
|
||||||
|
public static implicit operator BasicEntry(ShapeshifterPanel panel)
|
||||||
|
{
|
||||||
|
return new(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate that is invoked when the mouse interacts with a panel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="highlight">The panel's highlight that the delegate modifies.</param>
|
||||||
|
/// <param name="icon">The panel's icon that the delegate modifies.</param>
|
||||||
|
/// <param name="isSelected">A flag that indicates if the current panel is selected.</param>
|
||||||
|
public delegate void PanelButtonOnMouse(SpriteRenderer highlight, SpriteRenderer icon, bool isSelected);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the wrapped component that the menu modifies.
|
||||||
|
/// </summary>
|
||||||
|
public CustomPhoneMenuComponent Component { get; internal set; } = null!;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public List<IMenuEntry> MenuEntries { get; protected set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all of the panels of the menu.
|
||||||
|
/// </summary>
|
||||||
|
public List<ShapeshifterPanel> EntryPanels => [.. MenuEntries.Select(e => e.Panel)];
|
||||||
|
|
||||||
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Unity convention.)
|
||||||
|
#pragma warning disable S1104 // Fields should not have public accessibility (Justification: Read above.)
|
||||||
|
public Transform transform => Component.transform;
|
||||||
|
public GameObject gameObject => Component.gameObject;
|
||||||
|
public string name => Component.name;
|
||||||
|
|
||||||
|
public float xStart = -0.8f;
|
||||||
|
public float yStart = 2.15f;
|
||||||
|
public float xOffset = 1.95f;
|
||||||
|
public float yOffset = -0.65f;
|
||||||
|
|
||||||
|
public ShapeshifterPanel panelPrefab;
|
||||||
|
public UiElement backButton;
|
||||||
|
public UiElement defaultButtonSelected;
|
||||||
|
|
||||||
|
protected PanelButtonOnMouse? onMouseOverAction;
|
||||||
|
protected PanelButtonOnMouse? onMouseOutAction;
|
||||||
|
#pragma warning restore S1104 // Fields should not have public accessibility
|
||||||
|
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the transform of the menu's Phone UI.
|
||||||
|
/// </summary>
|
||||||
|
public Transform PhoneUI => transform.FindChild("PhoneUI");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the z-axis depth that the menu is instantiated at.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual float MenuDepth => -50f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the menu.
|
||||||
|
/// </summary>
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
Component.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <typeparamref name="TMenu"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TMenu">The type of <see cref="CustomPhoneMenu"/>.</typeparam>
|
||||||
|
/// <param name="onMouseOut">Function that can optionally be run when the mouse is moved outside a menu panel.</param>
|
||||||
|
/// <param name="onMouseOver">Function that can optionally be run when the mouse is moved over a menu panel.</param>
|
||||||
|
/// <returns>New <typeparamref name="TMenu"/> object.</returns>
|
||||||
|
protected static TMenu Create<TMenu>(PanelButtonOnMouse? onMouseOut = null, PanelButtonOnMouse? onMouseOver = null) where TMenu : CustomPhoneMenu, new()
|
||||||
|
{
|
||||||
|
var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter);
|
||||||
|
|
||||||
|
var ogMenu = shapeShifterRole.TryCast<ShapeshifterRole>()!.ShapeshifterMenu;
|
||||||
|
var newMenu = Object.Instantiate(ogMenu);
|
||||||
|
var component = newMenu.gameObject.AddComponent<CustomPhoneMenuComponent>();
|
||||||
|
var customMenu = new TMenu
|
||||||
|
{
|
||||||
|
Component = component,
|
||||||
|
panelPrefab = newMenu.PanelPrefab,
|
||||||
|
xStart = newMenu.XStart,
|
||||||
|
yStart = newMenu.YStart,
|
||||||
|
xOffset = newMenu.XOffset,
|
||||||
|
yOffset = newMenu.YOffset,
|
||||||
|
defaultButtonSelected = newMenu.DefaultButtonSelected,
|
||||||
|
backButton = newMenu.BackButton,
|
||||||
|
};
|
||||||
|
|
||||||
|
var back = customMenu.backButton.GetComponent<PassiveButton>();
|
||||||
|
back.OnClick.RemoveAllListeners();
|
||||||
|
back.OnClick.AddListener((UnityAction)customMenu.Close);
|
||||||
|
|
||||||
|
component.CloseSound = newMenu.CloseSound;
|
||||||
|
component.logger = newMenu.logger;
|
||||||
|
component.OpenSound = newMenu.OpenSound;
|
||||||
|
|
||||||
|
newMenu.DestroyImmediate();
|
||||||
|
|
||||||
|
customMenu.transform.SetParent(Camera.main!.transform, false);
|
||||||
|
customMenu.transform.localPosition = new Vector3(0f, 0f, customMenu.MenuDepth);
|
||||||
|
|
||||||
|
customMenu.onMouseOverAction = onMouseOver;
|
||||||
|
customMenu.onMouseOutAction = onMouseOut;
|
||||||
|
|
||||||
|
return customMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register new menu panels given a set of entries.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEntry">The type of the entry.</typeparam>
|
||||||
|
/// <param name="entries">The entries to create panels for.</param>
|
||||||
|
/// <param name="entryPanelConfig">Function to configure the menu's panel once created, given its index and entry.</param>
|
||||||
|
/// <param name="menuEntryMaker">Function to create a <see cref="IMenuEntry"/> for a given panel and it's entry.
|
||||||
|
/// If <see langword="null"/>, it will create <see cref="BasicEntry"/>s, acting as wrappers for the <see cref="ShapeshifterPanel"/>s.</param>
|
||||||
|
protected void RegisterPanels<TEntry>(
|
||||||
|
IEnumerable<TEntry> entries,
|
||||||
|
Action<ShapeshifterPanel, int, TEntry> entryPanelConfig,
|
||||||
|
Func<ShapeshifterPanel, TEntry, IMenuEntry>? menuEntryMaker = null)
|
||||||
|
{
|
||||||
|
int currentEntries = MenuEntries.Count;
|
||||||
|
|
||||||
|
var list = entries.ToList();
|
||||||
|
|
||||||
|
for (var i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
var index = currentEntries + i;
|
||||||
|
var entry = list[i];
|
||||||
|
|
||||||
|
var shapeshifterPanel = Object.Instantiate(panelPrefab, transform);
|
||||||
|
shapeshifterPanel.transform.localPosition = new Vector3(0f, 0f, -1f);
|
||||||
|
entryPanelConfig(shapeshifterPanel, index, entry);
|
||||||
|
|
||||||
|
menuEntryMaker ??= (s, _) => new BasicEntry(s);
|
||||||
|
var menuEntry = menuEntryMaker(shapeshifterPanel, entry);
|
||||||
|
MenuEntries.Add(menuEntry);
|
||||||
|
|
||||||
|
var button = shapeshifterPanel.Button;
|
||||||
|
var nameplate = shapeshifterPanel.gameObject.transform.FindChild("Nameplate");
|
||||||
|
var highlight = nameplate.FindChild("Highlight").GetComponent<SpriteRenderer>();
|
||||||
|
var icon = highlight.transform.GetChild(0).GetComponent<SpriteRenderer>();
|
||||||
|
|
||||||
|
if (onMouseOverAction != null)
|
||||||
|
{
|
||||||
|
button.OnMouseOver.RemoveAllListeners();
|
||||||
|
button.OnMouseOver = new UnityEvent();
|
||||||
|
button.OnMouseOver.AddListener((UnityAction)
|
||||||
|
(() => onMouseOverAction(highlight, icon, IsEntrySelected(menuEntry))));
|
||||||
|
}
|
||||||
|
if (onMouseOutAction != null)
|
||||||
|
{
|
||||||
|
button.OnMouseOut.RemoveAllListeners();
|
||||||
|
button.OnMouseOut = new UnityEvent();
|
||||||
|
button.OnMouseOut.AddListener((UnityAction)
|
||||||
|
(() => onMouseOutAction(highlight, icon, IsEntrySelected(menuEntry))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="RegisterPanels{TEntry}(IEnumerable{TEntry}, Action{ShapeshifterPanel, int, TEntry}, Func{ShapeshifterPanel, TEntry, IMenuEntry}?)"/>
|
||||||
|
/// <param name="entries"></param>
|
||||||
|
/// <param name="onEntryClick">Action to perform when a given entry is clicked on.
|
||||||
|
/// Argument will be <see langword="null"/> if the back button was clicked instead.</param>
|
||||||
|
/// <param name="entryPanelActionConfig">Function to configure the menu's panel once created, given its index, entry, and onClick action.</param>
|
||||||
|
/// <param name="menuEntryMaker"></param>
|
||||||
|
protected void RegisterPanels<TEntry>(
|
||||||
|
IEnumerable<TEntry> entries,
|
||||||
|
Action<TEntry?> onEntryClick,
|
||||||
|
Action<ShapeshifterPanel, int, TEntry, Action> entryPanelActionConfig,
|
||||||
|
Func<ShapeshifterPanel, TEntry, IMenuEntry>? menuEntryMaker = null)
|
||||||
|
{
|
||||||
|
RegisterPanels(entries, (p, i, e) => entryPanelActionConfig(p, i, e, () => onEntryClick(e)), menuEntryMaker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a given entry is currently selected, not just hovered over.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entry">The <see cref="IMenuEntry"/> to check.</param>
|
||||||
|
/// <returns><see langword="true"/> if the <paramref name="entry"/> is selected, else <see langword="false"/>.</returns>
|
||||||
|
protected virtual bool IsEntrySelected(IMenuEntry entry)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the icon, over color, and unselected color for a given entry.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="menuEntry">The menu entry.</param>
|
||||||
|
/// <param name="sprite">The <see cref="Sprite"/> to use as the icon, if any.</param>
|
||||||
|
/// <param name="overColor">The <see cref="Color"/> to use when hovering over an entry.</param>
|
||||||
|
/// <param name="unselectedColor">The <see cref="Color"/> to use when not hovering over an entry.</param>
|
||||||
|
protected static void SetNameplateAppearance(
|
||||||
|
IMenuEntry menuEntry, LoadableAsset<Sprite>? sprite, Color? overColor, Color? unselectedColor)
|
||||||
|
{
|
||||||
|
ShapeshifterPanel panel = menuEntry.Panel;
|
||||||
|
|
||||||
|
var nameplate = panel.gameObject.transform.FindChild("Nameplate");
|
||||||
|
var highlight = nameplate.FindChild("Highlight").GetComponent<SpriteRenderer>();
|
||||||
|
var icon = highlight.transform.GetChild(0).GetComponent<SpriteRenderer>();
|
||||||
|
if (sprite != null)
|
||||||
|
{
|
||||||
|
icon.sprite = sprite.LoadAsset();
|
||||||
|
}
|
||||||
|
var button = nameplate.GetComponent<ButtonRolloverHandler>();
|
||||||
|
if (overColor is { } oColor)
|
||||||
|
{
|
||||||
|
button.OverColor = oColor;
|
||||||
|
}
|
||||||
|
if (unselectedColor is { } uColor)
|
||||||
|
{
|
||||||
|
button.UnselectedColor = uColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="CustomPhoneMenu"/>
|
||||||
|
/// <typeparam name="TMenu">The type of menu entries.</typeparam>
|
||||||
|
public abstract class CustomPhoneMenu<TMenu> : CustomPhoneMenu, ICustomMenu<TMenu> where TMenu : IMenuEntry
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public new List<TMenu> MenuEntries
|
||||||
|
{
|
||||||
|
get => [.. base.MenuEntries.Cast<TMenu>()];
|
||||||
|
protected set => base.MenuEntries = [.. value.Cast<IMenuEntry>()];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,36 +2,22 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AmongUs.GameOptions;
|
|
||||||
using Il2CppInterop.Runtime.Attributes;
|
|
||||||
using MiraAPI.Patches.Stubs;
|
|
||||||
using Reactor.Utilities.Attributes;
|
|
||||||
using Reactor.Utilities.Extensions;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Events;
|
|
||||||
|
|
||||||
namespace MiraAPI.Hud;
|
namespace MiraAPI.Hud;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Custom Player Menu using the <see cref="ShapeshifterPanel"/> as a base.
|
/// Custom Player Menu using the <see cref="ShapeshifterPanel"/> as a base.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="il2CppPtr">Used by Il2Cpp. Do not use constructor, this is a <see cref="MonoBehaviour"/>.</param>
|
|
||||||
[RegisterInIl2Cpp]
|
|
||||||
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention.")]
|
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention.")]
|
||||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention.")]
|
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
|
||||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention.")]
|
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Read above.")]
|
||||||
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")]
|
public class CustomPlayerMenu : CustomMultiSelectMenu<PlayerControl>
|
||||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Unity fields to mirror base game ShapeshifterMinigame; not worth documenting)
|
|
||||||
public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
|
|
||||||
{
|
{
|
||||||
public ShapeshifterPanel panelPrefab;
|
[SuppressMessage("Minor Code Smell", "S1104:Fields should not have public accessibility", Justification = "Read above.")]
|
||||||
public float xStart = -0.8f;
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
public float yStart = 2.15f;
|
|
||||||
public float xOffset = 1.95f;
|
|
||||||
public float yOffset = -0.65f;
|
|
||||||
public UiElement backButton;
|
|
||||||
public UiElement defaultButtonSelected;
|
|
||||||
public List<ShapeshifterPanel> potentialVictims;
|
public List<ShapeshifterPanel> potentialVictims;
|
||||||
|
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a <see cref="CustomPlayerMenu"/>.
|
/// Creates a <see cref="CustomPlayerMenu"/>.
|
||||||
@@ -39,43 +25,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
|
|||||||
/// <returns>New <see cref="CustomPlayerMenu"/> object.</returns>
|
/// <returns>New <see cref="CustomPlayerMenu"/> object.</returns>
|
||||||
public static CustomPlayerMenu Create()
|
public static CustomPlayerMenu Create()
|
||||||
{
|
{
|
||||||
var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter);
|
return Create<CustomPlayerMenu>();
|
||||||
|
|
||||||
var ogMenu = shapeShifterRole.TryCast<ShapeshifterRole>()!.ShapeshifterMenu;
|
|
||||||
var newMenu = Instantiate(ogMenu);
|
|
||||||
var customMenu = newMenu.gameObject.AddComponent<CustomPlayerMenu>();
|
|
||||||
|
|
||||||
customMenu.panelPrefab = newMenu.PanelPrefab;
|
|
||||||
customMenu.xStart = newMenu.XStart;
|
|
||||||
customMenu.yStart = newMenu.YStart;
|
|
||||||
customMenu.xOffset = newMenu.XOffset;
|
|
||||||
customMenu.yOffset = newMenu.YOffset;
|
|
||||||
customMenu.backButton = newMenu.BackButton;
|
|
||||||
|
|
||||||
var back = customMenu.backButton.GetComponent<PassiveButton>();
|
|
||||||
back.OnClick.RemoveAllListeners();
|
|
||||||
back.OnClick.AddListener((UnityAction)Instance.Close);
|
|
||||||
|
|
||||||
customMenu.CloseSound = newMenu.CloseSound;
|
|
||||||
customMenu.logger = newMenu.logger;
|
|
||||||
customMenu.OpenSound = newMenu.OpenSound;
|
|
||||||
|
|
||||||
newMenu.DestroyImmediate();
|
|
||||||
|
|
||||||
customMenu.transform.SetParent(Camera.main!.transform, false);
|
|
||||||
customMenu.transform.localPosition = new Vector3(0f, 0f, -50f);
|
|
||||||
return customMenu;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
|
||||||
{
|
|
||||||
ControllerManager.Instance.CloseOverlayMenu(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Begin(PlayerTask task)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException("Use the other Begin method.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -83,36 +33,31 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="playerMatch">Function to determine if player should show in the custom menu.</param>
|
/// <param name="playerMatch">Function to determine if player should show in the custom menu.</param>
|
||||||
/// <param name="onClick"><see cref="PassiveButton.OnClick"/> action for player.</param>
|
/// <param name="onClick"><see cref="PassiveButton.OnClick"/> action for player.</param>
|
||||||
[HideFromIl2Cpp]
|
/// <param name="shouldConfirm">Whether the set of both selections should be confirmed manually.</param>
|
||||||
public void Begin(Func<PlayerControl, bool> playerMatch, Action<PlayerControl?> onClick)
|
public void Begin(Func<PlayerControl, bool> playerMatch, Action<PlayerControl?> onClick, bool shouldConfirm = false)
|
||||||
{
|
{
|
||||||
MinigameStubs.Begin(this, null);
|
Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick, shouldConfirm);
|
||||||
|
potentialVictims = EntryPanels;
|
||||||
|
}
|
||||||
|
|
||||||
var back = backButton.GetComponent<PassiveButton>();
|
/// <summary>
|
||||||
back.OnClick.AddListener((UnityAction)(() =>
|
/// Begins/opens the custom player menu.
|
||||||
{
|
/// </summary>
|
||||||
onClick(null);
|
/// <param name="playerMatch">Function to determine if player should show in the custom menu.</param>
|
||||||
}));
|
/// <param name="onClick"><see cref="PassiveButton.OnClick"/> action for the two-player selection.</param>
|
||||||
|
/// <param name="shouldConfirm">Whether the set of both selections should be confirmed manually.</param>
|
||||||
|
/// <param name="canRepeat">If the same entry can be selected both times, else unselect entry on click.</param>
|
||||||
|
public void Begin(Func<PlayerControl, bool> playerMatch, Action<PlayerControl?, PlayerControl?> onClick, bool shouldConfirm = false, bool canRepeat = false)
|
||||||
|
{
|
||||||
|
Begin(PlayerControl.AllPlayerControls.ToArray().Where(playerMatch), onClick, shouldConfirm, canRepeat);
|
||||||
|
potentialVictims = EntryPanels;
|
||||||
|
}
|
||||||
|
|
||||||
DebugAnalytics.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType);
|
/// <inheritdoc/>
|
||||||
var list = PlayerControl.AllPlayerControls.ToArray().Where(playerMatch).ToList();
|
protected override void SetupPanelEntry(ShapeshifterPanel panel, int i, PlayerControl entry, Action onClick)
|
||||||
potentialVictims = [];
|
{
|
||||||
var list2 = new CppCollections.List<UiElement>();
|
var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == entry.Data.Role.NameColor;
|
||||||
|
panel.SetPlayer(i, entry.Data, (Il2CppSystem.Action)onClick);
|
||||||
for (var i = 0; i < list.Count; i++)
|
panel.NameText.color = flag ? entry.Data.Role.NameColor : Color.white;
|
||||||
{
|
|
||||||
var player = list[i];
|
|
||||||
var num = i % 3;
|
|
||||||
var num2 = i / 3;
|
|
||||||
var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor;
|
|
||||||
var shapeshifterPanel = Instantiate(panelPrefab, transform);
|
|
||||||
shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
|
|
||||||
shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => onClick(player)));
|
|
||||||
shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white;
|
|
||||||
potentialVictims.Add(shapeshifterPanel);
|
|
||||||
list2.Add(shapeshifterPanel.Button);
|
|
||||||
}
|
|
||||||
|
|
||||||
ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
|
|||||||
_pagination.gameObject.SetActive(true);
|
_pagination.gameObject.SetActive(true);
|
||||||
if (_pageText != null)
|
if (_pageText != null)
|
||||||
{
|
{
|
||||||
_pageText.text = $"Page {_currentPage + 1}/{Mathf.Max(totalPages, 1)}";
|
_pageText.text = $"{MiraLocaleManager.Get("MiraApi.ModifierDisplay.Page", "Page")} {_currentPage + 1}/{Mathf.Max(totalPages, 1)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
_nextButton.gameObject.SetActive(true);
|
_nextButton.gameObject.SetActive(true);
|
||||||
@@ -213,7 +213,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
|
|||||||
_modifiers.Remove(mod.Key);
|
_modifiers.Remove(mod.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
_toggleBtnText.text = $"Modifiers ({filteredModifiers.Count})";
|
_toggleBtnText.text = $"{MiraLocaleManager.Get("Modifiers", "Modifiers")} ({filteredModifiers.Count})";
|
||||||
|
|
||||||
_toggleButton.gameObject.SetActive(filteredModifiers.Count != 0);
|
_toggleButton.gameObject.SetActive(filteredModifiers.Count != 0);
|
||||||
|
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr)
|
|||||||
var smC1Btn = Instantiate(smModBtn, smModBtn.transform.parent);
|
var smC1Btn = Instantiate(smModBtn, smModBtn.transform.parent);
|
||||||
_smallButtons[MenuButton.CustomOne] = smC1Btn;
|
_smallButtons[MenuButton.CustomOne] = smC1Btn;
|
||||||
smC1Btn.name = "SmallCustomOneButton";
|
smC1Btn.name = "SmallCustomOneButton";
|
||||||
smC1Btn.buttonText.text = "Custom 1";
|
smC1Btn.buttonText.text = "MiraApi.CustomCategory1".Translate();
|
||||||
smC1Btn.OnClick = new Button.ButtonClickedEvent();
|
smC1Btn.OnClick = new Button.ButtonClickedEvent();
|
||||||
smC1Btn.OnClick.AddListener(
|
smC1Btn.OnClick.AddListener(
|
||||||
(UnityAction)(() =>
|
(UnityAction)(() =>
|
||||||
@@ -280,7 +280,7 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr)
|
|||||||
var smC2Btn = Instantiate(smModBtn, smModBtn.transform.parent);
|
var smC2Btn = Instantiate(smModBtn, smModBtn.transform.parent);
|
||||||
_smallButtons[MenuButton.CustomTwo] = smC2Btn;
|
_smallButtons[MenuButton.CustomTwo] = smC2Btn;
|
||||||
smC2Btn.name = "SmallCustomTwoButton";
|
smC2Btn.name = "SmallCustomTwoButton";
|
||||||
smC2Btn.buttonText.text = "Custom 2";
|
smC2Btn.buttonText.text = "MiraApi.CustomCategory2".Translate();
|
||||||
smC2Btn.OnClick = new Button.ButtonClickedEvent();
|
smC2Btn.OnClick = new Button.ButtonClickedEvent();
|
||||||
smC2Btn.OnClick.AddListener(
|
smC2Btn.OnClick.AddListener(
|
||||||
(UnityAction)(() =>
|
(UnityAction)(() =>
|
||||||
@@ -382,15 +382,16 @@ public class MenuState(IntPtr cppPtr) : MonoBehaviour(cppPtr)
|
|||||||
|
|
||||||
private void UpdateUi()
|
private void UpdateUi()
|
||||||
{
|
{
|
||||||
|
var pageText = "MiraApi.Page".Translate();
|
||||||
// Update mod text
|
// Update mod text
|
||||||
if (CurrentModIdx == 0)
|
if (CurrentModIdx == 0)
|
||||||
{
|
{
|
||||||
_text.text = $"<size=40%>(Page 1/{ModCount + 1})</size>\nMain";
|
_text.text = $"<size=40%>({pageText} 1/{ModCount + 1})</size>\n{"MiraApi.Main".Translate()}";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var modName = CurrentMod.MiraPlugin.OptionsTitleText.Translate();
|
var modName = CurrentMod.MiraPlugin.OptionsTitleText.Translate();
|
||||||
_text.text = $"<size=40%>(Page {CurrentModIdx + 1}/{ModCount + 1})</size>\n" +
|
_text.text = $"<size=40%>({pageText} {CurrentModIdx + 1}/{ModCount + 1})</size>\n" +
|
||||||
modName[..Math.Min(modName.Length, 25)];
|
modName[..Math.Min(modName.Length, 25)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,4 +40,7 @@ Outlive the timer or eliminate the crew.</string>
|
|||||||
<string name="MiraApi.RoleTeam.Neutral.Short">Neut</string>
|
<string name="MiraApi.RoleTeam.Neutral.Short">Neut</string>
|
||||||
<string name="MiraApi.RoleTeam.Neutral.Group">Neutral Roles</string>
|
<string name="MiraApi.RoleTeam.Neutral.Group">Neutral Roles</string>
|
||||||
<string name="MiraApi.RoleTeam.Other">Other</string>
|
<string name="MiraApi.RoleTeam.Other">Other</string>
|
||||||
|
<string name="MiraApi.ModifierDisplay.Page">Page</string>
|
||||||
|
<string name="MiraApi.Page">Page</string>
|
||||||
|
<string name="MiraApi.Main">Main</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user