Merge pull request #102 from WanderingPix/meeting-abilities

Meeting abilities
This commit is contained in:
Atony authored and GitHub committed 2026-08-24 21:56:01 -05:00
commit 5a23c38fba
19 files changed
+1155 -5

No files matched your search

+1
View File
@@ -9,5 +9,6 @@ public static class ExampleAssets
// Credit to EpicHorrors for the teleport button asset.
public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png");
public static LoadableResourceAsset SharpshootButton { get; } = new("MiraAPI.Example.Resources.SharpshootButton.png");
public static LoadableResourceAsset Banner { get; } = new("MiraAPI.Example.Resources.FortniteBanner.jpeg");
}
@@ -0,0 +1,33 @@
using MiraAPI.Example.Modifiers;
using MiraAPI.Example.Roles;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.MeetingAbilities;
using MiraAPI.Modifiers;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.MeetingAbilities;
public class EndMeetingButton : MeetingActionButton
{
public override string Name => "End Meeting";
public override float Cooldown => 15;
public override float InitialCooldown => 30;
public override int MaxUses => 3;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override bool Enabled(RoleBehaviour? role)
{
return PlayerControl.LocalPlayer != null && role is MayorRole;
}
protected override void OnClick()
{
MeetingHud.Instance.RpcClose();
}
}
@@ -0,0 +1,34 @@
using MiraAPI.MeetingAbilities;
using MiraAPI.Networking;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.MeetingAbilities;
public class ShootButton : TargetedMeetingButton
{
public override string Name => "Shoot";
public override int MaxUses => 3;
public override float InitialCooldown => 5;
public override float Cooldown => 3;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.SharpshootButton;
public override Color OutlineColor => Color.red;
public override bool Enabled(RoleBehaviour r)
{
return r.IsImpostor;
}
protected override void OnClick(PlayerVoteArea playerVoteArea)
{
playerVoteArea.SetDisabled();
var player = playerVoteArea.GetPlayer();
PlayerControl.LocalPlayer.RpcCustomMurder(player);
}
}
@@ -0,0 +1,51 @@
using System;
using MiraAPI.MeetingAbilities;
using MiraAPI.Networking;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.MeetingAbilities;
public class ShotgunButton : MultiTargetMeetingButton
{
public override string Name => "Shotgun";
public override int MaxUses => 3;
public override float InitialCooldown => 5;
public override float Cooldown => 3;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.SharpshootButton;
public override Color OutlineColor => Color.red;
public override bool Enabled(RoleBehaviour r)
{
return r.IsImpostor;
}
protected override void OnClick(PlayerVoteArea playerVoteArea)
{
}
public override LoadableAsset<Sprite> SpriteActive => ExampleAssets.CallMeetingButton;
public override int TargetsCount => 4;
protected override void OnSelect(PlayerVoteArea playerVoteArea, bool toggle)
{
HudManager.Instance.PlayerCam.ShakeScreen(0.5f, 2);
}
public override void OnFinish()
{
foreach (var playerVoteArea in Targets.Keys)
{
playerVoteArea.SetDisabled();
var player = playerVoteArea.GetPlayer();
PlayerControl.LocalPlayer.RpcCustomMurder(player);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -8,7 +8,7 @@ namespace MiraAPI.Hud;
/// <see cref="CustomActionButton"/> that has multiple target objects.
/// </summary>
/// <typeparam name="T">The type of the target objects.</typeparam>
public abstract class MultiTargetButton<T> : CustomActionButton where T : MonoBehaviour
public abstract class MultiTargetMeetingButton<T> : CustomActionButton where T : MonoBehaviour
{
/// <summary>
/// Gets or sets the list of targets of the button.
@@ -0,0 +1,120 @@
using System;
using System.Globalization;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
namespace MiraAPI.MeetingAbilities;
/// <summary>
/// A component for handling targeted meeting buttons.
/// </summary>
[RegisterInIl2Cpp]
public class MeetingAbilityBehaviour : MonoBehaviour
{
/// <summary>
/// Gets or sets whether the component has been initialized.
/// </summary>
private bool _init;
/// <summary>
/// The <see cref="TargetedMeetingButton"/> attached to this component.
/// </summary>
private TargetedMeetingButton _button = null!;
/// <summary>
/// The <see cref="PassiveButton"/> of this component.
/// </summary>
public PassiveButton Button;
/// <summary>
/// The parent <see cref="PlayerVoteArea"/>.
/// </summary>
public PlayerVoteArea VoteArea;
/// <summary>
/// The button's <see cref="SpriteRenderer"/> component.
/// </summary>
public SpriteRenderer Renderer;
/// <summary>
/// The <see cref="TextMeshPro"/> label used for showing the ability's cooldown.
/// </summary>
public TextMeshPro CooldownText;
/// <summary>
/// The <see cref="TextMeshPro"/> label used for showing the ability's remaining uses.
/// </summary>
public TextMeshPro UsesText;
/// <summary>
/// Initializes the various properties of this component.
/// </summary>
/// <param name="button">The <see cref="TargetedMeetingButton"/> to initialize this component for.</param>
/// <param name="voteArea">The parent <see cref="PlayerVoteArea"/>.</param>
public void Initialize(TargetedMeetingButton button, PlayerVoteArea voteArea)
{
if (_init) return;
VoteArea = voteArea;
_button = button;
Renderer = GetComponent<SpriteRenderer>();
SetFillUp(1, 1);
if (!TryGetComponent(out Button))
{
Error($"Could not initialize MeetingButtonBehaviour for {Button.GetType().Name}, Destroying...");
Destroy(this);
}
_init = true;
CooldownText = Instantiate(HudManager.Instance.KillButton.cooldownTimerText, transform);
CooldownText.GetComponent<TextTranslatorTMP>().Destroy();
CooldownText.transform.localPosition = new Vector3(0, 0, -10);
CooldownText.m_maxFontSize = 3f;
CooldownText.color = new Color(1, 1, 1, 0.7f);
UsesText = Instantiate(CooldownText, transform);
UsesText.transform.localPosition = new Vector3(0.25f, 0.25f, -10);
UsesText.transform.localScale = Vector3.one * 0.7f;
CooldownText.gameObject.SetActive(true);
UsesText.gameObject.SetActive(true);
}
private void OnEnable()
{
if (!_init) return;
Button.enabled = true;
Renderer.enabled = true;
UsesText.enabled = true;
CooldownText.enabled = true;
if (_button.Enabled(PlayerControl.LocalPlayer.Data.Role) && _button.IsTargetValid(VoteArea)) return;
Button.enabled = false;
Renderer.enabled = false;
UsesText.enabled = false;
CooldownText.enabled = false;
}
private void Update()
{
if (!_init) return;
if (_button.Timer < 0.01f)
{
Renderer.material.SetFloat("_Desat", 0.0f);
CooldownText.text = string.Empty;
UsesText.text = _button.LimitedUses ? _button.UsesLeft.ToString(CultureInfo.InvariantCulture) : string.Empty;
}
else
{
Renderer.material.SetFloat("_Desat", 0.5f);
CooldownText.text = _button.Timer.ToString(_button.CooldownTimerFormatString, NumberFormatInfo.InvariantInfo);
UsesText.text = string.Empty;
}
SetFillUp(_button.Timer, _button.InitialCooldown);
}
public void SetFillUp(float timer, float maxTimer)
{
float percentCool = Mathf.Clamp(timer / maxTimer, 0.001f, 1f);
if (percentCool <= 0) percentCool = 1f;
Renderer.material.SetFloat("_Percent", percentCool);
}
}
@@ -0,0 +1,321 @@
using System;
using MiraAPI.Events;
using MiraAPI.Events.Mira;
using MiraAPI.Hud;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.MeetingAbilities;
/// <summary>
/// Class for making custom meeting action buttons.
/// </summary>
public abstract class MeetingActionButton
{
/// <summary>
/// Gets the name and text of the button.
/// </summary>
public abstract string Name { get; }
/// <summary>
/// Gets the initial cooldown duration in seconds.
/// </summary>
public virtual float InitialCooldown => Cooldown;
/// <summary>
/// Gets the button's cooldown duration in seconds.
/// </summary>
public abstract float Cooldown { get; }
/// <summary>
/// Gets the sprite of the button. Use <see cref="LoadableResourceAsset"/> to load a sprite from a resource path. Use <see cref="LoadableBundleAsset{T}"/> to load a sprite from an asset bundle.
/// </summary>
public abstract LoadableAsset<Sprite> Sprite { get; }
/// <summary>
/// Gets the format string for the cooldown timer.
/// </summary>
public virtual string CooldownTimerFormatString => "0";
/// <summary>
/// Gets a value indicating whether the button has limited uses.
/// </summary>
public bool LimitedUses => ZeroIsInfinite ? MaxUses > 0 : MaxUses >= 0;
/// <summary>
/// Gets the maximum number of uses the button has. If the button has infinite uses, set to 0 or -1 based on what value that ZeroIsInfinite is set to.
/// </summary>
public virtual int MaxUses => ZeroIsInfinite ? 0 : -1;
/// <summary>
/// Gets the value indicating uses mode.
/// </summary>
public virtual MeetingButtonUsesMode UsesMode => MeetingButtonUsesMode.PerGame;
/// <summary>
/// Gets the button's text outline <see cref="Color"/>.
/// </summary>
public virtual Color TextOutlineColor => Color.clear;
/// <summary>
/// Gets or sets a value indicating whether limited uses are determined via zero or a negative number of uses.
/// </summary>
public virtual bool ZeroIsInfinite { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether the timer is currently active.
/// </summary>
public bool TimerPaused { get; set; }
/// <summary>
/// Gets or sets the amount of uses left.
/// </summary>
public int UsesLeft { get; set; }
/// <summary>
/// Gets or sets the timer variable to measure cooldowns and effects.
/// </summary>
public float Timer { get; set; }
/// <summary>
/// Gets or sets the <see cref="ActionButton"/> object in game. This is created by Mira API automatically.
/// </summary>
public ActionButton? Button { get; set; }
/// <summary>
/// The method used to create the button.
/// </summary>
/// <param name="parent">The parent of the button.</param>
public virtual void CreateButton(Transform parent)
{
if (Button)
{
return;
}
Timer = AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay ? 0 : InitialCooldown;
TimerPaused = false;
Button = Object.Instantiate(MeetingHud.Instance.MeetingAbilityButton, parent);
Button.name = Name + "Button";
Button.OverrideText(Name.ToUpperInvariant());
Button.graphic.sprite = Sprite.LoadAsset();
Button.SetUsesRemaining(UsesLeft);
if (MaxUses <= 0)
{
Button.SetInfiniteUses();
}
if (TextOutlineColor != Color.clear)
{
SetTextOutline(TextOutlineColor);
}
var pb = Button.GetComponent<PassiveButton>();
pb.OnClick = new Button.ButtonClickedEvent();
pb.OnClick.AddListener((UnityAction)(() =>
{
ClickHandler();
}));
Button.gameObject.SetActive(Enabled(PlayerControl.LocalPlayer.Data.Role));
}
/// <summary>
/// A utility function to change the outline <see cref="Color"/> of the button's text.
/// </summary>
/// <param name="color">The new <see cref="Color"/>.</param>
public virtual void SetTextOutline(Color color)
{
Button?.buttonLabelText.SetOutlineColor(color);
}
/// <summary>
/// A utility function to override the <see cref="UnityEngine.Sprite"/> of the button.
/// </summary>
/// <param name="sprite">The new <see cref="UnityEngine.Sprite"/> to override with.</param>
public virtual void OverrideSprite(Sprite sprite)
{
if (Button != null)
{
Button.graphic.sprite = sprite;
}
}
/// <summary>
/// A utility function to override the name of the button.
/// </summary>
/// <param name="name">The new name to override with.</param>
public virtual void OverrideName(string name)
{
Button?.OverrideText(name);
}
/// <summary>
/// Set the button's timer.
/// </summary>
/// <param name="time">The time you want to set to.</param>
public virtual void SetTimer(float time)
{
Timer = Mathf.Clamp(time, -1, float.MaxValue);
}
/// <summary>
/// Increase the button's timer.
/// </summary>
/// <param name="amount">The amount you want to increase by.</param>
public virtual void IncreaseTimer(float amount)
{
SetTimer(Timer + amount);
}
/// <summary>
/// Decrease the button's timer.
/// </summary>
/// <param name="amount">The amount you want to decrease by.</param>
public virtual void DecreaseTimer(float amount)
{
SetTimer(Timer - amount);
}
/// <summary>
/// Sets whether the timer is paused or not.
/// </summary>
/// <param name="val">Whether you want to pause/resume the timer.</param>
public virtual void SetTimerPaused(bool val)
{
TimerPaused = val;
}
/// <summary>
/// Set the amount of uses this button has left.
/// </summary>
/// <param name="amount">The amount you want to set to.</param>
public virtual void SetUses(int amount)
{
UsesLeft = Mathf.Clamp(amount, 0, int.MaxValue);
Button?.SetUsesRemaining(UsesLeft);
if (Button != null)
{
Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white;
}
}
/// <summary>
/// Increase the amount of uses this button has left.
/// </summary>
/// <param name="amount">The amount you want to increase by. Default: 1.</param>
public virtual void IncreaseUses(int amount = 1)
{
SetUses(UsesLeft + amount);
}
/// <summary>
/// Decrease the amount of uses this button has left.
/// </summary>
/// <param name="amount">The amount you want to decrease by. Default: 1.</param>
public virtual void DecreaseUses(int amount = 1)
{
SetUses(UsesLeft - amount);
}
/// <summary>
/// A utility function that runs with the local player's <see cref="MeetingHud.Update"/> if the button is enabled.
/// </summary>
/// <param name="hud">the <see cref="MeetingHud"/> instance.</param>
protected virtual void FixedUpdate(MeetingHud hud)
{
}
/// <summary>
/// Callback method for the button click event.
/// </summary>
protected abstract void OnClick();
/// <summary>
/// This method determines if the button should be active or not.
/// <see langword="true"/> means the button is active, <see langword="false"/> means the button is disabled.
/// </summary>
/// <param name="role">The <see cref="RoleBehaviour"/> of the local player.</param>
/// <returns><see langword="true"/> if the button is enabled, <see langword="false"/> otherwise.</returns>
public abstract bool Enabled(RoleBehaviour? role);
/// <summary>
/// When the button is usable, this method is called to determine if the button can be clicked.
/// By default, it takes into account the timer, effect, and uses.
/// You can override it to change the behavior.
/// </summary>
/// <returns>A value that represents whether the button can be clicked or not.</returns>
public virtual bool CanClick()
{
return Timer <= 0 && CanUse();
}
/// <summary>
/// Whether the button should light up or not. This is also the base for CanClick.
/// You can override it to change the behaviour. Do not include timer in here, that is for CanClick.
/// </summary>
/// <returns>A value that represents whether the button should light up or not.</returns>
public virtual bool CanUse()
{
return !LimitedUses || UsesLeft > 0;
}
/// <summary>
/// This method handles the button click event. It is a wrapper for the <see cref="OnClick"/> method.
/// This method takes into account cooldowns, effects, and uses, before calling the <see cref="OnClick"/> method.
/// It can be overridden for custom behavior.
/// </summary>
public virtual void ClickHandler()
{
if (!CanClick())
{
return;
}
if (LimitedUses)
{
UsesLeft--;
Button?.SetUsesRemaining(UsesLeft);
}
Timer = Cooldown;
OnClick();
}
/// <summary>
/// This method is called on the <see cref="MeetingHud.Update"/> method. It is a wrapper for the <see cref="FixedUpdate"/> method.
/// By default, it handles the cooldown and effect timers, and sets the button to enabled or disabled.
/// It can be overridden for custom behavior.
/// </summary>
/// <param name="hud">The <see cref="MeetingHud"/> instance.</param>
public virtual void UpdateHandler(MeetingHud hud)
{
if (Timer >= 0 && !TimerPaused)
{
Timer -= Time.deltaTime;
}
if (Button)
{
if (CanUse())
{
Button!.SetEnabled();
}
else
{
Button!.SetDisabled();
}
Button.SetCooldownFormat(Timer, Cooldown, CooldownTimerFormatString);
Button.SetCoolDown(Timer, Cooldown);
}
FixedUpdate(hud);
}
}
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities.Assets;
using MiraAPI.Utilities.Components;
using UnityEngine;
namespace MiraAPI.MeetingAbilities;
/// <summary>
/// Class for managing all registered meeting abilities.
/// </summary>
public static class MeetingButtonManager
{
/// <summary>
/// Gets a list if==of all registered <see cref="TargetedMeetingButton"/>.
/// </summary>
public static List<TargetedMeetingButton> TargetedButtons { get; internal set; } = new();
/// <summary>
/// Gets a list if==of all registered <see cref="MeetingActionButton"/>.
/// </summary>
public static List<MeetingActionButton> UntargetedButtons { get; internal set; } = new();
internal static bool RegisterTargetedMeetingButton(Type type, MiraPluginInfo info)
{
if (!typeof(TargetedMeetingButton).IsAssignableFrom(type))
{
return false;
}
var button = Activator.CreateInstance(type) as TargetedMeetingButton;
info.InternalTargetedMeetingButtons.Add(button);
TargetedButtons.Add(button);
return true;
}
internal static bool RegisterMeetingButton(Type type, MiraPluginInfo info)
{
if (!typeof(MeetingActionButton).IsAssignableFrom(type))
{
return false;
}
var button = Activator.CreateInstance(type) as MeetingActionButton;
info.InternalMeetingButtons.Add(button);
UntargetedButtons.Add(button);
return true;
}
private static CenteredGridArrange? actionButtonsParent = null!;
public static void OnGameStart()
{
foreach (var ability in TargetedButtons.Where(x => x.UsesMode == MeetingButtonUsesMode.PerGame))
{
ability.UsesLeft = ability.MaxUses;
}
foreach (var ability in UntargetedButtons.Where(x => x.UsesMode == MeetingButtonUsesMode.PerGame))
{
ability.UsesLeft = ability.MaxUses;
}
}
/// <summary>
/// Called on <see cref="MeetingHud.Start"/>. Handles button creation, and ability uses.
/// </summary>
/// <param name="meetingHud">The <see cref="MeetingHud"/> instance.</param>
public static void OnMeetingStart(MeetingHud meetingHud)
{
// Untargeted buttons parent.
actionButtonsParent = new GameObject("ActionsButtonParent").AddComponent<CenteredGridArrange>();
actionButtonsParent.transform.SetParent(meetingHud.MeetingAbilityButton.transform.parent, meetingHud.MeetingAbilityButton.transform.parent);
actionButtonsParent.CellSize = new Vector2(0.7f, 0.7f);
actionButtonsParent.transform.localPosition = new Vector3(0, -2.1f, -10);
meetingHud.MeetingAbilityButton.transform.SetParent(actionButtonsParent.transform);
// Untargeted buttons creation.
foreach (var ability in UntargetedButtons)
{
try
{
ability.CreateButton(actionButtonsParent.transform);
if (ability.UsesMode == MeetingButtonUsesMode.PerMeeting) ability.UsesLeft = ability.MaxUses;
ability.Timer = ability.InitialCooldown;
}
catch (Exception e)
{
Error($"Failed to create meeting button {ability.Name}: {e.Message}");
}
}
// Targeted buttons creation.
foreach (var ability in TargetedButtons)
{
try
{
foreach (var playerVoteArea in meetingHud.playerStates)
{
var btn = ability.CreateButton(playerVoteArea);
btn.transform.SetParent(playerVoteArea.Buttons.transform);
playerVoteArea.CancelButton.transform.SetAsFirstSibling();
}
if (ability.UsesMode == MeetingButtonUsesMode.PerMeeting) ability.UsesLeft = ability.MaxUses;
if (ability is MultiTargetMeetingButton multiAbility) multiAbility.Targets = new();
}
catch (Exception e)
{
Error($"Failed to create targeted meeting button {ability.Name}: {e.Message}");
}
}
}
}
@@ -0,0 +1,7 @@
namespace MiraAPI.MeetingAbilities;
public enum MeetingButtonUsesMode
{
PerGame,
PerMeeting
}
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Utilities.Assets;
using UnityEngine;
using UnityEngine.UI;
namespace MiraAPI.MeetingAbilities;
/// <summary>
/// Abstract class for creating multi target meeting abilities
/// </summary>
public abstract class MultiTargetMeetingButton : TargetedMeetingButton
{
/// <summary>
/// The <see cref="LoadableAsset{Sprite}"/> used for when the button is on.
/// </summary>
public abstract LoadableAsset<Sprite> SpriteActive { get; }
/// <summary>
/// Gets the required targets count.
/// </summary>
public abstract int TargetsCount { get; }
/// <summary>
/// Gets or sets all selected targets.
/// </summary>
public Dictionary<PlayerVoteArea, MeetingAbilityBehaviour> Targets { get; set; } = new();
/// <inheritdoc />
public override void ClickHandler(MeetingAbilityBehaviour button, PlayerVoteArea playerVoteArea)
{
if (UsesLeft <= 0 && LimitedUses) return;
if (Timer > 0) return;
var toggle = !Targets.Remove(playerVoteArea);
if (toggle) Targets.Add(playerVoteArea, button);
button.Renderer.sprite = toggle ? SpriteActive.LoadAsset() : Sprite.LoadAsset();
if (Targets.Count >= TargetsCount)
{
playerVoteArea.Cancel();
HandleFinish();
return;
}
OnSelect(playerVoteArea, toggle);
}
/// <summary>
/// Handles the finish logic, increasing timer, and decreasing uses.
/// </summary>
public virtual void HandleFinish()
{
Targets = Targets.Take(TargetsCount).ToDictionary(pair => pair.Key, pair => pair.Value);
foreach (var button in Targets.Values)
{
button.Renderer.sprite = Sprite.LoadAsset();
}
Timer = Cooldown;
UsesLeft -= 1;
OnFinish();
Targets = new();
}
/// <summary>
/// Callback method for when a player is selected.
/// </summary>
/// <param name="playerVoteArea">The target <see cref="PlayerVoteArea"/>.</param>
/// <param name="toggle">Whether the button was toggled on/off.</param>
protected abstract void OnSelect(PlayerVoteArea playerVoteArea, bool toggle);
/// <summary>
/// Callback method for the button click.
/// USE <see cref="OnSelect"/> INSTEAD!!!.
/// </summary>
/// <param name="playerVoteArea">The target <see cref="PlayerVoteArea"/>.</param>
protected override void OnClick(PlayerVoteArea playerVoteArea)
{
}
/// <summary>
/// Callback method for when enough targets are chosen.
/// </summary>
public abstract void OnFinish();
/// <inheritdoc />
public override MeetingAbilityBehaviour CreateButton(PlayerVoteArea playerVoteArea)
{
Targets = new();
var button = base.CreateButton(playerVoteArea);
button.Button.OnClick = new Button.ButtonClickedEvent();
button.Button.OnClick.AddListener(new System.Action(() => { ClickHandler(button, playerVoteArea); }));
return button;
}
}
@@ -0,0 +1,155 @@
using System.Collections.Generic;
using MiraAPI.Utilities.Assets;
using UnityEngine;
using UnityEngine.UI;
namespace MiraAPI.MeetingAbilities;
/// <summary>
/// Abstract class for creating targeted meeting abilities.
/// </summary>
public abstract class TargetedMeetingButton
{
/// <summary>
/// Gets the name of the <see cref="TargetedMeetingButton"/>.
/// </summary>
public abstract string Name { get; }
/// <summary>
/// Gets the Max Uses of the button.
/// Set it to 0 for Infinite uses.
/// </summary>
public abstract int MaxUses { get; }
/// <summary>
/// Gets or sets the amount of uses left for this button.
/// </summary>
public int UsesLeft { get; set; }
/// <summary>
/// Gets a value indicating whether the <see cref="TargetedMeetingButton"/> has limited uses.
/// </summary>
public bool LimitedUses => MaxUses != 0;
/// <summary>
/// Gets the cooldown time for this button upon meeting start.
/// Defaults to <see cref="Cooldown"/> unless overridden.
/// </summary>
public virtual float InitialCooldown => Cooldown;
/// <summary>
/// Gets the cooldown time for this button after being used.
/// </summary>
public abstract float Cooldown { get; }
/// <summary>
/// Gets or sets the current cooldown timer for this button.
/// </summary>
public float Timer { get; set; }
/// <summary>
/// Gets the cooldown time for this button.
/// </summary>
public virtual string CooldownTimerFormatString => "0";
/// <summary>
/// Gets the sprite used for this button.
/// </summary>
public abstract LoadableAsset<Sprite> Sprite { get; }
/// <summary>
/// Gets the button's outline color.
/// </summary>
public abstract Color OutlineColor { get; }
/// <summary>
/// Gets the <see cref="MeetingButtonUsesMode"/> for this button.
/// </summary>
public virtual MeetingButtonUsesMode UsesMode { get; } = MeetingButtonUsesMode.PerGame;
/// <summary>
/// Determines if the button is enabled.
/// </summary>
/// <param name="r">The local player's <see cref="RoleBehaviour"/>.</param>
/// <returns>Whether the button is enabled or not.</returns>
public abstract bool Enabled(RoleBehaviour r);
/// <summary>
/// Callback method for when the button is clicked.
/// </summary>
/// <param name="playerVoteArea">The target <see cref="PlayerVoteArea"/>.</param>
protected abstract void OnClick(PlayerVoteArea playerVoteArea);
/// <summary>
/// Handles the button creation.
/// </summary>
/// <param name="playerVoteArea">The target <see cref="PlayerVoteArea"/>.</param>
/// <returns>The <see cref="MeetingAbilityBehaviour"/> instance.</returns>
public virtual MeetingAbilityBehaviour CreateButton(PlayerVoteArea playerVoteArea)
{
if (MeetingHud.Instance == null) return null!;
var btn = Object.Instantiate(MeetingHud.Instance.PlayerButtonPrefab.ConfirmButton).GetComponent<PassiveButton>();
btn.gameObject.name = Name + "Button";
var spriteRenderer = btn.GetComponent<SpriteRenderer>();
spriteRenderer.sprite = Sprite.LoadAsset();
spriteRenderer.material = new Material(HudManager.Instance.KillButton.graphic.material);
spriteRenderer.SetCooldownNormalizedUvs();
btn.gameObject.SetActive(true);
btn.transform.GetChild(0).GetComponent<SpriteRenderer>().color = OutlineColor;
btn.OnClick = new Button.ButtonClickedEvent();
var abilityBehaviour = btn.gameObject.AddComponent<MeetingAbilityBehaviour>();
abilityBehaviour.Initialize(this, playerVoteArea);
btn.OnClick.AddListener(new System.Action(() => ClickHandler(abilityBehaviour, playerVoteArea)));
Timer = InitialCooldown;
return abilityBehaviour;
}
/// <summary>
/// Called every <see cref="MeetingHud.Update"/>.
/// Handles the timer logic.
/// </summary>
public virtual void UpdateHandler()
{
Timer -= Time.deltaTime;
Timer = Mathf.Clamp(Timer, 0, int.MaxValue);
FixedUpdate();
}
/// <summary>
/// Callback method for handling logic every frame.
/// </summary>
public virtual void FixedUpdate()
{
}
/// <summary>
/// Called when the button is clicked
/// Handles increasing cooldown and decreasing uses remaining.
/// </summary>
/// <param name="button">The <see cref="MeetingAbilityBehaviour"/> instance.</param>
/// <param name="playerVoteArea">The target <see cref="PlayerVoteArea"/>.</param>
public virtual void ClickHandler(MeetingAbilityBehaviour button, PlayerVoteArea playerVoteArea)
{
if (UsesLeft <= 0 && LimitedUses) return;
if (Timer > 0) return;
Timer = Cooldown;
UsesLeft--;
playerVoteArea.Cancel();
OnClick(playerVoteArea);
}
/// <summary>
/// Determines whether the target is valid.
/// </summary>
/// <param name="playerVoteArea">The target <see cref="PlayerVoteArea"/>.</param>
/// <returns>Whether the target is valid or not.</returns>
public virtual bool IsTargetValid(PlayerVoteArea playerVoteArea)
{
return playerVoteArea != null && playerVoteArea.PlayerId.Value != PlayerControl.LocalPlayer.Data.PlayerId;
}
}
@@ -0,0 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace MiraAPI.MeetingAbilities;
/// <summary>
/// A utility class to get the instance of a <see cref="TargetedMeetingButton"/>.
/// </summary>
/// <typeparam name="T">The type of the button you are trying to access.</typeparam>
public static class TargetedMeetingButtonSingleton<T> where T : TargetedMeetingButton
{
private static T? _instance;
/// <summary>
/// Gets the instance of the <typeparamref name="T"/> button.
/// </summary>
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a targeted meeting button.")]
public static T Instance => _instance ??= MeetingButtonManager.TargetedButtons.OfType<T>().Single();
}
@@ -1,7 +1,9 @@
using System.Reflection;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.MeetingAbilities;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
@@ -18,6 +20,7 @@ public static class FreeplayRoundStartPatch
{
if (!__result)
{
MeetingButtonManager.OnGameStart();
MiraEventManager.InvokeEvent(new RoundStartEvent(true));
}
}
+5 -1
View File
@@ -1,8 +1,10 @@
using System.Reflection;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.MeetingAbilities;
using MiraAPI.GameModes;
using MiraAPI.Roles;
using MiraAPI.Utilities;
@@ -126,7 +128,9 @@ public static class IntroCutscenePatches
{
introCutscene = __instance.Cast<IntroCutscene>();
}
Info("IntroCutscene ended");
MeetingButtonManager.OnGameStart();
MiraEventManager.InvokeEvent(new IntroEndEvent(introCutscene));
var @event = new BeforeRoundStartEvent(true);
+53 -2
View File
@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
@@ -6,9 +8,12 @@ using InnerNet;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Events.Vanilla.Meeting.Voting;
using MiraAPI.MeetingAbilities;
using MiraAPI.Modifiers;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Components;
using MiraAPI.Voting;
using UnityEngine;
namespace MiraAPI.Patches.Voting;
@@ -34,10 +39,46 @@ internal static class MeetingHudPatches
return true;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.Select))]
public static bool VoteAreaSelectPatch(PlayerVoteArea __instance)
{
if (PlayerControl.LocalPlayer.Data.IsDead || __instance.AmDead || !(bool) (UnityEngine.Object) __instance.Parent)
return false;
JudgeRole? judgeRole = PlayerControl.LocalPlayer.Data.Role.TryCast<JudgeRole>();
bool flag = judgeRole && PlayerControl.LocalPlayer.PlayerId != __instance.PlayerId;
__instance.JudgeOverruleButton?.gameObject.SetActive(flag);
if (__instance.VoteComplete || !__instance.Parent.Select((byte) __instance.PlayerId))
return false;
__instance.Buttons.SetActive(true);
float startPos = __instance.AnimateButtonsFromLeft ? 0.2f : 1.95f;
Il2CppSystem.Collections.Generic.List<UiElement> selectableElements = new Il2CppSystem.Collections.Generic.List<UiElement>();
foreach (var btn in __instance.Buttons.GetComponentsInChildren<PassiveButton>())
{
selectableElements.Add(btn);
}
for (int i = 0; i < selectableElements.Count; i++)
{
var button = selectableElements[i];
float endPos = 1.3f - 0.65f * i;
float duration = 0.25f + 0.1f * i;
__instance.StartCoroutine(Effects.All(Effects.Lerp(duration, (Action<float>) (t =>
button.transform.localPosition = Vector2.Lerp(Vector2.right * startPos, Vector2.right * endPos, Effects.ExpOut(t))))));
}
ControllerManager.Instance.OpenOverlayMenu(__instance.name, __instance.CancelButton, selectableElements[1], selectableElements);
return false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(MeetingHud.Start))]
public static void MeetingHudStartPatch(MeetingHud __instance)
{
MeetingButtonManager.OnMeetingStart(__instance);
foreach (var plr in PlayerControl.AllPlayerControls)
{
var voteData = plr.GetVoteData();
@@ -78,6 +119,16 @@ internal static class MeetingHudPatches
[HarmonyPatch(nameof(MeetingHud.Update))]
public static void ForceSkipPatch(MeetingHud __instance)
{
foreach (var targetedMeetingAbility in MeetingButtonManager.TargetedButtons.Where(x => x.Enabled(PlayerControl.LocalPlayer.Data.Role)))
{
targetedMeetingAbility.UpdateHandler();
}
foreach (var meetingAbility in MeetingButtonManager.UntargetedButtons.Where(x => x.Enabled(PlayerControl.LocalPlayer.Data.Role)))
{
meetingAbility.UpdateHandler(__instance);
}
if (__instance.state is not (MeetingHud.MeetingStates.NotVoted or MeetingHud.MeetingStates.Voted))
{
return;
@@ -106,7 +157,7 @@ internal static class MeetingHudPatches
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.Select))]
public static bool SelectPatch(MeetingHud __instance, int suspectStateIdx)
public static bool MeetingHudSelectPatch(MeetingHud __instance, int suspectStateIdx)
{
var voteData = PlayerControl.LocalPlayer.GetVoteData();
+17
View File
@@ -5,6 +5,7 @@ using BepInEx.Configuration;
using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.MeetingAbilities;
using MiraAPI.Modifiers;
using MiraAPI.Presets;
@@ -58,6 +59,16 @@ public class MiraPluginInfo
/// </summary>
public IReadOnlyCollection<CustomActionButton> Buttons { get; private set; } = null!;
/// <summary>
/// Gets a <see cref="IReadOnlyCollection{T}"/> of this <see cref="IMiraPlugin"/>'s <see cref="TargetedMeetingButton"/>.
/// </summary>
public IReadOnlyCollection<TargetedMeetingButton> TargetedMeetingButtons { get; private set; } = null!;
/// <summary>
/// Gets a <see cref="IReadOnlyCollection{T}"/> of this <see cref="IMiraPlugin"/>'s <see cref="MeetingActionButton"/>.
/// </summary>
public IReadOnlyCollection<MeetingActionButton> MeetingButtons { get; private set; } = null!;
/// <summary>
/// Gets a <see cref="ReadOnlyDictionary{TKey, TValue}"/> of this <see cref="IMiraPlugin"/>'s <see cref="OptionPreset"/>s.
/// </summary>
@@ -71,6 +82,8 @@ public class MiraPluginInfo
Options = [..InternalOptions];
Roles = new ReadOnlyDictionary<ushort, RoleBehaviour>(InternalRoles);
Buttons = [..InternalButtons];
MeetingButtons = [..InternalMeetingButtons];
TargetedMeetingButtons = [..InternalTargetedMeetingButtons];
}
internal List<OptionPreset> InternalPresets { get; } = [];
@@ -86,6 +99,10 @@ public class MiraPluginInfo
internal List<CustomActionButton> InternalButtons { get; } = [];
internal List<TargetedMeetingButton> InternalTargetedMeetingButtons { get; } = [];
internal List<MeetingActionButton> InternalMeetingButtons { get; } = [];
/// <summary>
/// Gets the plugin's ID, as defined in the plugin's BepInEx metadata.
/// </summary>
@@ -18,6 +18,7 @@ using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.LocalSettings;
using MiraAPI.LocalSettings.Attributes;
using MiraAPI.MeetingAbilities;
using MiraAPI.Modifiers;
using MiraAPI.Patches.Options;
using MiraAPI.Presets;
@@ -223,6 +224,17 @@ public sealed class MiraPluginManager
continue;
}
if (RegisterMeetingAbility(type, info))
{
continue;
}
if (RegisterTargetedMeetingAbility(type, info))
{
continue;
}
if (RegisterGameModeAttribute(type, info))
{
continue;
@@ -583,4 +595,29 @@ public sealed class MiraPluginManager
Error($"Failed to register keybind class {type.Name}: {e}");
}
}
private static bool RegisterMeetingAbility(Type type, MiraPluginInfo info)
{
try
{
return MeetingButtonManager.RegisterMeetingButton(type, info);
}
catch (Exception e)
{
Error($"Failed to register meeting ability {type.Name}: {e}");
return false;
}
}
private static bool RegisterTargetedMeetingAbility(Type type, MiraPluginInfo info)
{
try
{
return MeetingButtonManager.RegisterTargetedMeetingButton(type, info);
}
catch (Exception e)
{
Error($"Failed to register targeted meeting ability {type.Name}: {e}");
return false;
}
}
}
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Reactor.Utilities.Attributes;
using UnityEngine;
namespace MiraAPI.Utilities.Components;
/// <summary>
/// Utility component for arranging objects in a centered layout, similar to <see cref="GridArrange"/>.
/// </summary>
/// <param name="ptr"></param>
[RegisterInIl2Cpp]
public class CenteredGridArrange(IntPtr ptr) : MonoBehaviour(ptr)
{
/// <summary>
/// Gets or sets the cell size, which is used for spacing.
/// </summary>
public Vector2 CellSize;
/// <summary>
/// Gets or sets the maximum amount of columns.
/// </summary>
public int MaxColumns = 6;
private List<Transform> cells;
private static List<Transform> currentChildren = new List<Transform>();
private void Start()
{
cells = new List<Transform>();
GetChildsActive();
CheckCurrentChildren();
}
private void FixedUpdate() => CheckCurrentChildren();
private void CheckCurrentChildren()
{
GetChildsActive();
if (cells.SequenceEqual(currentChildren))
return;
cells.Clear();
foreach (Transform currentChild in currentChildren)
cells.Add(currentChild);
ArrangeChilds();
}
private void GetChildsActive()
{
currentChildren.Clear();
foreach (var obj in transform)
{
var child = obj.TryCast<Transform>();
if (child == null) continue;
if (child.gameObject.activeSelf)
currentChildren.Add(child);
}
}
private void ArrangeChilds()
{
if (cells.Count == 0)
return;
int totalRows = Mathf.CeilToInt((float)cells.Count / MaxColumns);
float totalHeight = (totalRows - 1) * CellSize.y;
float startY = transform.position.y + totalHeight * 0.5f;
for (int index = 0; index < cells.Count; ++index)
{
int row = index / MaxColumns;
int rowStartIndex = row * MaxColumns;
int itemsInRow = Mathf.Min(MaxColumns, cells.Count - rowStartIndex);
int col = index - rowStartIndex;
float rowWidth = (itemsInRow - 1) * CellSize.x;
float startX = transform.position.x - rowWidth * 0.5f;
float x = startX + col * CellSize.x;
float y = startY - row * CellSize.y;
Transform cell = cells[index];
cell.position = new Vector3(x, y, cell.position.z);
}
}
}