WIP Targeted meeting abilities API

This commit is contained in:
wanderingpix committed 2026-08-20 22:41:26 +01:00
1 parent f96f75cea1
commit e381de033e
11 files changed
+376 -3

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,28 @@
using MiraAPI.MeetingAbilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.MeetingAbilities;
public class ShootButton : TargetedMeetingButton
{
public override string Name => "Shooooooot";
public override int MaxUses => 0;
public override float Cooldown => 10;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.SharpshootButton;
public override MeetingButtonUsesMode ButtonUsesMode => MeetingButtonUsesMode.PerGame;
public override bool Enabled(RoleBehaviour r)
{
return true;
}
protected override void OnClick(PlayerVoteArea playerVoteArea)
{
playerVoteArea.SetDisabled();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -0,0 +1,77 @@
using System;
using System.Globalization;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
namespace MiraAPI.MeetingAbilities;
[RegisterInIl2Cpp]
public class MeetingAbilityBehaviour(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
private bool _init = false;
private TargetedMeetingButton _button = null!;
public PassiveButton Button;
public PlayerVoteArea VoteArea;
public SpriteRenderer Renderer;
public TextMeshPro CooldownText;
public TextMeshPro UsesText;
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;
}
private void OnEnable()
{
if (!_init) return;
Button.enabled = true;
Renderer.enabled = true;
if (_button.Enabled(PlayerControl.LocalPlayer.Data.Role) && _button.IsTargetValid(VoteArea)) return;
Button.enabled = false;
Renderer.enabled = false;
}
public void Update()
{
if (!_init) return;
if (_button.Timer < 0)
{
Renderer.material.SetFloat("_Desat", 0.0f);
CooldownText.text = string.Empty;
UsesText.text = _button.LimitedUses ? _button.UsesRemaining.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.Cooldown);
}
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,7 @@
namespace MiraAPI.MeetingAbilities;
public enum MeetingButtonUsesMode
{
PerGame,
PerMeeting
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.MeetingAbilities;
public static class TargetedMeetingAbilityManager
{
/// <summary>
/// Gets a list if==of all registered <see cref="TargetedMeetingButton"/>.
/// </summary>
public static List<TargetedMeetingButton> Buttons { get; internal set; } = new();
internal static bool RegisterMeetingAbility(Type type, MiraPluginInfo info)
{
if (!typeof(TargetedMeetingButton).IsAssignableFrom(type))
{
return false;
}
var button = Activator.CreateInstance(type) as TargetedMeetingButton;
info.InternalMeetingAbilities.Add(button);
Buttons.Add(button);
return true;
}
}
@@ -0,0 +1,140 @@
using System.Collections.Generic;
using MiraAPI.Utilities.Assets;
using UnityEngine;
using UnityEngine.UI;
namespace MiraAPI.MeetingAbilities;
/// <summary>
/// Abstract class for creating custom 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 UsesRemaining { 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.
/// </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 <see cref="MeetingButtonUsesMode"/> for this button.
/// </summary>
public virtual MeetingButtonUsesMode ButtonUsesMode { 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.OnClick = new Button.ButtonClickedEvent();
btn.OnClick.AddListener(new System.Action(() => ClickHandler(playerVoteArea)));
var abilityBehaviour = btn.gameObject.AddComponent<MeetingAbilityBehaviour>();
abilityBehaviour.Initialize(this, playerVoteArea);
Timer = Cooldown;
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="playerVoteArea">The target <see cref="PlayerVoteArea"/>.</param>
public virtual void ClickHandler(PlayerVoteArea playerVoteArea)
{
if (UsesRemaining <= 0 && LimitedUses) return;
if (Timer > 0) return;
Timer = Cooldown;
UsesRemaining--;
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;
}
}
+8 -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.Roles;
using MiraAPI.Utilities;
@@ -128,6 +130,11 @@ public static class IntroCutscenePatches
var @event = new BeforeRoundStartEvent(true);
MiraEventManager.InvokeEvent(@event);
foreach (var ability in TargetedMeetingAbilityManager.Buttons.Where(x => x.ButtonUsesMode == MeetingButtonUsesMode.PerGame))
{
ability.UsesRemaining = ability.MaxUses;
}
if (@event.IsCancelled) return;
MiraEventManager.InvokeEvent(new RoundStartEvent(true));
}
+63 -2
View File
@@ -1,13 +1,17 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
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.Voting;
using UnityEngine;
namespace MiraAPI.Patches.Voting;
@@ -33,6 +37,54 @@ internal static class MeetingHudPatches
return true;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.Start))]
public static void VoteAreaStartPostfix(PlayerVoteArea __instance)
{
foreach (var targetedMeetingAbility in TargetedMeetingAbilityManager.Buttons)
{
var btn = targetedMeetingAbility.CreateButton(__instance);
btn.transform.SetParent(__instance.Buttons.transform);
btn.gameObject.SetActive(targetedMeetingAbility.Enabled(PlayerControl.LocalPlayer.Data.Role) && targetedMeetingAbility.IsTargetValid(__instance));
}
__instance.CancelButton.transform.SetAsFirstSibling();
}
[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)
@@ -54,6 +106,10 @@ internal static class MeetingHudPatches
}
}
foreach (var ability in TargetedMeetingAbilityManager.Buttons.Where(x => x.ButtonUsesMode == MeetingButtonUsesMode.PerMeeting))
{
ability.UsesRemaining = ability.MaxUses;
}
var @event = new StartMeetingEvent(__instance);
MiraEventManager.InvokeEvent(@event);
}
@@ -77,6 +133,11 @@ internal static class MeetingHudPatches
[HarmonyPatch(nameof(MeetingHud.Update))]
public static void ForceSkipPatch(MeetingHud __instance)
{
foreach (var targetedMeetingAbility in TargetedMeetingAbilityManager.Buttons)
{
targetedMeetingAbility.UpdateHandler();
}
if (__instance.state is not (MeetingHud.MeetingStates.NotVoted or MeetingHud.MeetingStates.Voted))
{
return;
@@ -105,7 +166,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();
+3
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;
@@ -78,6 +79,8 @@ public class MiraPluginInfo
internal List<CustomActionButton> InternalButtons { get; } = [];
internal List<TargetedMeetingButton?> InternalMeetingAbilities { get; } = [];
/// <summary>
/// Gets the plugin's ID, as defined in the plugin's BepInEx metadata.
/// </summary>
@@ -16,6 +16,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;
@@ -117,6 +118,11 @@ public sealed class MiraPluginManager
{
continue;
}
if (RegisterMeetingAbility(type, info))
{
continue;
}
RegisterColorClasses(type);
RegisterKeybinds(type, plugin);
@@ -443,4 +449,17 @@ public sealed class MiraPluginManager
Error($"Failed to register keybind class {type.Name}: {e}");
}
}
private static bool RegisterMeetingAbility(Type type, MiraPluginInfo info)
{
try
{
return TargetedMeetingAbilityManager.RegisterMeetingAbility(type, info);
}
catch (Exception e)
{
Error($"Failed to register meeting ability {type.Name}: {e}");
return false;
}
}
}