diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1b19d53..881c731 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,11 +2,11 @@ name: NewMod CI
on:
push:
- branches: [main, dev]
+ branches: [main, dev, v1.3.0]
tags:
- 'v*'
pull_request:
- branches: [main, dev]
+ branches: [main, dev, v1.3.0]
workflow_dispatch:
jobs:
diff --git a/.gitignore b/.gitignore
index 74c2aaa..7c0fbec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ obj/
References/
NewMod/Components/Minigames
NewMod/Components/Hidden.cs
+NewMod/Private
/packages/
riderModule.iml
.idea
diff --git a/NewMod/Achievements/NewModAchievements.cs b/NewMod/Achievements/NewModAchievements.cs
new file mode 100644
index 0000000..76a1b3e
--- /dev/null
+++ b/NewMod/Achievements/NewModAchievements.cs
@@ -0,0 +1,53 @@
+/*using AchievementsAPI.API;
+using UnityEngine;
+
+namespace NewMod.Achievements;
+
+public sealed class NewModAchievementsTab : AchievementsTab
+{
+ public override string Name => "NewMod";
+
+ public override Color GetTabColor()
+ {
+ return new Color32(69, 38, 105, 255);
+ }
+
+ public override Sprite GetIcon()
+ {
+ return NewModAsset.NMIcon.LoadAsset();
+ }
+
+ public static BaseAchievement WelcomeToNewMod { get; } = new(
+ "Welcome to NewMod",
+ "Start a Freeplay or online NewMod match.",
+ NewModAsset.NMIcon.LoadAsset());
+
+ public static CountAchievement ThreeInARow { get; } = new(
+ "Three in a Row",
+ "Complete three online NewMod matches in one session.",
+ NewModAsset.OG_NewModHat.LoadAsset(),
+ 0,
+ 3,
+ AchPersistence.ThroughoutSessions,
+ rarity: 3);
+
+ public static BaseAchievement TraceEvidence { get; } = new(
+ "Trace Evidence",
+ "As Warden, seal a location and investigate its Residual Trace.",
+ NewModAsset.ResidualTrace.LoadAsset(),
+ rarity: 2);
+
+ public static BaseAchievement EventHorizonDenied { get; } = new(
+ "Event Horizon Denied",
+ "Enter the Crimson Vortex and successfully escape it.",
+ NewModAsset.CrismonIcon.LoadAsset(),
+ rarity: 2);
+
+ public static BaseAchievement TrustButVerify { get; } = new(
+ "Trust, But Verify",
+ "As Verifier, receive a Confirmed or Denied result.",
+ NewModAsset.VerifyIcon.LoadAsset(),
+ rarity: 1);
+}
+*/
+
diff --git a/NewMod/Achievements/NewModAchievementsTracker.cs b/NewMod/Achievements/NewModAchievementsTracker.cs
new file mode 100644
index 0000000..c43a3b7
--- /dev/null
+++ b/NewMod/Achievements/NewModAchievementsTracker.cs
@@ -0,0 +1,61 @@
+using System.Linq;
+using HarmonyLib;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Gameplay;
+using NewMod.Seasons;
+using UnityEngine;
+
+namespace NewMod.Achievements;
+
+public static class NewModAchievementTracker
+{
+ private static bool _onlineMatchActive;
+
+ [RegisterEvent]
+ public static void OnRoundStart(RoundStartEvent evt)
+ {
+ _onlineMatchActive = false;
+
+ if (!evt.TriggeredByIntro || Application.platform == RuntimePlatform.Android)
+ return;
+
+ if (!SeasonManager.AvailableAchievementTabTypes.Contains(typeof(PreseasonAchievementsTab)))
+ return;
+
+ PreseasonAchievementsTab.WelcomeToNewMod.Unlock();
+ _onlineMatchActive = AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame;
+ }
+
+ [RegisterEvent]
+ public static void OnGameEnd(GameEndEvent evt)
+ {
+ if (!_onlineMatchActive)
+ return;
+
+ _onlineMatchActive = false;
+
+ if (Application.platform == RuntimePlatform.Android)
+ return;
+
+ if (!SeasonManager.AvailableAchievementTabTypes.Contains(typeof(PreseasonAchievementsTab))) return;
+
+ if (!PreseasonAchievementsTab.ThreeInARow.Unlocked)
+ PreseasonAchievementsTab.ThreeInARow.Increment(1, false);
+ }
+
+ [HarmonyPatch(typeof(GameData), nameof(GameData.OnDisconnected))]
+ public static class GameDataOnDisconnectedPatch
+ {
+ [HarmonyPostfix]
+ public static void Postfix()
+ {
+ if (!_onlineMatchActive)
+ return;
+
+ _onlineMatchActive = false;
+
+ if (!PreseasonAchievementsTab.ThreeInARow.Unlocked)
+ PreseasonAchievementsTab.ThreeInARow.SetValue(0, false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Achievements/PreseasonAchievements.cs b/NewMod/Achievements/PreseasonAchievements.cs
new file mode 100644
index 0000000..209f663
--- /dev/null
+++ b/NewMod/Achievements/PreseasonAchievements.cs
@@ -0,0 +1,31 @@
+using System.Linq;
+using AchievementsAPI.API;
+using NewMod.Seasons;
+using UnityEngine;
+
+namespace NewMod.Achievements;
+
+public sealed class PreseasonAchievementsTab : AchievementsTab
+{
+ public override string Name => "NewMod Preseason ALPHA";
+
+ public override bool IsSelectable => SeasonManager.AvailableAchievementTabTypes.Contains(typeof(PreseasonAchievementsTab));
+
+ public static BaseAchievement WelcomeToNewMod { get; } = new("Welcome to NewMod", "Start a Freeplay or online NewMod match.", NewModAsset.NMIcon.LoadAsset());
+
+ public static CountAchievement ThreeInARow { get; } = new("Three in a Row", "Complete three online NewMod matches in one session.", NewModAsset.OG_NewModHat.LoadAsset(), 0, 3, AchPersistence.ThroughoutSessions, 3);
+
+ public static BaseAchievement EventHorizonDenied { get; } = new("Event Horizon Denied", "Enter the Crimson Vortex and successfully escape it.", NewModAsset.CrismonIcon.LoadAsset(), 2);
+
+ public static BaseAchievement TrustButVerify { get; } = new("Trust, But Verify", "As Verifier, receive a Confirmed or Denied result.", NewModAsset.VerifyIcon.LoadAsset(), 1);
+
+ public override Color GetTabColor()
+ {
+ return new Color32(69, 38, 105, 255);
+ }
+
+ public override Sprite GetIcon()
+ {
+ return NewModAsset.NMIcon.LoadAsset();
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Aegis/AegisButton.cs b/NewMod/Buttons/Aegis/AegisButton.cs
deleted file mode 100644
index cd8fc92..0000000
--- a/NewMod/Buttons/Aegis/AegisButton.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities.Assets;
-using NewMod.Options.Roles.AegisOptions;
-using UnityEngine;
-using AG = NewMod.Roles.CrewmateRoles.Aegis;
-using NewMod.Utilities;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.Aegis
-{
- ///
- /// Custom action button for the Aegis role. Places a configurable shield zone.
- ///
- public class AegisButton : CustomActionButton
- {
- ///
- /// Gets the display name for this button.
- ///
- public override string Name => "Sentinel Ward";
-
- ///
- /// Cooldown is driven by AegisOptions.
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.AegisCooldown;
-
- ///
- /// Maximum number of uses is driven by AegisOptions.
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.MaxCharges;
-
- ///
- /// Button location on HUD.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomLeft;
-
- ///
- /// Default keybind for Aegis.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// No “hold” effect, instant cast.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// Icon for the button (replace with your actual asset).
- ///
- public override LoadableAsset Sprite => NewModAsset.Shield;
-
- ///
- /// Enabled only for the Aegis role.
- ///
- public override bool Enabled(RoleBehaviour role) => role is AG;
-
- ///
- /// On click, place the Aegis shield at the player's current position using the configured settings.
- ///
- protected override void OnClick()
- {
- var player = PlayerControl.LocalPlayer;
-
- AegisUtilities.ActivateShield(player, (Vector2)player.transform.position);
- }
- }
-}
diff --git a/NewMod/Buttons/Edgeveil/ArcButton.cs b/NewMod/Buttons/Edgeveil/ArcButton.cs
deleted file mode 100644
index 3414baa..0000000
--- a/NewMod/Buttons/Edgeveil/ArcButton.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities.Assets;
-using NewMod.Options.Roles.EdgeveilOptions;
-using EV = NewMod.Roles.ImpostorRoles.Edgeveil;
-using Rewired;
-using UnityEngine;
-using NewMod.Components.ScreenEffects;
-using NewMod.Utilities;
-using Reactor.Utilities;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.Edgeveil
-{
- ///
- /// Defines a custom action button for Edgeviel's Arc ability.
- ///
- public class ArcButton : CustomActionButton
- {
- ///
- /// The name displayed on the button.
- ///
- public override string Name => "Arc";
-
- ///
- /// Gets the cooldown time for this button, based on .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.SlashCooldown;
-
- ///
- /// Gets the maximum number of uses for this button (0 = infinite).
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.SlashMaxUses;
-
- ///
- /// Determines how long the effect lasts. For Arc, none.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// Default keybind for Edgeveil's Arc ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// Defines where on the screen this button should appear.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomLeft;
-
- ///
- /// The visual icon for this button, set to the Edgeveil Arc sprite asset.
- ///
- public override LoadableAsset Sprite => NewModAsset.SlashIcon;
-
- ///
- /// Invoked when the Arc button is clicked.
- ///
- protected override void OnClick()
- {
- var player = PlayerControl.LocalPlayer;
-
- bool flipLeft = player.cosmetics.currentBodySprite.BodySprite.flipX;
- Vector2 dir = flipLeft ? Vector2.left : Vector2.right;
-
- float spawnOffset = 0.55f;
- var spawnPos = player.GetTruePosition() + dir * spawnOffset;
-
- var tray = SlashTray.CreateTray();
- tray.transform.SetParent(ShipStatus.Instance.transform, worldPositionStays: true);
- tray.transform.SetPositionAndRotation(
- new Vector3(spawnPos.x, spawnPos.y, player.transform.position.z),
- Quaternion.FromToRotation(Vector3.right, new Vector3(dir.x, dir.y, 0f))
- );
-
- tray.Owner = player;
- tray.SetMotion(dir, OptionGroupSingleton.Instance.SlashSpeed);
-
- float effectDuration = OptionGroupSingleton.Instance.EffectDuration;
-
- HudManager.Instance.PlayerCam.ShakeScreen(effectDuration, 2f);
- }
- ///
- /// Determines whether this button is enabled for the role, returning true if the role is .
- ///
- /// The current player's role.
- /// True if the role is Edgeveil; otherwise false.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is EV;
- }
- }
-}
diff --git a/NewMod/Buttons/EnergyThief/DrainButton.cs b/NewMod/Buttons/EnergyThief/DrainButton.cs
deleted file mode 100644
index 0900a61..0000000
--- a/NewMod/Buttons/EnergyThief/DrainButton.cs
+++ /dev/null
@@ -1,125 +0,0 @@
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities;
-using MiraAPI.Utilities.Assets;
-using NewMod.Options.Roles.EnergyThiefOptions;
-using ET = NewMod.Roles.NeutralRoles.EnergyThief;
-using UnityEngine;
-using NewMod.Utilities;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.EnergyThief
-{
- ///
- /// Defines a custom action button for the role.
- ///
- public class DrainButton : CustomActionButton
- {
- ///
- /// Gets the display name for this button.
- ///
- public override string Name => "Drain";
-
- ///
- /// Gets the cooldown value for this button, based on EnergyThiefOptions.
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.DrainCooldown;
-
- ///
- /// Gets the maximum number of uses for this button, based on EnergyThiefOptions.
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.DrainMaxUses;
-
- ///
- /// The on-screen position of this button.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomRight;
-
- ///
- /// Default keybind for EnergyThief's Drain ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// The duration of the effect applied by this button; in this case, zero.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// Gets the sprite asset for this button. Currently set to an empty sprite.
- ///
- public override LoadableAsset Sprite => MiraAssets.Empty;
-
- ///
- /// Determines the target for this button action.
- ///
- /// The closest valid PlayerControl, or null if none.
- public override PlayerControl GetTarget()
- {
- return PlayerControl.LocalPlayer.GetClosestPlayer(true, Distance, false, false, p => !p.Data.IsDead && !p.Data.Disconnected);
- }
-
- ///
- /// Enables or disables an outline around the targeted player.
- ///
- /// Whether to enable or disable the outline.
- public override void SetOutline(bool active)
- {
- Target?.cosmetics.SetOutline(active, new Il2CppSystem.Nullable(Color.magenta));
- }
-
- ///
- /// Determines whether the targeted player is valid for draining.
- ///
- /// The candidate player.
- /// Always true in this implementation.
- public override bool IsTargetValid(PlayerControl target)
- {
- return true;
- }
-
- ///
- /// Specifies whether the button is enabled for a given role.
- ///
- /// The role to check.
- /// True if the role is EnergyThief, otherwise false.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is ET;
- }
-
- ///
- /// Invoked when the button is clicked. Records a drain count, queues a pending effect,
- /// notifies the local player, and locks the button from reuse until the meeting ends.
- ///
- protected override void OnClick()
- {
- var clip = NewModAsset.DrainSound.LoadAsset();
-
- PendingEffectManager.AddPendingEffect(Target);
- SoundManager.Instance.PlaySound(clip, false, 1f, null);
-
- Utils.RecordDrainCount(PlayerControl.LocalPlayer);
-
- if (PlayerControl.LocalPlayer.AmOwner)
- {
- HudManager.Instance.Notifier.AddDisconnectMessage($"The Drain effect will be applied to {Target.Data.PlayerName} after the meeting ends.");
- }
- Utils.waitingPlayers.Add(PlayerControl.LocalPlayer);
- }
-
- ///
- /// Checks if the button can be used, preventing usage if the local player
- /// is already in the waitingPlayers set.
- ///
- /// True if usable, otherwise false.
- public override bool CanUse()
- {
- if (Utils.waitingPlayers.Contains(PlayerControl.LocalPlayer))
- {
- return false;
- }
- return base.CanUse();
- }
- }
-}
diff --git a/NewMod/Buttons/Injector/InjectButton.cs b/NewMod/Buttons/Injector/InjectButton.cs
deleted file mode 100644
index 63a4ba8..0000000
--- a/NewMod/Buttons/Injector/InjectButton.cs
+++ /dev/null
@@ -1,96 +0,0 @@
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities.Assets;
-using UnityEngine;
-using NewMod.Roles.NeutralRoles;
-using NewMod.Options.Roles.InjectorOptions;
-using MiraAPI.Utilities;
-using System;
-using static NewMod.Utilities.Utils;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.Injector
-{
- ///
- /// Represents the serum injection button for the Injector role.
- /// Allows injecting a random serum into nearby players.
- ///
- public class InjectButton : CustomActionButton
- {
- ///
- /// The name displayed on the button (if any).
- ///
- public override string Name => "Inject";
-
- ///
- /// Cooldown time between uses, configured via .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.SerumCooldown;
-
- ///
- /// Maximum allowed injections, configured via .
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.MaxSerumUses;
-
- ///
- /// Effect duration — unused here since injection is instant.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// Screen location of the button on the HUD.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomLeft;
-
- ///
- /// Default keybind for Injector's Inject ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// Sprite/icon displayed on the button.
- ///
- public override LoadableAsset Sprite => NewModAsset.InjectButton;
-
- ///
- /// Returns the closest valid player target within range,
- /// used by the Injector to determine who can be injected.
- ///
- /// The nearest PlayerControl instance, or null if none is in range.
- public override PlayerControl GetTarget()
- {
- return PlayerControl.LocalPlayer.GetClosestPlayer(true, Distance, false);
- }
-
- ///
- /// Sets an outline around the target player to visually indicate interaction,
- /// such as highlighting a valid injection target for the Injector role.
- ///
- /// True to show the outline; false to hide it.
- public override void SetOutline(bool active)
- {
- Target?.cosmetics.SetOutline(active, new Il2CppSystem.Nullable(Palette.AcceptedGreen));
- }
-
- ///
- /// Determines whether this button is available for the current role.
- ///
- /// The current player's role.
- /// True only for the Injector role.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is InjectorRole;
- }
-
- ///
- /// Called when the button is clicked. Applies a serum to the closest valid target.
- ///
- protected override void OnClick()
- {
- var values = (SerumType[])Enum.GetValues(typeof(SerumType));
- var serum = values[UnityEngine.Random.Range(0, values.Length)];
-
- RpcApplySerum(PlayerControl.LocalPlayer, Target, serum);
- }
- }
-}
diff --git a/NewMod/Buttons/Necromancer/ReviveButton.cs b/NewMod/Buttons/Necromancer/ReviveButton.cs
deleted file mode 100644
index e9b717b..0000000
--- a/NewMod/Buttons/Necromancer/ReviveButton.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities.Assets;
-using NewMod.Options.Roles.NecromancerOptions;
-using NewMod.Roles.ImpostorRoles;
-using UnityEngine;
-using NewMod.Utilities;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.Necromancer
-{
- ///
- /// Defines a custom action button for the role.
- ///
- public class ReviveButton : CustomActionButton
- {
- ///
- /// The name displayed on the button. Intentionally left empty to show an existing name elsewhere.
- ///
- public override string Name => ""; // It's currently empty since the button has already a name on it
-
- ///
- /// Gets the cooldown time for this button, based on .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.ButtonCooldown;
-
- ///
- /// Gets the maximum number of uses for this button, based on .
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.AbilityUses;
-
- ///
- /// Determines how long the effect from clicking the button lasts. In this case, no duration is set.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// Default keybind for Necromancer's Revive ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// Defines where on the screen this button should appear.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomLeft;
-
- ///
- /// The visual icon for this button, set to the necromancer sprite asset.
- ///
- public override LoadableAsset Sprite => NewModAsset.NecromancerButton;
-
- ///
- /// Invoked when the revive button is clicked. Plays a sound and revives the nearest dead body.
- ///
- protected override void OnClick()
- {
- SoundManager.Instance.PlaySound(NewModAsset.ReviveSound?.LoadAsset(), false, 2f);
-
- var closestBody = Utils.GetClosestBody();
- if (closestBody != null)
- {
- Utils.RpcRevive(closestBody);
- }
- }
-
- ///
- /// Determines whether this button is enabled for the role, returning true if the role is .
- ///
- /// The current player's role.
- /// True if the role is Necromancer; otherwise false.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is NecromancerRole;
- }
-
- ///
- /// Checks whether the player can currently use the revive button, ensuring cooldowns, ability uses, and conditions are met.
- ///
- /// True if all requirements to use this button are met; otherwise false.
- public override bool CanUse()
- {
- bool isTimerDone = Timer <= 0;
- bool hasUsesLeft = UsesLeft > 0;
- var closestBody = Utils.GetClosestBody();
- bool isNearDeadBody = closestBody != null;
- bool isFakeBody = isNearDeadBody && PranksterUtilities.IsPranksterBody(closestBody);
-
- if (closestBody == null)
- {
- return false;
- }
-
- bool wasNotKilledByNecromancer = true;
- var deadBody = closestBody.GetComponent();
- if (deadBody != null)
- {
- var killedPlayer = GameData.Instance.GetPlayerById(deadBody.ParentId)?.Object;
- if (killedPlayer != null)
- {
- var killer = Utils.GetKiller(killedPlayer);
- if (killer != null && killer.Data.Role is NecromancerRole)
- {
- wasNotKilledByNecromancer = false;
- }
- }
- }
- return isTimerDone && hasUsesLeft && isNearDeadBody && wasNotKilledByNecromancer && !isFakeBody;
- }
- }
-}
diff --git a/NewMod/Buttons/Overload/FinalButton.cs b/NewMod/Buttons/Overload/FinalButton.cs
deleted file mode 100644
index 164512f..0000000
--- a/NewMod/Buttons/Overload/FinalButton.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Keybinds;
-using MiraAPI.Utilities.Assets;
-using NewMod.Options.Roles.OverloadOptions;
-using NewMod.Roles.NeutralRoles;
-using Rewired;
-using UnityEngine;
-
-namespace NewMod.Buttons.Overload
-{
- ///
- /// The Final Ability button for Overload.
- /// Unlocks after reaching the required absorbed charge count.
- ///
- public class FinalAbilityButton : CustomActionButton
- {
- ///
- /// The name displayed on the button (if any).
- ///
- public override string Name => "OVERLOAD";
-
- ///
- /// Cooldown (none for final ability).
- ///
- public override float Cooldown => 0f;
-
- ///
- /// One-time use. Set to 1.
- ///
- public override int MaxUses => 1;
-
- ///
- /// Default keybind for the Final Ability button.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
-
- ///
- /// No duration effect.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// Screen location of the button on the HUD.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomRight;
-
- ///
- /// Icon sprite
- ///
- public override LoadableAsset Sprite => NewModAsset.FinalButton;
-
- ///
- /// Determines when the button should appear.
- /// Only enabled once Overload has enough absorbed abilities.
- ///
- public override bool Enabled(RoleBehaviour role)
- {
- return role is OverloadRole &&
- OverloadRole.AbsorbedAbilityCount >= OptionGroupSingleton.Instance.NeededCharge;
- }
-
- ///
- /// What happens when the final ability button is clicked.
- /// Ends the game with Overload win.
- ///
- protected override void OnClick()
- {
- GameManager.Instance.RpcEndGame(
- (GameOverReason)NewModEndReasons.OverloadWin,
- false
- );
- }
- }
-}
diff --git a/NewMod/Buttons/Overload/OverloadButton.cs b/NewMod/Buttons/Overload/OverloadButton.cs
deleted file mode 100644
index c2352ce..0000000
--- a/NewMod/Buttons/Overload/OverloadButton.cs
+++ /dev/null
@@ -1,151 +0,0 @@
-using MiraAPI.Hud;
-using MiraAPI.Keybinds;
-using MiraAPI.Utilities.Assets;
-using NewMod.Roles.NeutralRoles;
-using UnityEngine;
-
-namespace NewMod.Buttons.Overload
-{
- ///
- /// Defines a custom action button for the Overload role.
- /// This button mimics another role's ability by adopting its appearance and functionality.
- ///
- public class OverloadButton : CustomActionButton
- {
- public CustomActionButton absorbed;
- ///
- /// The display text shown on the button UI.
- /// Set by the absorbed ability.
- ///
- public string absorbedText = "";
-
- ///
- /// The sprite icon used on the button.
- /// Loaded from the absorbed ability.
- ///
- public LoadableAsset absorbedSprite;
-
- ///
- /// The method invoked when the Overload button is clicked.
- /// Mirrors the absorbed button's behavior.
- ///
- public System.Action absorbedOnClick;
-
- ///
- /// Maximum number of times the button can be used.
- /// Mirrors the absorbed button's uses.
- ///
- public int absorbedMaxUses;
-
- ///
- /// Cooldown in seconds for reusing the button.
- /// Mirrors the absorbed button's cooldown.
- ///
- public float absorbedCooldown;
-
- ///
- /// Stores the default key assigned to the absorbed button's action.
- /// Mirrors the keybind of the original absorbed button.
- ///
- public MiraKeybind absorbedKeybind;
-
- ///
- /// The name displayed on the button.
- ///
- public override string Name => absorbedText;
-
- ///
- /// Cooldown duration before the button can be reused.
- ///
- public override float Cooldown => absorbedCooldown;
-
- ///
- /// Number of remaining uses. Zero means unlimited.
- ///
- public override int MaxUses => absorbedMaxUses;
-
- ///
- /// Default keybind for Overload's Overload ability.
- ///
- public override MiraKeybind Keybind => absorbedKeybind;
-
- ///
- /// Determines how long the effect from clicking the button lasts. In this case, no duration is set.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// The UI position for the button.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomRight;
-
- ///
- /// The icon displayed on the button.
- ///
- public override LoadableAsset Sprite => absorbedSprite;
-
- ///
- /// Copies functionality and appearance from another role's button.
- ///
- /// The button to absorb.
- public void Absorb(CustomActionButton target)
- {
- absorbed = target;
-
- absorbedText = target.Name;
- absorbedCooldown = target.Cooldown;
- absorbedMaxUses = target.MaxUses;
- absorbedSprite = target.Sprite;
- absorbedKeybind = (MiraKeybind)target.Keybind;
- absorbedOnClick = () => target.GetType().GetMethod("OnClick", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
- ?.Invoke(target, null);
-
- OverrideName(absorbedText);
- OverrideSprite(absorbedSprite.LoadAsset());
-
- if (absorbedMaxUses <= 0f)
- {
- Button.SetInfiniteUses();
- }
- else
- {
- SetUses(absorbedMaxUses);
- }
-
- SetTimer(0f);
-
- HudManager.Instance.SetHudActive(PlayerControl.LocalPlayer, PlayerControl.LocalPlayer.Data.Role, false);
- HudManager.Instance.SetHudActive(PlayerControl.LocalPlayer, PlayerControl.LocalPlayer.Data.Role, true);
- }
-
- ///
- /// Called when the player presses the button.
- ///
- protected override void OnClick()
- {
- NewMod.Instance.Log.LogError("Overload invoking absorbed action...");
- absorbedOnClick?.Invoke();
- }
-
- ///
- /// Determines whether this button should be active on the HUD.
- /// Only visible when the role is Overload and an ability has been absorbed.
- ///
- /// The role of the player.
- /// True if Overload with a valid absorbed ability.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is OverloadRole && absorbed != null;
- }
-
- ///
- /// Determines if the button can currently be pressed.
- ///
- /// True if usable.
- public override bool CanUse()
- {
- absorbed?.FixedUpdateHandler(PlayerControl.LocalPlayer);
- return base.CanUse() && absorbed != null;
- }
- }
-}
diff --git a/NewMod/Buttons/Prankster/FakeBodyButton.cs b/NewMod/Buttons/Prankster/FakeBodyButton.cs
deleted file mode 100644
index 1918908..0000000
--- a/NewMod/Buttons/Prankster/FakeBodyButton.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities;
-using MiraAPI.Utilities.Assets;
-using NewMod.Options.Roles.PranksterOptions;
-using PRK = NewMod.Roles.NeutralRoles.Prankster;
-using UnityEngine;
-using NewMod.Utilities;
-using Rewired;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.Prankster
-{
- ///
- /// Defines a custom action button for the role.
- ///
- public class FakeBodyButton : CustomActionButton
- {
- ///
- /// Gets the name displayed on the button.
- ///
- public override string Name => "Prank";
-
- ///
- /// Gets the cooldown duration for using the prank ability, based on .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.PrankCooldown;
-
- ///
- /// Gets the maximum number of times the prank ability can be used, based on .
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.PrankMaxUses;
-
- ///
- /// Determines where on the screen this button will appear.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomRight;
-
- ///
- /// Default keybind for Prankster's Fake Body ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// The duration of any effect caused by this button press; in this case, no effect duration is used.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// The sprite asset representing this button.
- ///
- public override LoadableAsset Sprite => NewModAsset.DeadBodySprite;
-
- ///
- /// Checks whether the Prankster can use this button, ensuring that there is at least one dead player in the game.
- ///
- /// True if the base conditions are met and there is a dead player, otherwise false.
- public override bool CanUse()
- {
- return base.CanUse() && Utils.AnyDeadPlayer();
- }
-
- ///
- /// Called when the button is clicked. Spawns a fake dead body at the local player's position.
- ///
- protected override void OnClick()
- {
- PranksterUtilities.CreatePranksterDeadBody(PlayerControl.LocalPlayer, PlayerControl.LocalPlayer.PlayerId);
- }
-
- ///
- /// Determines whether this button is enabled for the specified role.
- ///
- /// The player's current role.
- /// True if the role is , otherwise false.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is PRK;
- }
- }
-}
diff --git a/NewMod/Buttons/PulseBlade/StrikeButton.cs b/NewMod/Buttons/PulseBlade/StrikeButton.cs
deleted file mode 100644
index 7e56ecb..0000000
--- a/NewMod/Buttons/PulseBlade/StrikeButton.cs
+++ /dev/null
@@ -1,153 +0,0 @@
-using System.Collections;
-using System.Linq;
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities.Assets;
-using UnityEngine;
-using Pb = NewMod.Roles.ImpostorRoles.PulseBlade;
-using MiraAPI.Networking;
-using NewMod.Options.Roles.PulseBladeOptions;
-using Reactor.Utilities;
-using MiraAPI.Utilities;
-using NewMod.Utilities;
-using Reactor.Networking.Attributes;
-using Rewired;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.Pulseblade
-{
- ///
- /// Custom button for the Pulseblade role to perform a high-speed strike on the closest player in aim direction.
- /// The strike teleports the user toward the target and executes a stealthy instant kill.
- ///
- public class StrikeButton : CustomActionButton
- {
- ///
- /// Display name for the button (not shown by default).
- ///
- public override string Name => "Strike";
-
- ///
- /// Cooldown between strikes, pulled from .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.StrikeCooldown;
-
- ///
- /// Maximum number of strike uses, from .
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.MaxStrikeUses;
-
- ///
- /// Effect duration (not used for this button).
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// Placement of the button on the HUD.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomRight;
-
- ///
- /// Default keybind for Pulseblade's Strike ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
- ///
- /// Sprite used for the button — set to empty;
- ///
- public override LoadableAsset Sprite => NewModAsset.StrikeButton;
-
- ///
- /// Determines whether the button is active for a given role.
- ///
- /// The current player's role.
- /// True only for Pulseblade role.
- public override bool Enabled(RoleBehaviour role) => role is Pb;
-
- ///
- /// Called when the button is pressed by the player.
- /// Searches for a valid target and executes the strike.
- ///
- protected override void OnClick()
- {
- var player = PlayerControl.LocalPlayer;
-
- var target = PlayerControl.AllPlayerControls
- .ToArray()
- .Where(p => p != player && !p.Data.IsDead && !p.Data.Disconnected && !p.inVent)
- .OrderBy(p => Vector2.Distance(player.GetTruePosition(), p.GetTruePosition()))
- .FirstOrDefault(p => Vector2.Distance(player.GetTruePosition(), p.GetTruePosition()) <= OptionGroupSingleton.Instance.StrikeRange);
-
- RpcPulseStrike(player, target);
- }
-
- ///
- /// RPC method to perform a Pulseblade strike on a target.
- ///
- /// The player performing the strike.
- /// The victim of the strike.
- [MethodRpc((uint)CustomRPC.Dash)]
- public static void RpcPulseStrike(PlayerControl source, PlayerControl target)
- {
- Coroutines.Start(DoPulseStrike(source, target));
- }
-
- ///
- /// Executes the strike: dashes to the target and performs a kill.
- /// Hides the body for a short time.
- ///
- /// The Pulseblade player.
- /// The struck victim.
- /// IEnumerator.
- public static IEnumerator DoPulseStrike(PlayerControl killer, PlayerControl target)
- {
- var sound = NewModAsset.StrikeSound.LoadAsset();
- float originalSpeed = killer.MyPhysics.Speed;
- float dashSpeed = OptionGroupSingleton.Instance.DashSpeed;
-
- killer.moveable = false;
- killer.MyPhysics.inputHandler.enabled = false;
- killer.MyPhysics.Speed = dashSpeed;
-
- while (Vector2.Distance(killer.GetTruePosition(), target.GetTruePosition()) > 0.1f)
- {
- Vector2 dir = target.GetTruePosition() - killer.GetTruePosition();
- killer.MyPhysics.SetNormalizedVelocity(dir.normalized);
-
- float step = killer.MyPhysics.TrueSpeed * Time.fixedDeltaTime;
- if (step >= dir.magnitude) break;
-
- yield return new WaitForFixedUpdate();
- }
-
- killer.MyPhysics.SetNormalizedVelocity(Vector2.zero);
- killer.MyPhysics.Speed = originalSpeed;
- killer.MyPhysics.inputHandler.enabled = true;
- killer.moveable = true;
-
- SoundManager.Instance.PlaySound(sound, false, 1f);
-
- killer.RpcCustomMurder(
- target,
- didSucceed: true,
- resetKillTimer: false,
- createDeadBody: true,
- teleportMurderer: false,
- showKillAnim: false,
- playKillSound: false
- );
-
- Utils.RegisterStrikeKill(killer, target);
-
- var notif = Helpers.CreateAndShowNotification($"Perfect kill {target.Data.PlayerName} eliminated", new(1f, 0.25f, 0.25f), spr: NewModAsset.StrikeIcon.LoadAsset());
- notif.Text.SetOutlineThickness(0.30f);
-
- var bodies = Helpers.GetNearestDeadBodies(target.GetTruePosition(), 0.5f, Helpers.CreateFilter(Constants.NotShipMask));
- if (bodies != null && bodies.Count > 0)
- {
- foreach (var b in bodies) if (b) b.gameObject.SetActive(false);
- yield return new WaitForSeconds(OptionGroupSingleton.Instance.HideBodyDuration);
- foreach (var b in bodies) if (b) b.gameObject.SetActive(true);
- }
- }
- }
-}
diff --git a/NewMod/Buttons/Revenant/DoomAwakening.cs b/NewMod/Buttons/Revenant/DoomAwakening.cs
deleted file mode 100644
index 8e5789d..0000000
--- a/NewMod/Buttons/Revenant/DoomAwakening.cs
+++ /dev/null
@@ -1,206 +0,0 @@
-using System.Collections.Generic;
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities.Assets;
-using MiraAPI.Networking;
-using MiraAPI.Utilities;
-using NewMod.Options.Roles.RevenantOptions;
-using RV = NewMod.Roles.ImpostorRoles.Revenant;
-using NewMod.Utilities;
-using Reactor.Utilities;
-using UnityEngine;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.Revenant
-{
- ///
- /// Defines a custom action button for the role.
- ///
- public class DoomAwakening : CustomActionButton
- {
- ///
- /// The name displayed on the button.
- ///
- public override string Name => "";
-
- ///
- /// Cooldown time for this ability, as configured in .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.DoomAwakeningCooldown;
-
- ///
- /// Maximum uses allowed for this ability, as configured in .
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.DoomAwakeningMaxUses;
-
- ///
- /// Defines the on-screen position of this button.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomLeft;
-
- ///
- /// Default keybind for Doom's Awakening ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
-
- ///
- /// Determines how long the effect lasts. Configured in .
- ///
- public override float EffectDuration => OptionGroupSingleton.Instance.DoomAwakeningDuration;
-
- ///
- /// The icon or sprite representing this button.
- ///
- public override LoadableAsset Sprite => NewModAsset.DoomAwakeningButton;
-
- ///
- /// Specifies whether this button is enabled for the specified role.
- ///
- /// The role under consideration.
- /// True if the role is , otherwise false.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is RV;
- }
-
- ///
- /// Checks if this button can be used, ensuring that Feign Death has already been used.
- ///
- /// True if base conditions are met and the player has used Feign Death, otherwise false.
- public override bool CanUse()
- {
- return base.CanUse() && RV.HasUsedFeignDeath;
- }
-
- ///
- /// Invoked when the button is clicked. Starts the Doom Awakening sequence.
- ///
- protected override void OnClick()
- {
- var player = PlayerControl.LocalPlayer;
- Coroutines.Start(StartDoomAwakening(player));
- }
- public static List killedPlayers = new();
-
- ///
- /// Executes the Doom Awakening effect, increasing speed, fading the screen, and killing nearby players.
- ///
- /// The PlayerControl instance of the local player.
- /// An IEnumerator for coroutine control.
- public System.Collections.IEnumerator StartDoomAwakening(PlayerControl player)
- {
- float originalSpeed = player.MyPhysics.Speed;
- player.MyPhysics.Speed *= 2f;
-
- var clip = NewModAsset.DoomAwakeningSound.LoadAsset();
- SoundManager.Instance.PlaySound(clip, true, 1f, null);
-
- var fullScreen = HudManager.Instance.FullScreen;
- fullScreen.color = new Color(1f, 0f, 0f, 0f);
- fullScreen.gameObject.SetActive(true);
-
- float fadeInTime = 0.5f;
-
- // Fade in to a red overlay
- for (float t = 0; t < fadeInTime; t += Time.deltaTime)
- {
- float alpha = Mathf.Lerp(0f, 0.3f, t / fadeInTime);
- fullScreen.color = new Color(1f, 0f, 0f, alpha);
- yield return null;
- }
- fullScreen.color = new Color(1f, 0f, 0f, 0.3f);
-
- float duration = EffectDuration;
- float timer = 0f;
- int killCount = 0;
-
- float ghostInterval = 0.2f;
- float ghostTimer = 0f;
-
- Queue ghosts = new Queue();
- SpriteRenderer playerRenderer = player.cosmetics.normalBodySprite.BodySprite;
-
- while (timer < duration)
- {
- timer += Time.deltaTime;
- ghostTimer += Time.deltaTime;
-
- if (ghostTimer >= ghostInterval && player.MyPhysics.Speed > 0.01f)
- {
- ghostTimer = 0f;
- GameObject ghost = new("Revenant-Ghost");
- var ghostRenderer = ghost.AddComponent();
- ghostRenderer.sprite = playerRenderer.sprite;
- ghostRenderer.flipX = playerRenderer.flipX;
- ghostRenderer.flipY = playerRenderer.flipY;
- ghostRenderer.sharedMaterial = playerRenderer.sharedMaterial;
- PlayerMaterial.SetColors(player.Data.DefaultOutfit.ColorId, ghostRenderer);
- ghostRenderer.sortingLayerID = playerRenderer.sortingLayerID;
- ghostRenderer.sortingOrder = playerRenderer.sortingOrder + 1;
- ghost.transform.position = player.transform.position;
- ghost.transform.rotation = player.transform.rotation;
- ghost.transform.localScale = player.transform.lossyScale;
-
- Coroutines.Start(Utils.FadeAndDestroy(ghost, 1f));
- ghosts.Enqueue(ghost);
-
- // Limit the number of ghost sprites
- if (ghosts.Count > 5)
- {
- var oldGhost = ghosts.Dequeue();
- if (oldGhost != null)
- Object.Destroy(oldGhost);
- }
- }
- // Kill any nearby players
- foreach (var target in PlayerControl.AllPlayerControls)
- {
- if (target == player || target.Data.IsDead || target.Data.Disconnected || target.inVent || target.Data.Role.IsImpostor)
- continue;
-
- if (Vector2.Distance(player.GetTruePosition(), target.GetTruePosition()) < 1f)
- {
- player.RpcCustomMurder(target,
- didSucceed: true,
- resetKillTimer: false,
- createDeadBody: true,
- teleportMurderer: false,
- showKillAnim: false,
- playKillSound: true);
- killCount++;
- killedPlayers.Add(target);
- }
- if (target.AmOwner)
- {
- SoundManager.Instance.PlaySound(NewModAsset.DoomAwakeningEndSound.LoadAsset(), false, 1f, null);
- }
- }
- yield return null;
- }
- if (killedPlayers.Count >= 3)
- {
- SoundManager.Instance.PlaySound(NewModAsset.DoomAwakeningEndSound.LoadAsset(), false, 1f, null);
- }
-
- // Fade out the red overlay
- float fadeOutTime = 0.5f;
- for (float t = 0f; t < fadeOutTime; t += Time.deltaTime)
- {
- float alpha = Mathf.Lerp(0.3f, 0f, t / fadeOutTime);
- fullScreen.color = new Color(1f, 0f, 0f, alpha);
- yield return null;
- }
- fullScreen.gameObject.SetActive(false);
-
- // Restore original speed and conclude
- player.MyPhysics.Speed = originalSpeed;
- SoundManager.Instance.StopSound(clip);
- RV.StalkingStates.Remove(player.PlayerId);
- Coroutines.Start(CoroutinesHelper.CoNotify("Doom Awakening ended."));
-
- var doomNotif = Helpers.CreateAndShowNotification($"Doom Awakening killed {killCount} players", Color.red, null, null);
- doomNotif.Text.SetOutlineThickness(0.36f);
- killedPlayers.Clear();
- }
- }
-}
diff --git a/NewMod/Buttons/Revenant/FeignDeathButton.cs b/NewMod/Buttons/Revenant/FeignDeathButton.cs
deleted file mode 100644
index f0f0c87..0000000
--- a/NewMod/Buttons/Revenant/FeignDeathButton.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities.Assets;
-using NewMod.Options.Roles.RevenantOptions;
-using Rev = NewMod.Roles.ImpostorRoles.Revenant;
-using NewMod.Utilities;
-using Reactor.Utilities;
-using UnityEngine;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.Revenant
-{
- ///
- /// Defines a custom action button for the role.
- ///
- public class FeignDeathButton : CustomActionButton
- {
- ///
- /// The name displayed on the button.
- ///
- public override string Name => "Feign Death";
-
- ///
- /// The cooldown time for the Feign Death ability, as set in .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.FeignDeathCooldown;
-
- ///
- /// The maximum uses of the Feign Death ability, as set in .
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.FeignDeathMaxUses;
-
- ///
- /// Determines where on the screen this button appears.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomRight;
-
- ///
- /// Default keybind for Revenant's Feign Death ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// The duration of any effect from this button. In this case, zero.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// The icon or sprite used for this button. Here, set to an empty sprite.
- ///
- public override LoadableAsset Sprite => MiraAssets.Empty;
-
- ///
- /// Specifies whether the button is enabled for the given role, ensuring Feign Death hasn't been used yet.
- ///
- /// The player's current role.
- /// True if the role is and Feign Death hasn't been used, otherwise false.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is Rev && !Rev.HasUsedFeignDeath;
- }
- ///
- /// Checks if this button can be used
- ///
- /// True if base conditions are met and the player hasn't used Feign Death; otherwise, false.
- public override bool CanUse()
- {
- return base.CanUse() && !Rev.HasUsedFeignDeath;
- }
-
- ///
- /// Invoked when the Feign Death button is clicked, starting the feign death coroutine.
- ///
- protected override void OnClick()
- {
- var player = PlayerControl.LocalPlayer;
- Coroutines.Start(Utils.StartFeignDeath(player));
- }
- }
-}
diff --git a/NewMod/Buttons/Roles/AcceptChampionButton.cs b/NewMod/Buttons/Roles/AcceptChampionButton.cs
new file mode 100644
index 0000000..9fff9e4
--- /dev/null
+++ b/NewMod/Buttons/Roles/AcceptChampionButton.cs
@@ -0,0 +1,23 @@
+using MiraAPI.Hud;
+using MiraAPI.Utilities.Assets;
+using NewMod.Roles.ImpostorRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class AcceptChampionButton : CustomActionButton
+{
+ public override string Name => "Accept Alliance";
+ public override float Cooldown => 0f;
+ public override LoadableAsset Sprite => NewModAsset.TyrantAlliance;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return Button && PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data && PlayerControl.LocalPlayer.PlayerId == Tyrant.ChampionId && !Tyrant.OfferAnswered && !PlayerControl.LocalPlayer.Data.IsDead;
+ }
+
+ protected override void OnClick()
+ {
+ Tyrant.RpcAnswerOffer(PlayerControl.LocalPlayer, true);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/AegisButton.cs b/NewMod/Buttons/Roles/AegisButton.cs
new file mode 100644
index 0000000..fafa61a
--- /dev/null
+++ b/NewMod/Buttons/Roles/AegisButton.cs
@@ -0,0 +1,51 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Components;
+using NewMod.Options.Roles;
+using NewMod.Roles.CrewmateRoles;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+///
+/// Custom action button for the Aegis role. Places a configurable shield zone.
+///
+public class AegisButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Protection;
+
+ public override string Name => "Sentinel Ward";
+
+ public override float Cooldown => OptionGroupSingleton.Instance.AegisCooldown;
+
+ public override int MaxUses => (int)OptionGroupSingleton.Instance.MaxCharges;
+
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+
+ public override float EffectDuration => 0f;
+
+ public override LoadableAsset Sprite => NewModAsset.Shield;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Aegis;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && !ShieldArea._active.Exists(area => area.ownerId == PlayerControl.LocalPlayer.PlayerId);
+ }
+
+ protected override void OnClick()
+ {
+ var player = PlayerControl.LocalPlayer;
+
+ AegisUtilities.RpcRequestWard(player);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/ArcButton.cs b/NewMod/Buttons/Roles/ArcButton.cs
new file mode 100644
index 0000000..85cebea
--- /dev/null
+++ b/NewMod/Buttons/Roles/ArcButton.cs
@@ -0,0 +1,93 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using MiraAPI.Utilities;
+using NewMod.Options.Roles;
+using NewMod.Roles.ImpostorRoles;
+using NewMod.Roles.NeutralRoles;
+using Reactor.Networking.Attributes;
+using Reactor.Utilities;
+using System.Collections;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class ArcButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Aggression;
+
+ public override string Name => "Arc";
+
+ public override float Cooldown => OptionGroupSingleton.Instance.SlashCooldown;
+
+ public override float EffectDuration => OptionGroupSingleton.Instance.ChargeDuration;
+
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+
+ public override LoadableAsset Sprite => NewModAsset.Slash;
+
+ protected override void FixedUpdate(PlayerControl playerControl)
+ {
+ OverrideName(EffectActive ? "Charging" : Name);
+ }
+
+ protected override void OnClick()
+ {
+ var player = PlayerControl.LocalPlayer;
+
+ var flipLeft = player.cosmetics.currentBodySprite.BodySprite.flipX;
+ var dir = flipLeft ? Vector2.left : Vector2.right;
+
+ RpcArc(player, dir);
+ }
+
+ [MethodRpc((uint)CustomRPC.EdgeveilArc)]
+ public static void RpcArc(PlayerControl source, Vector2 direction)
+ {
+ if (source.Data.Role is not Edgeveil || source.Data.IsDead || source.inVent || MeetingHud.Instance)
+ return;
+ Coroutines.Start(CoArc(source, direction.normalized));
+ }
+
+ public static IEnumerator CoArc(PlayerControl source, Vector2 direction)
+ {
+ var options = OptionGroupSingleton.Instance;
+ if (source.AmOwner)
+ {
+ source.moveable = false;
+ source.MyPhysics.body.velocity = Vector2.zero;
+ }
+
+ var end = Time.time + options.ChargeDuration;
+ while (source && !source.Data.IsDead && !MeetingHud.Instance && Time.time < end)
+ {
+ source.cosmetics.currentBodySprite.BodySprite.UpdateOutline(Color.red);
+ yield return null;
+ }
+
+ if (!source)
+ yield break;
+ source.cosmetics.currentBodySprite.BodySprite.UpdateOutline(null);
+ if (source.AmOwner && !MeetingHud.Instance && !ExileController.Instance)
+ source.moveable = true;
+ if (source.Data.IsDead || MeetingHud.Instance || ExileController.Instance)
+ yield break;
+
+ var tray = SlashTray.CreateTray();
+ var position = source.GetTruePosition();
+ tray.transform.SetPositionAndRotation(new Vector3(position.x, position.y, source.transform.position.z), Quaternion.identity);
+ var scale = tray.transform.localScale;
+ scale.x = Mathf.Abs(scale.x) * (direction.x < 0f ? -1f : 1f);
+ tray.transform.localScale = scale;
+ tray.Owner = source;
+ tray.SetMotion(direction, options.SlashSpeed);
+ }
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Edgeveil;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/BeaconScanButton.cs b/NewMod/Buttons/Roles/BeaconScanButton.cs
new file mode 100644
index 0000000..480a431
--- /dev/null
+++ b/NewMod/Buttons/Roles/BeaconScanButton.cs
@@ -0,0 +1,38 @@
+using MiraAPI.Hud;
+using MiraAPI.GameOptions;
+using NewMod.Options.Roles;
+using NewMod.Utilities;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Patches.Roles.Beacon;
+using NewMod.Roles.CrewmateRoles;
+using NewMod.Roles.NeutralRoles;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class BeaconScanButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Intelligence;
+ public override string Name => "Scan";
+ public override float Cooldown => OptionGroupSingleton.Instance.PulseCooldown;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override LoadableAsset Sprite => NewModAsset.RadarIcon;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Beacon;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && Beacon.charges > 0 && !Utils.IsActive(SystemTypes.Comms);
+ }
+
+ protected override void OnClick()
+ {
+ Coroutines.Start(BeaconShowMapPatch.ShowSnapshot());
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/CallWraith.cs b/NewMod/Buttons/Roles/CallWraith.cs
new file mode 100644
index 0000000..225b3c9
--- /dev/null
+++ b/NewMod/Buttons/Roles/CallWraith.cs
@@ -0,0 +1,111 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Utilities.Assets;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Gameplay;
+using MiraAPI.Events.Vanilla.Meeting;
+using NewMod.Options.Roles;
+using NewMod.Roles.NeutralRoles;
+using Wraith = NewMod.Roles.NeutralRoles.WraithCaller;
+using UnityEngine;
+using NewMod.Utilities;
+using System.Collections.Generic;
+using System.Linq;
+using MiraAPI.Keybinds;
+using Reactor.Utilities;
+
+namespace NewMod.Buttons.Roles;
+
+public class CallWraithButton : CustomActionButton, IEnergyAbility
+{
+ public static CustomPlayerMenu ActiveMenu;
+
+ public EnergyCategory Category => EnergyCategory.Control;
+ public bool CaptureOnClick => false;
+ public override string Name => "Call Wraith";
+ public override float Cooldown => OptionGroupSingleton.Instance.CallWraithCooldown;
+
+ public override bool CanUse()
+ {
+ var ownerId = PlayerControl.LocalPlayer.PlayerId;
+ return base.CanUse() && WraithCallerUtilities.Traces.Contains(ownerId) && !WraithCallerUtilities.ActiveNpcs.Values.Any(npc => npc && npc.isActive && npc.Owner.PlayerId == ownerId);
+ }
+
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override float EffectDuration => 0f;
+ public override LoadableAsset Sprite => NewModAsset.CallWraith;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Wraith;
+ }
+
+ public override void ClickHandler()
+ {
+ if (CanClick())
+ OnClick();
+ }
+
+ protected override void OnClick()
+ {
+ CloseActiveMenu();
+
+ var menu = CustomPlayerMenu.Create();
+ ActiveMenu = menu;
+ var allowedPlayers = new HashSet();
+
+ foreach (var info in GameData.Instance.AllPlayers)
+ {
+ if (info.PlayerId == PlayerControl.LocalPlayer.PlayerId) continue;
+ if (info.IsDead || info.Disconnected) continue;
+
+ allowedPlayers.Add(info.PlayerId);
+ }
+
+ menu.Begin(player => allowedPlayers.Contains(player.PlayerId) && !player.notRealPlayer, player =>
+ {
+ if (!player)
+ {
+ CloseActiveMenu();
+ return;
+ }
+
+ CloseActiveMenu();
+
+ ResetCooldownAndOrEffect();
+
+ WraithCallerUtilities.RequestSummonNPC(PlayerControl.LocalPlayer, player);
+ EnergyThief.ReportAbilityUse(Category);
+ });
+
+ foreach (var panel in menu.potentialVictims)
+ {
+ var icon = panel.GetComponentsInChildren(true).FirstOrDefault(renderer => renderer.name == "ShapeshifterIcon");
+
+ if (icon)
+ icon.sprite = NewModAsset.WraithIcon.LoadAsset();
+ }
+ }
+
+ [RegisterEvent]
+ public static void OnMeetingStart(StartMeetingEvent evt)
+ {
+ CloseActiveMenu();
+ }
+
+ [RegisterEvent]
+ public static void OnGameEnd(GameEndEvent evt)
+ {
+ CloseActiveMenu();
+ }
+
+ public static void CloseActiveMenu()
+ {
+ var menu = ActiveMenu;
+ ActiveMenu = null;
+
+ if (menu)
+ menu.ForceClose();
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/CameraInteractButton.cs b/NewMod/Buttons/Roles/CameraInteractButton.cs
new file mode 100644
index 0000000..4a5644b
--- /dev/null
+++ b/NewMod/Buttons/Roles/CameraInteractButton.cs
@@ -0,0 +1,85 @@
+using System.Linq;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class CameraInteractButton : CustomActionButton
+{
+ public int TargetId = -1;
+ public int PendingId = -1;
+ public Vector2 Origin;
+ public override string Name => "Camera";
+ public override float Cooldown => 1f;
+ public override float EffectDuration => 1.5f;
+ public override MiraKeybind Keybind => null;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override LoadableAsset Sprite => NewModAsset.Camera;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return Button && PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data;
+ }
+
+ protected override void FixedUpdate(PlayerControl playerControl)
+ {
+ var canInteract = playerControl.CanMove && !MeetingHud.Instance && !playerControl.Data.IsDead && !playerControl.inVent;
+ if (EffectActive && (!canInteract || Vector2.Distance(Origin, playerControl.GetTruePosition()) >= 0.15f || !VisionaryUtilities.Cameras.TryGetValue(PendingId, out var pending) || !pending.Object))
+ {
+ PendingId = -1;
+ ResetCooldownAndOrEffect();
+ }
+
+ TargetId = -1;
+ if (canInteract)
+ {
+ var position = playerControl.GetTruePosition();
+ var nearby = VisionaryUtilities.Cameras.Where(pair =>
+ {
+ if (!pair.Value.Object || (pair.Value.Owner == playerControl.PlayerId && !pair.Value.Ready))
+ return false;
+ var offset = (Vector2)pair.Value.Object.transform.position - position;
+ return offset.magnitude <= 1.25f && !PhysicsHelpers.AnythingBetween(position, pair.Value.Object.transform.position, Constants.ShipAndObjectsMask, false, pair.Value.Object.GetComponent(), pair.Value.Object.transform);
+ }).OrderBy(pair => Vector2.Distance(pair.Value.Object.transform.position, position)).FirstOrDefault();
+ TargetId = EffectActive ? PendingId : nearby.Value != null ? nearby.Key : -1;
+ }
+
+ foreach (var (id, record) in VisionaryUtilities.Cameras)
+ {
+ if (!record.Object)
+ continue;
+ var outlined = id == TargetId && record.Owner == playerControl.PlayerId;
+ var asset = !record.Ready ? NewModAsset.CameraOff : record.Failed ? outlined ? NewModAsset.CameraDisabledOutline : NewModAsset.CameraDisabled : outlined ? NewModAsset.CameraEnabledOutline : NewModAsset.CameraEnabled;
+ record.Object.GetComponentInChildren().sprite = asset.LoadAsset();
+ }
+
+ if (Button)
+ {
+ Button.gameObject.SetActive(TargetId >= 0);
+ if (TargetId >= 0)
+ OverrideName(VisionaryUtilities.Cameras[TargetId].Owner == playerControl.PlayerId ? "Retrieve" : "Disable Camera");
+ }
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && TargetId >= 0;
+ }
+
+ protected override void OnClick()
+ {
+ PendingId = TargetId;
+ Origin = PlayerControl.LocalPlayer.GetTruePosition();
+ }
+
+ public override void OnEffectEnd()
+ {
+ if (PendingId >= 0 && !MeetingHud.Instance && !PlayerControl.LocalPlayer.Data.IsDead && Vector2.Distance(Origin, PlayerControl.LocalPlayer.GetTruePosition()) < 0.15f)
+ VisionaryUtilities.RpcInteractCamera(PlayerControl.LocalPlayer, PendingId);
+ PendingId = -1;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/CaptureButton.cs b/NewMod/Buttons/Roles/CaptureButton.cs
new file mode 100644
index 0000000..3e8489f
--- /dev/null
+++ b/NewMod/Buttons/Roles/CaptureButton.cs
@@ -0,0 +1,139 @@
+using System.Collections;
+using System.Linq;
+using MiraAPI.Utilities;
+using Reactor.Utilities;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.CrewmateRoles;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+///
+/// Defines a custom action button for the role.
+///
+public class CaptureButton : CustomActionButton, IEnergyAbility
+{
+ public GameObject PlacementPreview;
+ public Vector2 PlacementPosition;
+ public float PlacementAngle;
+
+ public EnergyCategory Category => EnergyCategory.Intelligence;
+
+ public override string Name => "Place Camera";
+
+ public override float Cooldown => OptionGroupSingleton.Instance.ScreenshotCooldown;
+
+ public override float EffectDuration => 0;
+
+ public override int MaxUses => (int)OptionGroupSingleton.Instance.MaxScreenshots;
+
+ public override LoadableAsset Sprite => NewModAsset.Camera;
+
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+
+ protected override void OnClick()
+ {
+ VisionaryUtilities.RpcRequestCamera(PlayerControl.LocalPlayer, PlacementPosition.x, PlacementPosition.y, PlacementAngle);
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && !PlacementPreview;
+ }
+
+ public override void ClickHandler()
+ {
+ if (CanClick())
+ Coroutines.Start(PlaceCamera());
+ }
+
+ public IEnumerator PlaceCamera()
+ {
+ var player = PlayerControl.LocalPlayer;
+ var preview = new GameObject("CameraPlacement");
+ PlacementPreview = preview;
+ preview.transform.SetParent(ShipStatus.Instance.transform, false);
+ preview.layer = LayerMask.NameToLayer("UI");
+ preview.transform.localScale = Vector3.one * 0.05f;
+ var sprite = new GameObject("CameraSprite").AddComponent();
+ sprite.gameObject.layer = preview.layer;
+ sprite.transform.SetParent(preview.transform, false);
+ sprite.sprite = NewModAsset.CameraOff.LoadAsset();
+ sprite.transform.localPosition = Vector3.down * sprite.sprite.vertices.Min(vertex => vertex.y);
+ sprite.color = new Color(0.6f, 0.6f, 0.6f, 0.6f);
+ var arrow = preview.AddComponent();
+ arrow.sharedMaterial = Utils.GetCircleMat();
+ arrow.useWorldSpace = true;
+ arrow.positionCount = 5;
+ arrow.startWidth = arrow.endWidth = 0.04f;
+ var aiming = false;
+ var confirmed = false;
+ PlacementPosition = player.GetTruePosition();
+ preview.transform.position = new Vector3(PlacementPosition.x, PlacementPosition.y, PlacementPosition.y / 1000f);
+ PlacementAngle = player.cosmetics.FlipX ? 180f : 0f;
+ try
+ {
+ while (preview && player && !MeetingHud.Instance && (Input.GetMouseButton(0) || Input.touchCount > 0))
+ yield return null;
+ while (preview && player && player.Data.Role is TheVisionary && !player.Data.IsDead && player.CanMove && !player.inVent && !MeetingHud.Instance && ShipStatus.Instance)
+ {
+ if (Input.GetMouseButtonDown(1) || Input.GetKeyDown(KeyCode.Escape))
+ break;
+ if (Input.touchCount > 1 || (Input.touchCount == 1 && Input.GetTouch(0).phase == TouchPhase.Canceled))
+ break;
+ var touching = Input.touchCount == 1;
+ var pointer = touching ? Input.GetTouch(0).position : (Vector2)Input.mousePosition;
+ var point = (Vector2)Camera.main.ScreenToWorldPoint(pointer);
+ var pressed = touching ? Input.GetTouch(0).phase == TouchPhase.Began : Input.GetMouseButtonDown(0);
+ var released = touching ? Input.GetTouch(0).phase == TouchPhase.Ended : Input.GetMouseButtonUp(0);
+ if (!aiming)
+ PlacementPosition = point;
+ var offset = PlacementPosition - player.GetTruePosition();
+ var valid = offset.magnitude <= OptionGroupSingleton.Instance.PlacementRange && !PhysicsHelpers.AnyNonTriggersBetween(player.GetTruePosition(), offset.normalized, offset.magnitude, Constants.ShipAndObjectsMask);
+ if (pressed && valid)
+ aiming = true;
+ var aim = point - PlacementPosition;
+ if (aiming && aim.sqrMagnitude > 0.01f)
+ PlacementAngle = Mathf.Atan2(aim.y, aim.x) * Mathf.Rad2Deg;
+ preview.transform.position = new Vector3(PlacementPosition.x, PlacementPosition.y, PlacementPosition.y / 1000f);
+ var radians = PlacementAngle * Mathf.Deg2Rad;
+ var direction = new Vector2(Mathf.Cos(radians), Mathf.Sin(radians));
+ var tip = PlacementPosition + direction;
+ var side = new Vector2(-direction.y, direction.x) * 0.15f;
+ arrow.SetPositions(new Vector3[] { PlacementPosition, tip, tip - direction * 0.25f + side, tip, tip - direction * 0.25f - side });
+ var color = valid ? new Color(0.6f, 0.6f, 0.6f, 0.6f) : new Color(1f, 0.3f, 0.3f, 0.6f);
+ sprite.color = color;
+ arrow.startColor = arrow.endColor = color;
+ if (aiming && released)
+ {
+ confirmed = valid;
+ break;
+ }
+
+ yield return null;
+ }
+ }
+ finally
+ {
+ Object.Destroy(preview);
+ if (PlacementPreview == preview)
+ PlacementPreview = null;
+ }
+
+ if (confirmed)
+ base.ClickHandler();
+ }
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is TheVisionary;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/CleanseSerumButton.cs b/NewMod/Buttons/Roles/CleanseSerumButton.cs
new file mode 100644
index 0000000..4f7842a
--- /dev/null
+++ b/NewMod/Buttons/Roles/CleanseSerumButton.cs
@@ -0,0 +1,32 @@
+using System.Linq;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class CleanseSerumButton : CustomActionButton
+{
+ public override string Name => "Cleanse";
+ public override float Cooldown => 0f;
+ public override float EffectDuration => OptionGroupSingleton.Instance.CleanseDuration;
+ public override LoadableAsset Sprite => NewModAsset.InjectIcon;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return Button && PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data && !PlayerControl.LocalPlayer.Data.IsDead && InjectorUtilities.Experiments.Values.Any(sample => sample.TargetId == PlayerControl.LocalPlayer.PlayerId);
+ }
+
+ protected override void OnClick()
+ {
+ }
+
+ public override void OnEffectEnd()
+ {
+ if (!MeetingHud.Instance && !ExileController.Instance)
+ InjectorUtilities.RpcCleanse(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/CollectWraithTraceButton.cs b/NewMod/Buttons/Roles/CollectWraithTraceButton.cs
new file mode 100644
index 0000000..0299ae9
--- /dev/null
+++ b/NewMod/Buttons/Roles/CollectWraithTraceButton.cs
@@ -0,0 +1,49 @@
+using System.Linq;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class CollectWraithTraceButton : CustomActionButton
+{
+ public override string Name => "Collect Trace";
+ public override float Cooldown => 0f;
+ public override float Distance => OptionGroupSingleton.Instance.TraceRange;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override LoadableAsset Sprite => NewModAsset.WraithIcon;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is WraithCaller;
+ }
+
+ public override DeadBody GetTarget()
+ {
+ var player = PlayerControl.LocalPlayer;
+ if (WraithCallerUtilities.Traces.Contains(player.PlayerId) || WraithCallerUtilities.ActiveNpcs.Values.Any(npc => npc && npc.isActive && npc.Owner == player))
+ return null;
+
+ var position = player.GetTruePosition();
+ return Helpers.GetNearestDeadBodies(position, Distance, Helpers.CreateFilter(Constants.NotShipMask)).Where(body => !body.Reported && !PranksterUtilities.IsPranksterBody(body) && !WraithCallerUtilities.CollectedTraces.Contains((player.PlayerId, body.ParentId)) && !PhysicsHelpers.AnythingBetween(position, body.TruePosition, Constants.ShipAndObjectsMask, false)).OrderBy(body => Vector2.Distance(position, body.TruePosition)).FirstOrDefault();
+ }
+
+ public override void SetOutline(bool active)
+ {
+ if (!Target) return;
+ foreach (var renderer in Target.bodyRenderers)
+ renderer.material.SetFloat("_Outline", active ? 1f : 0f);
+ }
+
+ protected override void OnClick()
+ {
+ WraithCallerUtilities.RpcCollectTrace(PlayerControl.LocalPlayer, Target.ParentId);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/DeployShadow.cs b/NewMod/Buttons/Roles/DeployShadow.cs
new file mode 100644
index 0000000..028fc4c
--- /dev/null
+++ b/NewMod/Buttons/Roles/DeployShadow.cs
@@ -0,0 +1,37 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Components;
+using NewMod.Options.Roles;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class DeployShadow : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Control;
+ public override string Name => "Deploy Shadow";
+ public override float Cooldown => OptionGroupSingleton.Instance.Cooldown;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.DeployZone;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Shade;
+ }
+
+ protected override void OnClick()
+ {
+ var player = PlayerControl.LocalPlayer;
+
+ var pos = player.GetTruePosition();
+
+ var radius = OptionGroupSingleton.Instance.Radius;
+ var dur = OptionGroupSingleton.Instance.Duration;
+
+ ShadowZone.RpcDeployZone(player, pos, radius, dur);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/DoomAwakening.cs b/NewMod/Buttons/Roles/DoomAwakening.cs
new file mode 100644
index 0000000..7516c5c
--- /dev/null
+++ b/NewMod/Buttons/Roles/DoomAwakening.cs
@@ -0,0 +1,106 @@
+using System.Collections;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.ImpostorRoles;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public sealed class DoomAwakening : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Aggression;
+ public override string Name => "";
+ public override float Cooldown => 0f;
+ public override int MaxUses => 1;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.DoomAwakeningButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Revenant;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && Revenant.Phases.TryGetValue(PlayerControl.LocalPlayer.PlayerId, out var phase) && phase == Revenant.Phase.Revived;
+ }
+
+ protected override void OnClick()
+ {
+ Revenant.RpcRequestDoom(PlayerControl.LocalPlayer);
+ }
+
+ public static IEnumerator CoDoom(PlayerControl player, float expiresAt)
+ {
+ if (!player || !player.AmOwner)
+ yield break;
+
+ var options = OptionGroupSingleton.Instance;
+ var physics = player.MyPhysics;
+ var originalSpeed = physics.Speed;
+ var overlay = HudManager.Instance.FullScreen;
+ var originalColor = overlay.color;
+ var overlayWasActive = overlay.gameObject.activeSelf;
+ var sound = NewModAsset.DoomAwakeningSound.LoadAsset();
+ var interval = new WaitForSeconds(0.25f);
+
+ physics.Speed *= 1f + options.DoomSpeedBonus / 100f;
+ overlay.color = new Color(0.55f, 0f, 0.08f, 0.24f);
+ overlay.gameObject.SetActive(true);
+ SoundManager.Instance.PlaySound(sound, true);
+
+ while (player && !player.Data.IsDead && !player.Data.Disconnected && AmongUsClient.Instance.IsGameStarted && !MeetingHud.Instance && !ExileController.Instance && Time.time < expiresAt && Revenant.Phases.TryGetValue(player.PlayerId, out var phase) && phase == Revenant.Phase.DoomActive)
+ {
+ if (physics.GetVelocity().sqrMagnitude > 0.01f)
+ {
+ var ghost = new GameObject("RevenantTrail");
+ var renderer = ghost.AddComponent();
+ var body = player.cosmetics.normalBodySprite.BodySprite;
+
+ renderer.sprite = body.sprite;
+ renderer.flipX = body.flipX;
+ renderer.flipY = body.flipY;
+ renderer.sharedMaterial = body.sharedMaterial;
+ renderer.sortingLayerID = body.sortingLayerID;
+ renderer.sortingOrder = body.sortingOrder + 1;
+ PlayerMaterial.SetColors(player.Data.DefaultOutfit.ColorId, renderer);
+ ghost.transform.SetPositionAndRotation(body.transform.position, body.transform.rotation);
+ ghost.transform.localScale = body.transform.lossyScale;
+ Coroutines.Start(Utils.FadeAndDestroy(ghost, 0.8f));
+ }
+
+ foreach (var target in PlayerControl.AllPlayerControls)
+ {
+ if (!target || target.Data.IsDead || target.Data.Disconnected || target.inVent || target.Data.Role.IsImpostor)
+ continue;
+
+ if (Vector2.Distance(player.GetTruePosition(), target.GetTruePosition()) > options.DoomContactRadius)
+ continue;
+
+ Revenant.RpcRequestDoomContact(player, target);
+ break;
+ }
+
+ yield return interval;
+ }
+
+ if (physics)
+ physics.Speed = originalSpeed;
+ if (overlay)
+ {
+ overlay.color = originalColor;
+ overlay.gameObject.SetActive(overlayWasActive);
+ }
+
+ SoundManager.Instance.StopSound(sound);
+ if (player && !player.Data.IsDead && AmongUsClient.Instance.IsGameStarted)
+ SoundManager.Instance.PlaySoundImmediate(NewModAsset.DoomAwakeningEndSound.LoadAsset(), false, 1f, 1f, null);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/DrainButton.cs b/NewMod/Buttons/Roles/DrainButton.cs
new file mode 100644
index 0000000..6d08201
--- /dev/null
+++ b/NewMod/Buttons/Roles/DrainButton.cs
@@ -0,0 +1,81 @@
+using Il2CppSystem;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public sealed class DrainButton : CustomActionButton
+{
+ private PlayerControl _target;
+ private bool _breachMode;
+
+ public override string Name => "Siphon";
+ public override float Cooldown => OptionGroupSingleton.Instance.SiphonCooldown;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => MiraAssets.Empty;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is EnergyThief;
+ }
+
+ public override bool CanUse()
+ {
+ if (!base.CanUse() || EnergyThief.BreachActive)
+ return false;
+
+ var player = PlayerControl.LocalPlayer;
+ var ready = EnergyThief.IsReady(player.PlayerId);
+
+ if (ready != _breachMode)
+ {
+ _breachMode = ready;
+ OverrideName(ready ? "GRID BREACH" : "Siphon");
+ }
+
+ if (ready)
+ {
+ if (_target)
+ {
+ _target.cosmetics.SetOutline(false, new Nullable(Color.magenta));
+ _target = null;
+ }
+
+ return EnergyThief.NodeOwnerId == player.PlayerId && Vector2.Distance(player.GetTruePosition(), EnergyThief.GetNodePosition()) <= OptionGroupSingleton.Instance.GridBreachRadius;
+ }
+
+ if (EnergyThief.TetherTargets.ContainsKey(player.PlayerId))
+ return false;
+
+ var previous = _target;
+ _target = player.GetClosestPlayer(true, OptionGroupSingleton.Instance.SiphonRange, false, false, candidate => !candidate.Data.IsDead && !candidate.Data.Disconnected && (!EnergyThief.HarvestedTargets.TryGetValue(player.PlayerId, out var harvested) || !harvested.Contains(candidate.PlayerId)));
+
+ if (previous && previous != _target)
+ previous.cosmetics.SetOutline(false, new Nullable(Color.magenta));
+
+ if (_target)
+ _target.cosmetics.SetOutline(true, new Nullable(Color.magenta));
+
+ return _target;
+ }
+
+ protected override void OnClick()
+ {
+ if (_breachMode)
+ {
+ EnergyThief.RpcRequestBreach(PlayerControl.LocalPlayer);
+ return;
+ }
+
+ _target.cosmetics.SetOutline(false, new Nullable(Color.magenta));
+ EnergyThief.RpcRequestSiphon(PlayerControl.LocalPlayer, _target);
+ _target = null;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/EnergyGroundButton.cs b/NewMod/Buttons/Roles/EnergyGroundButton.cs
new file mode 100644
index 0000000..51b938c
--- /dev/null
+++ b/NewMod/Buttons/Roles/EnergyGroundButton.cs
@@ -0,0 +1,59 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public sealed class EnergyGroundButton : CustomActionButton
+{
+ public override string Name => "Ground";
+ public override float Cooldown => 0f;
+ public override float EffectDuration => OptionGroupSingleton.Instance.GroundDuration;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.GroundAbilityButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return true;
+ }
+
+ public override bool CanUse()
+ {
+ var player = PlayerControl.LocalPlayer;
+
+ if (!player)
+ return false;
+
+ return EnergyThief.BreachActive && player.PlayerId != EnergyThief.NodeOwnerId && Vector2.Distance(player.GetTruePosition(), EnergyThief.GetNodePosition()) <= OptionGroupSingleton.Instance.GridBreachRadius && base.CanUse();
+ }
+
+ protected override void OnClick()
+ {
+ //indeed nothing
+ }
+
+ public override void OnEffectEnd()
+ {
+ if (CanUse())
+ EnergyThief.RpcRequestGround(PlayerControl.LocalPlayer);
+ }
+
+ protected override void FixedUpdate(PlayerControl playerControl)
+ {
+ var visible = EnergyThief.BreachActive && playerControl.PlayerId != EnergyThief.NodeOwnerId;
+ Button.ToggleVisible(visible);
+
+ var holdingInRange = visible && playerControl.moveable && Vector2.Distance(playerControl.GetTruePosition(), EnergyThief.GetNodePosition()) <= OptionGroupSingleton.Instance.GridBreachRadius;
+
+ if (EffectActive && !holdingInRange)
+ {
+ EffectActive = false;
+ Timer = 0f;
+ }
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/FakeBodyButton.cs b/NewMod/Buttons/Roles/FakeBodyButton.cs
new file mode 100644
index 0000000..8d20b9b
--- /dev/null
+++ b/NewMod/Buttons/Roles/FakeBodyButton.cs
@@ -0,0 +1,36 @@
+using System.Linq;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class FakeBodyButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Control;
+ public override string Name => "Prank";
+ public override float Cooldown => OptionGroupSingleton.Instance.PrankCooldown;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.DeadBodySprite;
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && !PranksterUtilities.FindAllPranksterBodies().Any(body => body.ParentId == PlayerControl.LocalPlayer.PlayerId);
+ }
+
+ protected override void OnClick()
+ {
+ PranksterUtilities.CreatePranksterDeadBody(PlayerControl.LocalPlayer, PlayerControl.LocalPlayer.PlayerId);
+ }
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Prankster;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/FeignDeathButton.cs b/NewMod/Buttons/Roles/FeignDeathButton.cs
new file mode 100644
index 0000000..63312a7
--- /dev/null
+++ b/NewMod/Buttons/Roles/FeignDeathButton.cs
@@ -0,0 +1,37 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.ImpostorRoles;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public sealed class FeignDeathButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Protection;
+ public override string Name => "Feign Death";
+ public override float InitialCooldown => 0f;
+ public override float Cooldown => OptionGroupSingleton.Instance.FeignDeathCooldown;
+ public override int MaxUses => 1;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.DeadBodySprite;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Revenant;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && (!Revenant.Phases.TryGetValue(PlayerControl.LocalPlayer.PlayerId, out var phase) || phase == Revenant.Phase.Ready);
+ }
+
+ protected override void OnClick()
+ {
+ Revenant.RpcRequestFeign(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/InjectButton.cs b/NewMod/Buttons/Roles/InjectButton.cs
new file mode 100644
index 0000000..ca08ef7
--- /dev/null
+++ b/NewMod/Buttons/Roles/InjectButton.cs
@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+using Il2CppSystem;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class InjectButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Control;
+ public override string Name => "Inject";
+ public override float Cooldown => 0.25f;
+ public override float Distance => OptionGroupSingleton.Instance.InjectionRange;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.InjectButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is InjectorRole;
+ }
+
+ public override void CreateButton(Transform parent)
+ {
+ base.CreateButton(parent);
+ Button.graphic.SetCooldownNormalizedUvs();
+ }
+
+ public override PlayerControl GetTarget()
+ {
+ var player = PlayerControl.LocalPlayer;
+ if (InjectorUtilities.Experiments.TryGetValue(player.PlayerId, out var sample))
+ {
+ var target = Utils.PlayerById(sample.TargetId);
+ return target && !target.Data.IsDead && !target.Data.Disconnected && !target.inVent && Time.time >= sample.ReadyAt && Vector2.Distance(player.GetTruePosition(), target.GetTruePosition()) <= Distance && !PhysicsHelpers.AnythingBetween(player.GetTruePosition(), target.GetTruePosition(), Constants.ShipAndObjectsMask, false) ? target : null;
+ }
+
+ if (Time.time < InjectorUtilities.NextInjection.GetValueOrDefault(player.PlayerId)) return null;
+ return player.GetClosestPlayer(false, Distance, predicate: target => !target.inVent && !InjectorUtilities.Samples.Contains((player.PlayerId, target.PlayerId)));
+ }
+
+ public override void SetOutline(bool active)
+ {
+ Target?.cosmetics.SetOutline(active, new Nullable(Palette.AcceptedGreen));
+ }
+
+ protected override void FixedUpdate(PlayerControl player)
+ {
+ OverrideName(InjectorUtilities.Experiments.ContainsKey(player.PlayerId) ? "Collect Sample" : Name);
+ }
+
+ protected override void OnClick()
+ {
+ var player = PlayerControl.LocalPlayer;
+ if (InjectorUtilities.Experiments.ContainsKey(player.PlayerId))
+ InjectorUtilities.RpcCollectSample(player);
+ else
+ InjectorUtilities.RpcApplySerum(player, Target, InjectorUtilities.SelectedSerum);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/InspectBodyButton.cs b/NewMod/Buttons/Roles/InspectBodyButton.cs
new file mode 100644
index 0000000..9d9ee7e
--- /dev/null
+++ b/NewMod/Buttons/Roles/InspectBodyButton.cs
@@ -0,0 +1,58 @@
+using System.Linq;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class InspectBodyButton : CustomActionButton
+{
+ public DeadBody Inspecting;
+ public override string Name => "Inspect Body";
+ public override float Cooldown => 3f;
+ public override float Distance => OptionGroupSingleton.Instance.InspectRange;
+ public override float EffectDuration => OptionGroupSingleton.Instance.InspectDuration;
+ public override LoadableAsset Sprite => NewModAsset.DeadBodySprite;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return Button && PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data && !PlayerControl.LocalPlayer.Data.IsDead && PlayerControl.AllPlayerControls.ToArray().Any(player => player.Data && player.Data.Role is Prankster);
+ }
+
+ public override DeadBody GetTarget()
+ {
+ var position = PlayerControl.LocalPlayer.GetTruePosition();
+ return Helpers.GetNearestDeadBodies(position, Distance, Helpers.CreateFilter(Constants.NotShipMask)).Where(body => !body.Reported && !PhysicsHelpers.AnythingBetween(position, body.TruePosition, Constants.ShipAndObjectsMask, false)).OrderBy(body => Vector2.Distance(position, body.TruePosition)).FirstOrDefault();
+ }
+
+ public override void SetOutline(bool active)
+ {
+ }
+
+ protected override void OnClick()
+ {
+ Inspecting = Target;
+ }
+
+ protected override void FixedUpdate(PlayerControl player)
+ {
+ if (EffectActive && (!Inspecting || Vector2.Distance(player.GetTruePosition(), Inspecting.TruePosition) > Distance || MeetingHud.Instance))
+ {
+ EffectActive = false;
+ Timer = Cooldown;
+ }
+ }
+
+ public override void OnEffectEnd()
+ {
+ if (!Inspecting || Vector2.Distance(PlayerControl.LocalPlayer.GetTruePosition(), Inspecting.TruePosition) > Distance || MeetingHud.Instance) return;
+ var position = Inspecting.TruePosition;
+ PranksterUtilities.RpcInspectBody(PlayerControl.LocalPlayer, Inspecting.ParentId, position.x, position.y);
+ Inspecting = null;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/OfferChampionButton.cs b/NewMod/Buttons/Roles/OfferChampionButton.cs
new file mode 100644
index 0000000..0b1f4f9
--- /dev/null
+++ b/NewMod/Buttons/Roles/OfferChampionButton.cs
@@ -0,0 +1,52 @@
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Meeting;
+using MiraAPI.Events.Vanilla.Gameplay;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Roles.ImpostorRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class OfferChampionButton : CustomActionButton
+{
+ public static CustomPlayerMenu ActiveMenu;
+ public override string Name => "Offer Alliance";
+ public override float Cooldown => 0f;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.TyrantAlliance;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Tyrant;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && !ActiveMenu && Tyrant.ApexThroneReady && Tyrant.ChampionId == byte.MaxValue;
+ }
+
+ [RegisterEvent]
+ public static void OnMeetingStart(StartMeetingEvent evt)
+ {
+ if (ActiveMenu) ActiveMenu.ForceClose();
+ }
+
+ [RegisterEvent]
+ public static void OnGameEnd(GameEndEvent evt)
+ {
+ if (ActiveMenu) ActiveMenu.ForceClose();
+ }
+
+ protected override void OnClick()
+ {
+ var menu = CustomPlayerMenu.Create();
+ ActiveMenu = menu;
+ menu.Begin(player => player != PlayerControl.LocalPlayer && !player.Data.IsDead && !player.Data.Disconnected, player =>
+ {
+ if (player) Tyrant.RpcNotifyChampion(PlayerControl.LocalPlayer, player);
+ menu.ForceClose();
+ });
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/RejectChampionButton.cs b/NewMod/Buttons/Roles/RejectChampionButton.cs
new file mode 100644
index 0000000..7b39b63
--- /dev/null
+++ b/NewMod/Buttons/Roles/RejectChampionButton.cs
@@ -0,0 +1,23 @@
+using MiraAPI.Hud;
+using MiraAPI.Utilities.Assets;
+using NewMod.Roles.ImpostorRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class RejectChampionButton : CustomActionButton
+{
+ public override string Name => "Reject Alliance";
+ public override float Cooldown => 0f;
+ public override LoadableAsset Sprite => NewModAsset.CrownIcon;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return Button && PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data && PlayerControl.LocalPlayer.PlayerId == Tyrant.ChampionId && !Tyrant.OfferAnswered && !PlayerControl.LocalPlayer.Data.IsDead;
+ }
+
+ protected override void OnClick()
+ {
+ Tyrant.RpcAnswerOffer(PlayerControl.LocalPlayer, false);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/ReviveButton.cs b/NewMod/Buttons/Roles/ReviveButton.cs
new file mode 100644
index 0000000..f8db6be
--- /dev/null
+++ b/NewMod/Buttons/Roles/ReviveButton.cs
@@ -0,0 +1,116 @@
+using System.Linq;
+using AmongUs.GameOptions;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.ImpostorRoles;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+///
+/// Defines a custom action button for the role.
+///
+public class ReviveButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Protection;
+
+ ///
+ /// The name displayed on the button. Intentionally left empty to show an existing name elsewhere.
+ ///
+ public override string Name => ""; // It's currently empty since the button has already a name on it
+
+ ///
+ /// Gets the cooldown time for this button, based on .
+ ///
+ public override float Cooldown => OptionGroupSingleton.Instance.ButtonCooldown;
+
+ ///
+ /// Gets the maximum number of uses for this button, based on .
+ ///
+ public override int MaxUses => (int)OptionGroupSingleton.Instance.AbilityUses;
+
+ ///
+ /// Determines how long the effect from clicking the button lasts. In this case, no duration is set.
+ ///
+ public override float EffectDuration => 0f;
+
+ ///
+ /// Default keybind for Necromancer's Revive ability.
+ ///
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+
+ ///
+ /// Defines where on the screen this button should appear.
+ ///
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+
+ ///
+ /// The visual icon for this button, set to the necromancer sprite asset.
+ ///
+ public override LoadableAsset Sprite => NewModAsset.NecromancerButton;
+
+ public DeadBody GetReviveTarget()
+ {
+ var local = PlayerControl.LocalPlayer;
+ var localPos = local.GetTruePosition();
+
+ return Helpers.GetNearestDeadBodies(localPos, ShipStatus.Instance.MaxLightRadius, Helpers.CreateFilter(Constants.NotShipMask)).Where(body => body != null && IsValidReviveTarget(local, body)).OrderBy(body => Vector2.Distance(localPos, body.TruePosition)).FirstOrDefault();
+ }
+
+ public static bool IsValidReviveTarget(PlayerControl local, DeadBody body)
+ {
+ if (PranksterUtilities.IsPranksterBody(body))
+ return false;
+
+ var killedPlayer = GameData.Instance.GetPlayerById(body.ParentId)?.Object;
+ if (!killedPlayer || !killedPlayer.Data.IsDead || killedPlayer.Data.Disconnected)
+ return false;
+
+ var killer = Utils.GetKiller(killedPlayer);
+
+ if (killer != null && killer.PlayerId == local.PlayerId)
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Checks whether the player can currently use the revive button, ensuring cooldowns, ability uses, and conditions are
+ /// met.
+ ///
+ /// True if all requirements to use this button are met; otherwise false.
+ /// Invoked when the revive button is clicked. Plays a sound and revives the nearest dead body.
+ ///
+ protected override void OnClick()
+ {
+ var local = PlayerControl.LocalPlayer;
+ var body = GetReviveTarget();
+
+ SoundManager.Instance.PlaySound(NewModAsset.ReviveSound?.LoadAsset(), false, 2f);
+
+ Utils.HandleRevive(local, body.ParentId, RoleTypes.Crewmate, body.transform.position.x, body.transform.position.y);
+ }
+
+ ///
+ /// Determines whether this button is enabled for the role, returning true if the role is
+ /// .
+ ///
+ /// The current player's role.
+ /// True if the role is Necromancer; otherwise false.
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is NecromancerRole;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/RevivedKillButton.cs b/NewMod/Buttons/Roles/RevivedKillButton.cs
new file mode 100644
index 0000000..f6d82e4
--- /dev/null
+++ b/NewMod/Buttons/Roles/RevivedKillButton.cs
@@ -0,0 +1,72 @@
+using AmongUs.GameOptions;
+using Il2CppSystem;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Networking;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using NewMod.Roles.ImpostorRoles;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class RevivedKillButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Aggression;
+ public override string Name => "KILL";
+ public override float Cooldown => GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown);
+ public override int MaxUses => 0;
+ public override float EffectDuration => 0f;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override LoadableAsset Sprite => NewModAsset.VanillaKillButton;
+
+ public static bool CanUseRevivedKillButton()
+ {
+ var local = PlayerControl.LocalPlayer;
+ return local && local.Data && !local.Data.IsDead && NecromancerRole.RevivedPlayers.ContainsKey(local.PlayerId);
+ }
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return CanUseRevivedKillButton();
+ }
+
+ protected override void FixedUpdate(PlayerControl playerControl)
+ {
+ Button?.ToggleVisible(CanUseRevivedKillButton());
+ }
+
+ public override PlayerControl GetTarget()
+ {
+ return PlayerControl.LocalPlayer.GetClosestPlayer(true, Distance);
+ }
+
+ public override bool IsTargetValid(PlayerControl target)
+ {
+ return CanUseRevivedKillButton() && target != null && target.PlayerId != PlayerControl.LocalPlayer.PlayerId && !target.Data.IsDead && !target.Data.Disconnected;
+ }
+
+ public override void SetOutline(bool active)
+ {
+ if (Target && Target.cosmetics)
+ Target.cosmetics.SetOutline(active, new Nullable(Palette.ImpostorRed));
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && CanUseRevivedKillButton();
+ }
+
+ protected override void OnClick()
+ {
+ var local = PlayerControl.LocalPlayer;
+
+ local.RpcCustomMurder(Target, MeetingCheck.OutsideMeeting);
+
+ NecromancerRole.RevivedPlayers.Remove(local.PlayerId);
+ ResetTarget();
+ Button?.ToggleVisible(false);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/AnchorButton.cs b/NewMod/Buttons/Roles/S1/AnchorButton.cs
new file mode 100644
index 0000000..7329590
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/AnchorButton.cs
@@ -0,0 +1,52 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Roles.NeutralRoles.S1;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class AnchorButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Mobility;
+ public byte _roomId;
+
+ public override string Name => "Anchor";
+ public override float Cooldown => OptionGroupSingleton.Instance.AnchorCooldown;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.AnchorButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Nomad;
+ }
+
+ public override bool CanUse()
+ {
+ if (!base.CanUse())
+ return false;
+
+ foreach (var room in ShipStatus.Instance.AllRooms)
+ {
+ if (room.RoomId == SystemTypes.Hallway || !room.roomArea.OverlapPoint(PlayerControl.LocalPlayer.GetTruePosition()))
+ continue;
+
+ _roomId = (byte)room.RoomId;
+ return true;
+ }
+
+ return false;
+ }
+
+ protected override void OnClick()
+ {
+ var position = PlayerControl.LocalPlayer.GetTruePosition();
+ Nomad.RpcRequestAnchor(PlayerControl.LocalPlayer, _roomId, position.x, position.y);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/ArbitratorLeverageButton.cs b/NewMod/Buttons/Roles/S1/ArbitratorLeverageButton.cs
new file mode 100644
index 0000000..26cf19d
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/ArbitratorLeverageButton.cs
@@ -0,0 +1,53 @@
+using Il2CppSystem;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Roles.NeutralRoles.S1;
+using NewMod.Utilities;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class ArbitratorLeverageButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Intelligence;
+ public override string Name => "Leverage";
+ public override float Cooldown => OptionGroupSingleton.Instance.LeverageCooldown;
+ public override float Distance => OptionGroupSingleton.Instance.LeverageRange;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override LoadableAsset Sprite => NewModAsset.LeverageButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is ArbitratorRole;
+ }
+
+ public override PlayerControl GetTarget()
+ {
+ return PlayerControl.LocalPlayer.GetClosestPlayer(false, Distance, predicate: player => !player.inVent && ArbitratorRole.LastVotes.ContainsKey(player.PlayerId));
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && ArbitratorRole.LastVotes.ContainsKey(PlayerControl.LocalPlayer.PlayerId);
+ }
+
+ public override void SetOutline(bool active)
+ {
+ Target?.cosmetics.SetOutline(active, new Nullable(Color.yellow));
+ }
+
+ protected override void OnClick()
+ {
+ var sameVote = ArbitratorRole.LastVotes[PlayerControl.LocalPlayer.PlayerId] == ArbitratorRole.LastVotes[Target.PlayerId];
+ Coroutines.Start(CoroutinesHelper.CoNotify(sameVote ? $"{Target.Data.PlayerName} chose the same vote as you last meeting." : $"{Target.Data.PlayerName} chose a different vote last meeting."));
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/CashOutButton.cs b/NewMod/Buttons/Roles/S1/CashOutButton.cs
new file mode 100644
index 0000000..4898f9a
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/CashOutButton.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Roles.NeutralRoles.S1;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class CashOutButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Aggression;
+ public override string Name => "Cash Out";
+ public override float Cooldown => OptionGroupSingleton.Instance.CashOutCooldown;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => MiraAssets.Empty;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Bounty;
+ }
+
+ public override bool CanUse()
+ {
+ var bountyId = PlayerControl.LocalPlayer.PlayerId;
+ if (!base.CanUse() || !Bounty.Contracts.TryGetValue(bountyId, out var contract) || !Bounty.CanCollect(bountyId, Time.time))
+ return false;
+
+ var target = Utils.PlayerById(contract.TargetId);
+ return !target.Data.IsDead && !target.Data.Disconnected && !target.inVent && !PhysicsHelpers.AnythingBetween(PlayerControl.LocalPlayer.GetTruePosition(), target.GetTruePosition(), Constants.ShipAndObjectsMask, false) && Vector2.Distance(PlayerControl.LocalPlayer.GetTruePosition(), target.GetTruePosition()) <= OptionGroupSingleton.Instance.CashOutRange;
+ }
+
+ protected override void OnClick()
+ {
+ Bounty.RpcRequestCashOut(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/ClaimButton.cs b/NewMod/Buttons/Roles/S1/ClaimButton.cs
new file mode 100644
index 0000000..6dd1be0
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/ClaimButton.cs
@@ -0,0 +1,58 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Roles.NeutralRoles.S1;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class ClaimButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Control;
+ public PlayerControl _target;
+
+ public override string Name => "Claim";
+ public override float Cooldown => OptionGroupSingleton.Instance.ClaimCooldown;
+ public override int MaxUses => 1;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => MiraAssets.Empty;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Usurper;
+ }
+
+ public override bool CanUse()
+ {
+ if (!base.CanUse() || !Usurper.States.TryGetValue(PlayerControl.LocalPlayer.PlayerId, out var state) || state.Phase != Usurper.CrownPhase.Unclaimed)
+ return false;
+
+ _target = null;
+ var range = OptionGroupSingleton.Instance.ClaimRange;
+ foreach (var player in PlayerControl.AllPlayerControls)
+ {
+ if (player == PlayerControl.LocalPlayer || player.Data.IsDead || player.Data.Disconnected || player.Data.Role is Usurper)
+ continue;
+
+ var distance = Vector2.Distance(PlayerControl.LocalPlayer.GetTruePosition(), player.GetTruePosition());
+ if (distance >= range)
+ continue;
+
+ range = distance;
+ _target = player;
+ }
+
+ return _target;
+ }
+
+ protected override void OnClick()
+ {
+ Usurper.RpcRequestClaim(PlayerControl.LocalPlayer, _target);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/ConvertFragmentsButton.cs b/NewMod/Buttons/Roles/S1/ConvertFragmentsButton.cs
new file mode 100644
index 0000000..5f9b612
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/ConvertFragmentsButton.cs
@@ -0,0 +1,32 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles.S1;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class ConvertFragmentsButton : CustomActionButton
+{
+ public override string Name => "Convert";
+ public override float Cooldown => 1f;
+ public override LoadableAsset Sprite => NewModAsset.LeverageButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Collector;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && !Collector.VictoryArmed.Contains(PlayerControl.LocalPlayer.PlayerId) && Collector.Inventories.TryGetValue(PlayerControl.LocalPlayer.PlayerId, out var inventory) && Collector.CanConvert(inventory, (int)OptionGroupSingleton.Instance.ConversionCost);
+ }
+
+ protected override void OnClick()
+ {
+ Collector.RpcRequestConvert(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/DeadlockButton.cs b/NewMod/Buttons/Roles/S1/DeadlockButton.cs
new file mode 100644
index 0000000..bac6633
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/DeadlockButton.cs
@@ -0,0 +1,48 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using MiraAPI.Utilities;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+using DeadwireRole = NewMod.Roles.ImpostorRoles.S1.Deadwire;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class DeadlockButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Control;
+ public override float Distance => OptionGroupSingleton.Instance.DeadlockRange;
+
+ public override string Name => "Deadlock";
+ public override float InitialCooldown => 0f;
+ public override float Cooldown => OptionGroupSingleton.Instance.DeadlockCooldown;
+ public override int MaxUses => (int)OptionGroupSingleton.Instance.DeadlockUses;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.DeadlockButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is DeadwireRole;
+ }
+
+ public override PlayerControl GetTarget()
+ {
+ return DeadwireRole.Records.ContainsKey(PlayerControl.LocalPlayer.PlayerId) ? null : PlayerControl.LocalPlayer.GetClosestPlayer(false, Distance, predicate: player => !player.inVent);
+ }
+
+ public override void SetOutline(bool active)
+ {
+ if (Target)
+ Target.cosmetics.SetOutline(active, new Il2CppSystem.Nullable(Color.red));
+ }
+
+ protected override void OnClick()
+ {
+ DeadwireRole.RpcRequestDeadlock(PlayerControl.LocalPlayer, Target);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/EnterVoidButton.cs b/NewMod/Buttons/Roles/S1/EnterVoidButton.cs
new file mode 100644
index 0000000..814996c
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/EnterVoidButton.cs
@@ -0,0 +1,45 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Modifiers;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Modifiers.S1;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.ImpostorRoles.S1;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class EnterVoid : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Mobility;
+ public override string Name => "Enter Void";
+ public override float Cooldown => OptionGroupSingleton.Instance.EnterVoidCooldown;
+ public override LoadableAsset Sprite => NewModAsset.EnterVoid;
+
+ public override bool IsEffectCancellable()
+ {
+ return true;
+ }
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Voidwalker;
+ }
+
+ public override float EffectDuration => OptionGroupSingleton.Instance.VoidTime;
+
+ protected override void OnClick()
+ {
+ PlayerControl.LocalPlayer.RpcAddModifier();
+ OverrideName("Exit Void");
+ }
+
+ public override void OnEffectEnd()
+ {
+ PlayerControl.LocalPlayer.RpcRemoveModifier();
+ OverrideName("Enter Void");
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/HarvestButton.cs b/NewMod/Buttons/Roles/S1/HarvestButton.cs
new file mode 100644
index 0000000..e210c93
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/HarvestButton.cs
@@ -0,0 +1,53 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Roles.NeutralRoles.S1;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class HarvestButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Control;
+ public uint _fragmentId;
+
+ public override string Name => "Harvest";
+ public override float Cooldown => OptionGroupSingleton.Instance.HarvestCooldown;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => MiraAssets.Empty;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Collector;
+ }
+
+ public override bool CanUse()
+ {
+ if (!base.CanUse())
+ return false;
+
+ var range = OptionGroupSingleton.Instance.HarvestRange;
+ foreach (var pair in Collector.Fragments)
+ {
+ var distance = Vector2.Distance(PlayerControl.LocalPlayer.GetTruePosition(), pair.Value.Position);
+ if (distance >= range)
+ continue;
+
+ range = distance;
+ _fragmentId = pair.Key;
+ }
+
+ return range < OptionGroupSingleton.Instance.HarvestRange;
+ }
+
+ protected override void OnClick()
+ {
+ Collector.RpcRequestHarvest(PlayerControl.LocalPlayer, _fragmentId);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/InspectResidualButton.cs b/NewMod/Buttons/Roles/S1/InspectResidualButton.cs
new file mode 100644
index 0000000..f0634fd
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/InspectResidualButton.cs
@@ -0,0 +1,40 @@
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Roles.CrewmateRoles.S1;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class InspectResidualButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Intelligence;
+ public const float InspectRange = 1.25f;
+
+ public override string Name => "Inspect";
+ public override float Cooldown => 0f;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override LoadableAsset Sprite => NewModAsset.WardenInvestigate;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is WardenRole;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && WardenRole.GetNearestResidualMark(PlayerControl.LocalPlayer.GetTruePosition(), InspectRange);
+ }
+
+ protected override void OnClick()
+ {
+ var mark = WardenRole.GetNearestResidualMark(PlayerControl.LocalPlayer.GetTruePosition(), InspectRange);
+
+ if (mark)
+ WardenRole.InspectResidual(mark);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/ManifestButton.cs b/NewMod/Buttons/Roles/S1/ManifestButton.cs
new file mode 100644
index 0000000..af81a8e
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/ManifestButton.cs
@@ -0,0 +1,39 @@
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.GameOptions;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Roles.NeutralRoles.S1;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class ManifestButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Control;
+ public override string Name => "Manifest";
+ public override float Cooldown => 5f;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+ public override LoadableAsset Sprite => MiraAssets.Empty;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Collector;
+ }
+
+ public override bool CanUse()
+ {
+ if (!base.CanUse() || Collector.VictoryArmed.Contains(PlayerControl.LocalPlayer.PlayerId) || !Collector.Inventories.TryGetValue(PlayerControl.LocalPlayer.PlayerId, out var inventory))
+ return false;
+
+ return Collector.CanManifest(inventory);
+ }
+
+ protected override void OnClick()
+ {
+ Collector.RpcRequestManifest(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/ObjectiveButton.cs b/NewMod/Buttons/Roles/S1/ObjectiveButton.cs
new file mode 100644
index 0000000..7fed4cb
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/ObjectiveButton.cs
@@ -0,0 +1,42 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Roles.NeutralRoles.S1;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class ObjectiveButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Aggression;
+ public override string Name => "Terminate";
+ public override float Cooldown => 0f;
+ public override int MaxUses => 1;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override LoadableAsset Sprite => NewModAsset.TerminateButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is TerminatorRole;
+ }
+
+ public override bool CanUse()
+ {
+ if (!base.CanUse() || !TerminatorRole.ObjectiveSpawned || TerminatorRole.FinalCountdownActive)
+ return false;
+
+ var radius = OptionGroupSingleton.Instance.FinalObjectiveRadius;
+ return Vector2.Distance(PlayerControl.LocalPlayer.GetTruePosition(), TerminatorRole.ObjectivePosition) <= radius;
+ }
+
+ protected override void OnClick()
+ {
+ TerminatorRole.RpcStartFinalCountdown(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/OverrideButton.cs b/NewMod/Buttons/Roles/S1/OverrideButton.cs
new file mode 100644
index 0000000..f896b3b
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/OverrideButton.cs
@@ -0,0 +1,51 @@
+using NewMod.Roles.ImpostorRoles.S1;
+using MiraAPI.Translation;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.GameOptions;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+using NewMod.Utilities;
+using DeadwireRole = NewMod.Roles.ImpostorRoles.S1.Deadwire;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class OverrideButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Control;
+ public override string Name => "Override";
+ public override float InitialCooldown => 0f;
+ public override float Cooldown => OptionGroupSingleton.Instance.OverrideCooldown;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.OverrideButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is DeadwireRole;
+ }
+
+ public override bool CanUse()
+ {
+ if (!base.CanUse() || !DeadwireRole.Records.TryGetValue(PlayerControl.LocalPlayer.PlayerId, out var record))
+ return false;
+ if (DeadwireRole.GetResponse(record.Category) is not (Deadwire.Response.TrackActor or Deadwire.Response.JamActor))
+ return true;
+ var actor = Utils.PlayerById(record.ActorId);
+ return actor && !actor.Data.IsDead && !actor.Data.Disconnected;
+ }
+
+ protected override void FixedUpdate(PlayerControl playerControl)
+ {
+ OverrideName(DeadwireRole.Records.TryGetValue(playerControl.PlayerId, out var record) ? MiraLocaleManager.Get($"NewMod.Deadwire.Reward.{DeadwireRole.GetResponse(record.Category)}") : Name);
+ }
+
+ protected override void OnClick()
+ {
+ DeadwireRole.RpcRequestOverride(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/ReflectButton.cs b/NewMod/Buttons/Roles/S1/ReflectButton.cs
new file mode 100644
index 0000000..8731ec7
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/ReflectButton.cs
@@ -0,0 +1,38 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.ImpostorRoles.S1;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class MirrorReflectButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Protection;
+ public override string Name => "Reflect";
+ public override float Cooldown => OptionGroupSingleton.Instance.ReflectCooldown;
+ public override float EffectDuration => OptionGroupSingleton.Instance.ReflectWindow;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override LoadableAsset Sprite => NewModAsset.ReflectButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is MirrorBladeRole;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && !MirrorBladeRole.ArmedReflections.Contains(PlayerControl.LocalPlayer.PlayerId);
+ }
+
+ protected override void OnClick()
+ {
+ MirrorBladeRole.RpcArmReflection(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/TerminateButton.cs b/NewMod/Buttons/Roles/S1/TerminateButton.cs
new file mode 100644
index 0000000..6f659e7
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/TerminateButton.cs
@@ -0,0 +1,47 @@
+using Il2CppSystem;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles.S1;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class TerminateButton : CustomActionButton
+{
+ public override string Name => "Terminate";
+ public override float Cooldown => 0f;
+ public override int MaxUses => 1;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override LoadableAsset Sprite => NewModAsset.TerminateButton;
+ public override float Distance => OptionGroupSingleton.Instance.CounterAttackRange;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return Button && PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data && role is not TerminatorRole;
+ }
+
+ public override void SetActive(bool visible, RoleBehaviour role)
+ {
+ base.SetActive(visible && TerminatorRole.FinalCountdownActive, role);
+ }
+
+ public override PlayerControl GetTarget()
+ {
+ var terminator = TerminatorRole.GetTerminator();
+ return terminator && !terminator.Data.IsDead && !terminator.Data.Disconnected && Vector2.Distance(PlayerControl.LocalPlayer.GetTruePosition(), terminator.GetTruePosition()) <= Distance && !PhysicsHelpers.AnythingBetween(PlayerControl.LocalPlayer.GetTruePosition(), terminator.GetTruePosition(), Constants.ShipAndObjectsMask, false) ? terminator : null;
+ }
+
+ public override void SetOutline(bool active)
+ {
+ Target?.cosmetics.SetOutline(active, new Nullable(Color.red));
+ }
+
+ protected override void OnClick()
+ {
+ TerminatorRole.RpcRequestTerminate(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/WanderButton.cs b/NewMod/Buttons/Roles/S1/WanderButton.cs
new file mode 100644
index 0000000..a0b8495
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/WanderButton.cs
@@ -0,0 +1,37 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Roles.NeutralRoles.S1;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class WanderButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Mobility;
+ public override string Name => "Wander";
+ public override float Cooldown => OptionGroupSingleton.Instance.WanderCooldown;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.WanderButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Nomad;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && Nomad.CanWander.Contains(PlayerControl.LocalPlayer.PlayerId);
+ }
+
+ protected override void OnClick()
+ {
+ Nomad.RpcRequestWander(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/WardenClueButton.cs b/NewMod/Buttons/Roles/S1/WardenClueButton.cs
new file mode 100644
index 0000000..589e126
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/WardenClueButton.cs
@@ -0,0 +1,27 @@
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Roles.CrewmateRoles.S1;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+public class WardenClueButton : CustomActionButton
+{
+ public override string Name => WardenRole.ClueType == 0 ? "Entry" : WardenRole.ClueType == 1 ? "Ability" : "Presence";
+ public override float Cooldown => 0f;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.TertiaryAbility;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override LoadableAsset Sprite => NewModAsset.WardenInvestigate;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is WardenRole;
+ }
+
+ protected override void OnClick()
+ {
+ WardenRole.ClueType = (WardenRole.ClueType + 1) % 3;
+ OverrideName(Name);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/S1/WardenSealButton.cs b/NewMod/Buttons/Roles/S1/WardenSealButton.cs
new file mode 100644
index 0000000..4f4277a
--- /dev/null
+++ b/NewMod/Buttons/Roles/S1/WardenSealButton.cs
@@ -0,0 +1,49 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.S1;
+using NewMod.Roles.CrewmateRoles.S1;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles.S1;
+
+[MiraIgnore]
+public class WardenSealButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Control;
+ public override string Name => "Seal";
+ public override float Cooldown => OptionGroupSingleton.Instance.SealCooldown;
+ public override float EffectDuration => OptionGroupSingleton.Instance.SealDuration;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override LoadableAsset Sprite => NewModAsset.WardenSeal;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is WardenRole;
+ }
+
+ public override bool CanUse()
+ {
+ if (!base.CanUse() || WardenRole.SealActive)
+ return false;
+
+ var room = WardenRole.GetRoom(PlayerControl.LocalPlayer.GetTruePosition());
+
+ return room && room.RoomId != SystemTypes.Hallway;
+ }
+
+ protected override void OnClick()
+ {
+ var player = PlayerControl.LocalPlayer;
+ var room = WardenRole.GetRoom(player.GetTruePosition());
+
+ if (!room || room.RoomId == SystemTypes.Hallway)
+ return;
+
+ WardenRole.RpcStartSeal(player, (byte)room.RoomId);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/SelectSerumButton.cs b/NewMod/Buttons/Roles/SelectSerumButton.cs
new file mode 100644
index 0000000..acb2902
--- /dev/null
+++ b/NewMod/Buttons/Roles/SelectSerumButton.cs
@@ -0,0 +1,27 @@
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class SelectSerumButton : CustomActionButton
+{
+ public override string Name => InjectorUtilities.SelectedSerum.ToString();
+ public override float Cooldown => 0f;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.InjectIcon;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is InjectorRole;
+ }
+
+ protected override void OnClick()
+ {
+ InjectorUtilities.SelectedSerum = InjectorUtilities.SelectedSerum == InjectorRole.SerumType.Adrenaline ? InjectorRole.SerumType.Sedative : InjectorRole.SerumType.Adrenaline;
+ OverrideName(Name);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/ShowScreenshotButton.cs b/NewMod/Buttons/Roles/ShowScreenshotButton.cs
new file mode 100644
index 0000000..764968d
--- /dev/null
+++ b/NewMod/Buttons/Roles/ShowScreenshotButton.cs
@@ -0,0 +1,49 @@
+using MiraAPI.Utilities.Assets;
+using MiraAPI.Hud;
+using MiraAPI.GameOptions;
+using NewMod.Options.Roles;
+using NewMod.Roles.CrewmateRoles;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+using NewMod.Utilities;
+using Reactor.Utilities;
+using MiraAPI.Keybinds;
+
+namespace NewMod.Buttons.Roles;
+
+///
+/// Defines a custom action button for the role.
+///
+public class ShowScreenshotButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Intelligence;
+
+ public override string Name => "Preview";
+
+ public override float Cooldown => 1f;
+
+ public override float EffectDuration => 0;
+
+ public override int MaxUses => 0;
+
+ public override LoadableAsset Sprite => NewModAsset.ShowScreenshotButton;
+
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && VisionaryUtilities.HasScreenshots && !VisionaryUtilities.IsShowing;
+ }
+
+ protected override void OnClick()
+ {
+ Coroutines.Start(VisionaryUtilities.ShowScreenshots(OptionGroupSingleton.Instance.MaxDisplayDuration));
+ }
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is TheVisionary;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/SpecialistModeButton.cs b/NewMod/Buttons/Roles/SpecialistModeButton.cs
new file mode 100644
index 0000000..5d9bcae
--- /dev/null
+++ b/NewMod/Buttons/Roles/SpecialistModeButton.cs
@@ -0,0 +1,35 @@
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Roles.CrewmateRoles;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class SpecialistModeButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Intelligence;
+ public bool CaptureOnClick => false;
+ public override string Name => "Scan Mode";
+ public override float Cooldown => 0f;
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.RadarIcon;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Specialist;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && !CustomButtonSingleton.Instance.EffectActive && Specialist.ScanStates.ContainsKey(PlayerControl.LocalPlayer.PlayerId);
+ }
+
+ protected override void OnClick()
+ {
+ Specialist.CycleMode();
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/SpecialistScanButton.cs b/NewMod/Buttons/Roles/SpecialistScanButton.cs
new file mode 100644
index 0000000..ff340d0
--- /dev/null
+++ b/NewMod/Buttons/Roles/SpecialistScanButton.cs
@@ -0,0 +1,54 @@
+using MiraAPI.Hud;
+using NewMod.Utilities;
+using MiraAPI.Keybinds;
+using MiraAPI.PluginLoading;
+using MiraAPI.Utilities.Assets;
+using NewMod.Roles.CrewmateRoles;
+using NewMod.Roles.NeutralRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class SpecialistScanButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Intelligence;
+ public override string Name => "Scan";
+ public Vector2 ScanOrigin;
+ public bool Interrupted;
+ public override float Cooldown => 8f;
+ public override float EffectDuration => 1.5f;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.RadarIcon;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Specialist;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && Specialist.ScanStates.TryGetValue(PlayerControl.LocalPlayer.PlayerId, out var state) && state.Charges > 0 && !Utils.IsActive(SystemTypes.Comms);
+ }
+
+ protected override void OnClick()
+ {
+ Interrupted = false;
+ ScanOrigin = PlayerControl.LocalPlayer.GetTruePosition();
+ }
+
+ protected override void FixedUpdate(PlayerControl playerControl)
+ {
+ if (EffectActive && (Vector2.Distance(ScanOrigin, playerControl.GetTruePosition()) >= 0.15f || Utils.IsActive(SystemTypes.Comms)))
+ {
+ Interrupted = true;
+ ResetCooldownAndOrEffect();
+ }
+ }
+
+ public override void OnEffectEnd()
+ {
+ if (!Interrupted && PlayerControl.LocalPlayer.Data.Role is Specialist && !PlayerControl.LocalPlayer.Data.IsDead && !MeetingHud.Instance && Vector2.Distance(ScanOrigin, PlayerControl.LocalPlayer.GetTruePosition()) < 0.15f && !Utils.IsActive(SystemTypes.Comms))
+ Specialist.Scan();
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/StrikeButton.cs b/NewMod/Buttons/Roles/StrikeButton.cs
new file mode 100644
index 0000000..b61863d
--- /dev/null
+++ b/NewMod/Buttons/Roles/StrikeButton.cs
@@ -0,0 +1,116 @@
+using System.Collections;
+using System.Linq;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Networking;
+using MiraAPI.Utilities;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.ImpostorRoles;
+using NewMod.Roles.NeutralRoles;
+using Reactor.Networking.Attributes;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class StrikeButton : CustomActionButton, IEnergyAbility
+{
+ public EnergyCategory Category => EnergyCategory.Aggression;
+ public override string Name => "Strike";
+ public override float Cooldown => OptionGroupSingleton.Instance.StrikeCooldown;
+ public override float EffectDuration => OptionGroupSingleton.Instance.ChargeDuration;
+ public override float Distance => OptionGroupSingleton.Instance.StrikeRange;
+ public override ButtonLocation Location => ButtonLocation.BottomRight;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.StrikeButton;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is PulseBlade;
+ }
+
+ public override PlayerControl GetTarget()
+ {
+ return PlayerControl.LocalPlayer.GetClosestPlayer(false, Distance, predicate: player => !player.inVent);
+ }
+
+ public override void SetOutline(bool active)
+ {
+ if (Target)
+ Target.cosmetics.SetOutline(active && !EffectActive, new Il2CppSystem.Nullable(Color.red));
+ }
+
+ protected override void FixedUpdate(PlayerControl playerControl)
+ {
+ OverrideName(EffectActive ? "Charging" : Name);
+ }
+
+ protected override void OnClick()
+ {
+ var player = PlayerControl.LocalPlayer;
+ RpcPulseStrike(player, (Target.GetTruePosition() - player.GetTruePosition()).normalized);
+ }
+
+ [MethodRpc((uint)CustomRPC.Dash)]
+ public static void RpcPulseStrike(PlayerControl source, Vector2 direction)
+ {
+ if (source.Data.Role is not PulseBlade || source.Data.IsDead || source.inVent || MeetingHud.Instance || ExileController.Instance)
+ return;
+
+ Coroutines.Start(DoPulseStrike(source, direction.normalized));
+ }
+
+ public static IEnumerator DoPulseStrike(PlayerControl player, Vector2 direction)
+ {
+ var options = OptionGroupSingleton.Instance;
+ if (player.AmOwner)
+ {
+ player.moveable = false;
+ player.MyPhysics.body.velocity = Vector2.zero;
+ }
+
+ var end = Time.time + options.ChargeDuration;
+ while (player && !player.Data.IsDead && !MeetingHud.Instance && Time.time < end)
+ {
+ player.cosmetics.currentBodySprite.BodySprite.UpdateOutline(Color.red);
+ yield return null;
+ }
+
+ if (!player)
+ yield break;
+ player.cosmetics.currentBodySprite.BodySprite.UpdateOutline(null);
+ if (!player.Data.IsDead && !MeetingHud.Instance && !ExileController.Instance && Vector2.Distance(PlayerControl.LocalPlayer.GetTruePosition(), player.GetTruePosition()) <= options.StrikeRange && !PhysicsHelpers.AnyNonTriggersBetween(player.GetTruePosition(), direction, options.DashSpeed * Time.fixedDeltaTime + 0.2f, Constants.ShipAndObjectsMask))
+ SoundManager.Instance.PlaySound(NewModAsset.StrikeSound.LoadAsset(), false);
+ if (!player.AmOwner)
+ yield break;
+
+ var remaining = options.StrikeRange;
+ while (!player.Data.IsDead && !MeetingHud.Instance && !ExileController.Instance && remaining > 0f)
+ {
+ var step = Mathf.Min(options.DashSpeed * Time.fixedDeltaTime, remaining);
+ var position = player.GetTruePosition();
+ if (PhysicsHelpers.AnyNonTriggersBetween(position, direction, step + 0.2f, Constants.ShipAndObjectsMask))
+ break;
+
+ player.MyPhysics.body.velocity = direction * (step / Time.fixedDeltaTime);
+ remaining -= step;
+ var target = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p != player && !p.Data.IsDead && !p.Data.Disconnected && !p.inVent && !p.Data.Role.IsImpostor && Vector2.Distance(position, p.GetTruePosition()) <= 0.4f && !PhysicsHelpers.AnythingBetween(position, p.GetTruePosition(), Constants.ShipAndObjectsMask, false));
+ if (target)
+ {
+ player.RpcCustomMurder(target, MeetingCheck.OutsideMeeting, resetKillTimer: false, teleportMurderer: false, showKillAnim: false);
+ break;
+ }
+
+ yield return new WaitForFixedUpdate();
+ }
+
+ player.MyPhysics.body.velocity = Vector2.zero;
+ end = Time.time + options.RecoveryDuration;
+ while (!player.Data.IsDead && !MeetingHud.Instance && !ExileController.Instance && Time.time < end)
+ yield return null;
+ if (!MeetingHud.Instance && !ExileController.Instance)
+ player.moveable = true;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/SubmitResearchButton.cs b/NewMod/Buttons/Roles/SubmitResearchButton.cs
new file mode 100644
index 0000000..d558c92
--- /dev/null
+++ b/NewMod/Buttons/Roles/SubmitResearchButton.cs
@@ -0,0 +1,31 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class SubmitResearchButton : CustomActionButton
+{
+ public override string Name => "Submit Research";
+ public override float Cooldown => OptionGroupSingleton.Instance.SubmissionDuration;
+ public override LoadableAsset Sprite => NewModAsset.InjectIcon;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is InjectorRole;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && PlayerControl.LocalPlayer.CanMove && !InjectorUtilities.Submitting.Contains(PlayerControl.LocalPlayer.PlayerId) && !InjectorUtilities.Submitted.Contains(PlayerControl.LocalPlayer.PlayerId) && InjectorUtilities.SampleCount(PlayerControl.LocalPlayer.PlayerId) >= OptionGroupSingleton.Instance.RequiredInjectCount;
+ }
+
+ protected override void OnClick()
+ {
+ InjectorUtilities.RpcSubmit(PlayerControl.LocalPlayer);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Roles/TyrantPulseButton.cs b/NewMod/Buttons/Roles/TyrantPulseButton.cs
new file mode 100644
index 0000000..e715e86
--- /dev/null
+++ b/NewMod/Buttons/Roles/TyrantPulseButton.cs
@@ -0,0 +1,34 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Keybinds;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles;
+using NewMod.Roles.ImpostorRoles;
+using UnityEngine;
+
+namespace NewMod.Buttons.Roles;
+
+public class TyrantPulseButton : CustomActionButton
+{
+ public override string Name => "Intimidate";
+ public override float Cooldown => OptionGroupSingleton.Instance.PulseCooldown;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+ public override LoadableAsset Sprite => NewModAsset.TyrantIntimidate;
+
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is Tyrant;
+ }
+
+ public override bool CanUse()
+ {
+ return base.CanUse() && ((Tyrant)PlayerControl.LocalPlayer.Data.Role).Kills >= 1;
+ }
+
+ protected override void OnClick()
+ {
+ var player = PlayerControl.LocalPlayer;
+ var position = player.GetTruePosition();
+ Tyrant.RpcSpawnFearPulse(player, position.x, position.y);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Buttons/Shade/DeployShadow.cs b/NewMod/Buttons/Shade/DeployShadow.cs
deleted file mode 100644
index b14db52..0000000
--- a/NewMod/Buttons/Shade/DeployShadow.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Keybinds;
-using MiraAPI.Utilities.Assets;
-using UnityEngine;
-using NewMod.Components;
-using SH = NewMod.Roles.NeutralRoles.Shade;
-using NewMod.Options.Roles.ShadeOptions;
-
-namespace NewMod.Buttons.Shade
-{
- ///
- /// Custom action button for the Shade role.
- /// Deploys a Shadow Zone where the Shade becomes invisible and gains kill power.
- ///
- public class ShadeButton : CustomActionButton
- {
- ///
- /// Display name of the button.
- ///
- public override string Name => "Deploy Shadow";
-
- ///
- /// Cooldown duration between zone deployments.
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.Cooldown;
-
- ///
- /// Maximum number of uses.
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.MaxUses;
-
- ///
- /// Button HUD placement.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomLeft;
-
- ///
- /// Default keybind.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// Button icon.
- ///
- public override LoadableAsset Sprite => NewModAsset.DeployZone;
-
- ///
- /// Button enabled only for the Shade role.
- ///
- public override bool Enabled(RoleBehaviour role) => role is SH;
-
- ///
- /// Deploys a shadow zone at the Shade's position.
- ///
- protected override void OnClick()
- {
- var player = PlayerControl.LocalPlayer;
-
- Vector2 pos = player.GetTruePosition();
-
- float radius = OptionGroupSingleton.Instance.Radius;
- float dur = OptionGroupSingleton.Instance.Duration;
-
- ShadowZone.RpcDeployZone(player, pos, radius, dur);
- }
- }
-}
diff --git a/NewMod/Buttons/SpecialAgent/AssignButton.cs b/NewMod/Buttons/SpecialAgent/AssignButton.cs
deleted file mode 100644
index d080ef3..0000000
--- a/NewMod/Buttons/SpecialAgent/AssignButton.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-using System.Collections;
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities.Assets;
-using NewMod.Options.Roles.SpecialAgentOptions;
-using SA = NewMod.Roles.NeutralRoles.SpecialAgent;
-using UnityEngine;
-using NewMod.Utilities;
-using Reactor.Utilities;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.SpecialAgent
-{
- ///
- /// Defines a custom action button for the role.
- ///
- public class AssignButton : CustomActionButton
- {
- ///
- /// The name displayed on this button.
- ///
- public override string Name => "Assign Mission";
-
- ///
- /// The cooldown time before this button can be used again, based on .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.AssignCooldown;
-
- ///
- /// The maximum number of times the assign action can be performed, based on .
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.AssignMaxUses;
-
- ///
- /// The on-screen location of this button.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomLeft;
-
- ///
- /// Default keybind for Special Agent's Assign ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// The duration of any effect triggered by this button; here, it's zero.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// The sprite icon representing this button.
- ///
- public override LoadableAsset Sprite => NewModAsset.SpecialAgentButton;
-
- ///
- /// Specifies whether this button is enabled for the specified role.
- ///
- /// The current role to check.
- /// True if the role is , otherwise false.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is SA;
- }
-
- ///
- /// Checks whether the player can use this button, ensuring no one has been assigned a mission yet.
- ///
- /// True if base conditions are met and no player is assigned, otherwise false.
- public override bool CanUse()
- {
- return base.CanUse() && SA.AssignedPlayer == null;
- }
-
- ///
- /// Invoked when the button is clicked. Opens a custom player menu to pick a mission target.
- ///
- protected override void OnClick()
- {
- NewMod.Instance.Log.LogError("Special Agent assign menu open...");
- CustomPlayerMenu menu = CustomPlayerMenu.Create();
-
- SetTimerPaused(true);
-
- menu.Begin(
- player => !player.Data.IsDead &&
- !player.Data.Disconnected &&
- player.PlayerId != PlayerControl.LocalPlayer.PlayerId,
- player =>
- {
- SA.AssignedPlayer = player;
- Utils.RpcAssignMission(PlayerControl.LocalPlayer, SA.AssignedPlayer);
- NewMod.Instance.Log.LogError($"Assigning target: {SA.AssignedPlayer.Data.PlayerName}");
-
- if (OptionGroupSingleton.Instance.TargetCameraTracking)
- {
- var cam = Camera.main.GetComponent();
- cam?.SetTarget(player);
- Coroutines.Start(CoResetCamera(cam, OptionGroupSingleton.Instance.CameraTrackingDuration));
- }
- menu.Close();
- SetTimerPaused(false);
- }
- );
- }
-
- ///
- /// Resets the camera target to the local player after a specified duration, optionally shaking the camera at the end.
- ///
- /// The to reset.
- /// How long to track the target before resetting.
- /// An for coroutine control.
- public static IEnumerator CoResetCamera(FollowerCamera cam, float duration)
- {
- float timeElapsed = 0f;
- Vector3 originalPosition = cam.transform.position;
- float shakeThreshold = 1.5f;
- bool shouldShake = OptionGroupSingleton.Instance.ShouldShakeCamera;
-
- while (timeElapsed < duration)
- {
- timeElapsed += Time.deltaTime;
- if (shouldShake && (duration - timeElapsed) <= shakeThreshold)
- {
- float shakeMagnitude = 0.3f;
- Vector3 shakeOffset = Random.insideUnitSphere * shakeMagnitude;
- cam.transform.localPosition = originalPosition + shakeOffset;
- }
- else
- {
- cam.transform.localPosition = originalPosition;
- }
- yield return null;
- }
- cam.transform.localPosition = originalPosition;
- cam?.SetTarget(PlayerControl.LocalPlayer);
- }
- }
-}
diff --git a/NewMod/Buttons/Visionary/CaptureButton.cs b/NewMod/Buttons/Visionary/CaptureButton.cs
deleted file mode 100644
index 02967ee..0000000
--- a/NewMod/Buttons/Visionary/CaptureButton.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-using System.IO;
-using MiraAPI.Utilities.Assets;
-using MiraAPI.Hud;
-using MiraAPI.GameOptions;
-using NewMod.Options.Roles.VisionaryOptions;
-using NewMod.Roles.CrewmateRoles;
-using UnityEngine;
-using Reactor.Utilities;
-using NewMod.Utilities;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.Visionary
-{
- ///
- /// Defines a custom action button for the role.
- ///
- public class CaptureButton : CustomActionButton
- {
- ///
- /// The name shown on this button.
- ///
- public override string Name => "Capture";
-
- ///
- /// The cooldown time before this button can be used again, based on .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.ScreenshotCooldown;
-
- ///
- /// The duration of any effect triggered by this button; here, none.
- ///
- public override float EffectDuration => 0;
-
- ///
- /// The maximum number of screenshots the user can capture, based on .
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.MaxScreenshots;
-
- ///
- /// The icon or sprite associated with this button, set to a camera sprite.
- ///
- public override LoadableAsset Sprite => NewModAsset.Camera;
-
- ///
- /// The location on-screen where this button appears.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomLeft;
- ///
- /// Default keybind for Visionary's Capture ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// Handles the button click, capturing a screenshot and saving it to a unique path.
- ///
- protected override void OnClick()
- {
- var timestamp = System.DateTime.UtcNow.ToString("yyyy-MM-dd_HH-mm-ss");
- string path = Path.Combine(VisionaryUtilities.ScreenshotDirectory, $"screenshot_{timestamp}.png");
- Coroutines.Start(Utils.CaptureScreenshot(path));
- }
-
- ///
- /// Determines whether this button is enabled for the given role.
- ///
- /// The current player's role.
- /// True if the role is , otherwise false.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is TheVisionary;
- }
- }
-}
diff --git a/NewMod/Buttons/Visionary/ShowScreenshotButton.cs b/NewMod/Buttons/Visionary/ShowScreenshotButton.cs
deleted file mode 100644
index c8fa7c4..0000000
--- a/NewMod/Buttons/Visionary/ShowScreenshotButton.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-using MiraAPI.Utilities.Assets;
-using MiraAPI.Hud;
-using MiraAPI.GameOptions;
-using NewMod.Options.Roles.VisionaryOptions;
-using NewMod.Roles.CrewmateRoles;
-using UnityEngine;
-using NewMod.Utilities;
-using Reactor.Utilities;
-using System.Linq;
-using System.IO;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.Visionary
-{
- ///
- /// Defines a custom action button for the role.
- ///
- public class ShowScreenshotButton : CustomActionButton
- {
- ///
- /// The name displayed on this button.
- ///
- public override string Name => "";
-
- ///
- /// The cooldown time for this button, based on .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.ScreenshotCooldown;
-
- ///
- /// The duration of any effect triggered by this button; in this case, none.
- ///
- public override float EffectDuration => 0;
-
- ///
- /// The maximum number of times screenshots can be shown, based on .
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.MaxScreenshots;
-
- ///
- /// The sprite asset for this button.
- ///
- public override LoadableAsset Sprite => NewModAsset.ShowScreenshotButton;
-
- ///
- /// The on-screen location where the button will appear.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomRight;
-
- ///
- /// Default keybind for Visionary's Show ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
-
- ///
- /// Checks if the button can be used, ensuring there's at least one captured screenshot.
- ///
- /// True if base conditions are met and there is a screenshot, otherwise false.
- public override bool CanUse()
- {
- return base.CanUse() && Directory.GetFiles(VisionaryUtilities.ScreenshotDirectory, "screenshot_*.png").Any();
- }
-
- ///
- /// Invoked when the button is clicked, starting the coroutine to display screenshots.
- ///
- protected override void OnClick()
- {
- Coroutines.Start(VisionaryUtilities.ShowScreenshots(OptionGroupSingleton.Instance.MaxDisplayDuration));
- }
-
- ///
- /// Determines whether this button is enabled for the specified role.
- ///
- /// The player's current role.
- /// True if the role is , otherwise false.
- public override bool Enabled(RoleBehaviour role)
- {
- return role is TheVisionary;
- }
- }
-}
diff --git a/NewMod/Buttons/WraithCaller/CallWraith.cs b/NewMod/Buttons/WraithCaller/CallWraith.cs
deleted file mode 100644
index 7a1031f..0000000
--- a/NewMod/Buttons/WraithCaller/CallWraith.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-using MiraAPI.GameOptions;
-using MiraAPI.Hud;
-using MiraAPI.Utilities.Assets;
-using NewMod.Options.Roles.WraithCallerOptions;
-using Wraith = NewMod.Roles.NeutralRoles.WraithCaller;
-using UnityEngine;
-using NewMod.Utilities;
-using System.Collections.Generic;
-using System.Linq;
-using MiraAPI.Keybinds;
-
-namespace NewMod.Buttons.WraithCaller
-{
- ///
- /// Defines the Call Wraith ability button for the Wraith Caller role.
- ///
- public class CallWraithButton : CustomActionButton
- {
- ///
- /// The name displayed on the button.
- ///
- public override string Name => "Call Wraith";
-
- ///
- /// The cooldown time for the Call Wraith ability, as set in .
- ///
- public override float Cooldown => OptionGroupSingleton.Instance.CallWraithCooldown;
-
- ///
- /// The maximum uses for the Call Wraith ability, as set in .
- ///
- public override int MaxUses => (int)OptionGroupSingleton.Instance.CallWraithMaxUses;
-
- ///
- /// Location on the screen for the Call Wraith button.
- ///
- public override ButtonLocation Location => ButtonLocation.BottomRight;
-
- ///
- /// Default keybind for the Call Wraith ability.
- ///
- public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
-
- ///
- /// The duration of any effect triggered by this ability.
- ///
- public override float EffectDuration => 0f;
-
- ///
- /// The icon for the Call Wraith button.
- ///
- public override LoadableAsset Sprite => NewModAsset.CallWraith;
-
- ///
- /// Enables the button for the Wraith Caller role only.
- ///
- /// Current player's role
- /// True if role is Wraith Caller, otherwise false
- public override bool Enabled(RoleBehaviour role)
- {
- return role is Wraith;
- }
-
- ///
- /// Triggered when the Call Wraith button is clicked.
- ///
- protected override void OnClick()
- {
- //TODO: Replace this with the custom minigame once it’s fixed
- CustomPlayerMenu menu = CustomPlayerMenu.Create();
-
- SetTimerPaused(true);
-
- var allowedPlayers = new HashSet();
-
- foreach (var info in GameData.Instance.AllPlayers)
- {
- if (info.PlayerId == PlayerControl.LocalPlayer.PlayerId) continue;
- if (info.IsDead || info.Disconnected) continue;
-
- allowedPlayers.Add(info.PlayerId);
- }
-
- menu.Begin(player => allowedPlayers.Contains(player.PlayerId) && !player.notRealPlayer,
- player =>
- {
- menu.Close();
- WraithCallerUtilities.RpcSummonNPC(PlayerControl.LocalPlayer, player);
- SetTimerPaused(false);
- });
-
- foreach (var panel in menu.potentialVictims)
- {
- var icon = panel.GetComponentsInChildren(true).FirstOrDefault(sr => sr.name == "ShapeshifterIcon");
- icon.sprite = NewModAsset.WraithIcon.LoadAsset();
- }
- }
- }
-}
diff --git a/NewMod/Components/AreaBubble.cs b/NewMod/Components/AreaBubble.cs
new file mode 100644
index 0000000..eaa9c3b
--- /dev/null
+++ b/NewMod/Components/AreaBubble.cs
@@ -0,0 +1,142 @@
+using NewMod.Utilities;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components;
+
+[RegisterInIl2Cpp]
+public class AreaBubble(nint ptr) : MonoBehaviour(ptr)
+{
+ public Mesh mesh;
+ public float expiresAt;
+ public float breakStartedAt;
+ public float breakDuration = 0.5f;
+ public bool breaking;
+ public Vector3[] vertices;
+ public Color[] colors;
+ public int[] triangles;
+ public Vector3[] fragments;
+ public Color[] fragmentColors;
+
+ public void Init(float radius, Color color, float opacity = 0.04f)
+ {
+ vertices = new Vector3[25 * 49];
+ colors = new Color[vertices.Length];
+ triangles = new int[24 * 48 * 6];
+ var light = new Vector3(-0.4f, 0.65f, -0.65f).normalized;
+ for (var ring = 0; ring <= 24; ring++)
+ {
+ var angle = ring * Mathf.PI / 48f;
+ var distance = Mathf.Sin(angle);
+ var depth = Mathf.Cos(angle);
+ for (var segment = 0; segment <= 48; segment++)
+ {
+ var azimuth = segment * Mathf.PI / 24f;
+ var normal = new Vector3(distance * Mathf.Cos(azimuth), distance * Mathf.Sin(azimuth), -depth);
+ var index = ring * 49 + segment;
+ vertices[index] = new Vector3(normal.x, normal.y, normal.z * 0.45f) * radius;
+ var highlight = Mathf.Pow(Mathf.Max(0f, Vector3.Dot(normal, light)), 32f);
+ colors[index] = Color.Lerp(new Color(color.r, color.g, color.b, opacity + Mathf.Pow(distance, 10f) * 0.5f), new Color(0.85f, 0.97f, 1f, 0.6f), highlight);
+ if (ring == 24 || segment == 48)
+ continue;
+ var triangle = (ring * 48 + segment) * 6;
+ triangles[triangle] = index;
+ triangles[triangle + 1] = index + 49;
+ triangles[triangle + 2] = index + 1;
+ triangles[triangle + 3] = index + 1;
+ triangles[triangle + 4] = index + 49;
+ triangles[triangle + 5] = index + 50;
+ }
+ }
+
+ mesh = new Mesh();
+ mesh.vertices = vertices;
+ mesh.colors = colors;
+ mesh.triangles = triangles;
+ mesh.RecalculateBounds();
+ gameObject.AddComponent().mesh = mesh;
+ var renderer = gameObject.AddComponent();
+ renderer.sharedMaterial = Utils.GetCircleMat();
+ renderer.sortingOrder = 2;
+ }
+
+ public void Update()
+ {
+ if (!ShipStatus.Instance || (breaking && (MeetingHud.Instance || ExileController.Instance)))
+ {
+ Destroy(gameObject);
+ return;
+ }
+
+ if (!breaking)
+ {
+ if (Time.time >= expiresAt)
+ Break();
+ return;
+ }
+
+ var progress = Mathf.Clamp01((Time.time - breakStartedAt) / breakDuration);
+ if (progress >= 1f)
+ {
+ Destroy(gameObject);
+ return;
+ }
+
+ var fill = Mathf.Clamp01(progress / 0.25f);
+ var spread = Mathf.Clamp01((progress - 0.25f) / 0.75f);
+ for (var triangle = 0; triangle < triangles.Length; triangle += 3)
+ {
+ var center = (vertices[triangles[triangle]] + vertices[triangles[triangle + 1]] + vertices[triangles[triangle + 2]]) / 3f;
+ var offset = center * (spread * 0.35f);
+ for (var corner = 0; corner < 3; corner++)
+ {
+ var index = triangle + corner;
+ var vertex = triangles[index];
+ fragments[index] = center + (vertices[vertex] - center) * (1f - spread * 0.8f) + offset;
+ var color = Color.Lerp(colors[vertex], Color.white, fill * 0.3f);
+ color.a = Mathf.Lerp(colors[vertex].a, 0.65f, fill) * (1f - spread);
+ fragmentColors[index] = color;
+ }
+ }
+
+ mesh.vertices = fragments;
+ mesh.colors = fragmentColors;
+ mesh.RecalculateBounds();
+ }
+
+ public void Break()
+ {
+ if (breaking)
+ return;
+
+ if (!ShipStatus.Instance || MeetingHud.Instance || ExileController.Instance)
+ {
+ Destroy(gameObject);
+ return;
+ }
+
+ breaking = true;
+ breakStartedAt = Time.time;
+ fragments = new Vector3[triangles.Length];
+ fragmentColors = new Color[triangles.Length];
+ var indices = new int[triangles.Length];
+ for (var i = 0; i < indices.Length; i++)
+ {
+ indices[i] = i;
+ fragments[i] = vertices[triangles[i]];
+ fragmentColors[i] = colors[triangles[i]];
+ }
+
+ mesh.MarkDynamic();
+ mesh.Clear();
+ mesh.vertices = fragments;
+ mesh.colors = fragmentColors;
+ mesh.triangles = indices;
+ mesh.RecalculateBounds();
+ }
+
+ public void OnDestroy()
+ {
+ Destroy(mesh);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Components/Birthday/BirthdayToast.cs b/NewMod/Components/Birthday/BirthdayToast.cs
deleted file mode 100644
index 5aa66c6..0000000
--- a/NewMod/Components/Birthday/BirthdayToast.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using System;
-using System.Collections;
-using Reactor.Utilities;
-using TMPro;
-using UnityEngine;
-using NewMod;
-using NewMod.Utilities;
-using Reactor.Utilities.Attributes;
-using Il2CppInterop.Runtime.Attributes;
-
-[RegisterInIl2Cpp]
-public class BirthdayToast(IntPtr ptr) : MonoBehaviour(ptr)
-{
- public SpriteRenderer toastRend;
- public TextMeshPro TimerText;
- public bool isExpanded = false;
- public void Awake()
- {
- toastRend = transform.Find("Background").GetComponent();
- TimerText = transform.Find("Timer").GetComponent();
- }
- public static BirthdayToast CreateBirthdayToast()
- {
- var gameObject = Instantiate(NewModAsset.Toast.LoadAsset(), HudManager.Instance.transform);
- var toast = gameObject.AddComponent();
- return toast;
- }
- [HideFromIl2Cpp]
- public void SetText(string msg)
- {
- if (TimerText) TimerText.text = msg;
- }
-
- [HideFromIl2Cpp]
- public void StartCountdown(TimeSpan duration)
- {
- Coroutines.Start(CoCountdown(duration));
- }
- [HideFromIl2Cpp]
- public IEnumerator CoCountdown(TimeSpan span)
- {
- var end = DateTime.UtcNow + span;
- while (true)
- {
- var left = end - DateTime.UtcNow;
- if (left.TotalSeconds <= 0) break;
-
- if (TimerText)
- TimerText.text = Utils.FormatSpan(left);
-
- yield return new WaitForSecondsRealtime(0.2f);
- }
- if (TimerText) TimerText.text = "00:00:00:00";
-
- DisconnectAllPlayers();
- }
- [HideFromIl2Cpp]
- public static void DisconnectAllPlayers()
- {
- var client = AmongUsClient.Instance;
- if (client.GameState == InnerNet.InnerNetClient.GameStates.Started) return;
-
- client.LastCustomDisconnect =
- "The Birthday Update is now live! Please restart to see the new lobby and menu style.";
- client.HandleDisconnect(DisconnectReasons.Custom);
- }
-}
\ No newline at end of file
diff --git a/NewMod/Components/CrimsonVortexEscapeHud.cs b/NewMod/Components/CrimsonVortexEscapeHud.cs
new file mode 100644
index 0000000..71739d9
--- /dev/null
+++ b/NewMod/Components/CrimsonVortexEscapeHud.cs
@@ -0,0 +1,273 @@
+using Reactor.Utilities.Attributes;
+using TMPro;
+using UnityEngine;
+
+namespace NewMod.Components;
+
+[RegisterInIl2Cpp]
+public class CrimsonVortexEscapeHud(nint ptr) : MonoBehaviour(ptr)
+{
+ private SpriteRenderer _fill;
+ private SpriteRenderer _glow;
+ private TextMeshPro _label;
+ private int _lastPercentage = -1;
+ private TextMeshPro _percentage;
+
+ private float _pulse;
+
+ private GameObject _root;
+ private Sprite _sprite;
+ private State _state;
+ private float _successEndsAt;
+ private Texture2D _texture;
+ public static CrimsonVortexEscapeHud Instance { get; private set; }
+
+ public void Awake()
+ {
+ Instance = this;
+
+ _texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
+
+ _texture.filterMode = FilterMode.Bilinear;
+ _texture.wrapMode = TextureWrapMode.Clamp;
+ _texture.hideFlags = HideFlags.HideAndDontSave;
+
+ _texture.SetPixel(0, 0, Color.white);
+ _texture.SetPixel(1, 0, Color.white);
+ _texture.SetPixel(0, 1, Color.white);
+ _texture.SetPixel(1, 1, Color.white);
+ _texture.Apply();
+
+ _sprite = Sprite.Create(_texture, new Rect(0f, 0f, 2f, 2f), new Vector2(0.5f, 0.5f), 2f);
+
+ _sprite.hideFlags = HideFlags.HideAndDontSave;
+
+ _root = new GameObject("CrimsonVortexEscapeHud");
+ _root.layer = LayerMask.NameToLayer("UI");
+ _root.transform.SetParent(transform, false);
+
+ var aspectPosition = _root.AddComponent();
+
+ aspectPosition.Alignment = AspectPosition.EdgeAlignments.Bottom;
+
+ aspectPosition.DistanceFromEdge = new Vector3(0f, 1.22f, -40f);
+
+ aspectPosition.AdjustPosition();
+
+ var glowObject = new GameObject("Glow");
+ glowObject.layer = LayerMask.NameToLayer("UI");
+ glowObject.transform.SetParent(_root.transform, false);
+ glowObject.transform.localScale = new Vector3(4.55f, 0.58f, 1f);
+
+ _glow = glowObject.AddComponent();
+ _glow.sprite = _sprite;
+ _glow.color = new Color(0.95f, 0.02f, 0.04f, 0.18f);
+ _glow.sortingOrder = 90;
+
+ var borderObject = new GameObject("Border");
+ borderObject.layer = LayerMask.NameToLayer("UI");
+ borderObject.transform.SetParent(_root.transform, false);
+ borderObject.transform.localPosition = new Vector3(0f, 0f, -0.01f);
+ borderObject.transform.localScale = new Vector3(4.4f, 0.47f, 1f);
+
+ var border = borderObject.AddComponent();
+
+ border.sprite = _sprite;
+ border.color = new Color(0.9f, 0.035f, 0.05f, 1f);
+ border.sortingOrder = 91;
+
+ var backgroundObject = new GameObject("Background");
+
+ backgroundObject.layer = LayerMask.NameToLayer("UI");
+ backgroundObject.transform.SetParent(_root.transform, false);
+
+ backgroundObject.transform.localPosition = new Vector3(0f, 0f, -0.02f);
+
+ backgroundObject.transform.localScale = new Vector3(4.26f, 0.33f, 1f);
+
+ var background = backgroundObject.AddComponent();
+
+ background.sprite = _sprite;
+ background.color = new Color(0.24f, 0.008f, 0.015f, 0.98f);
+ background.sortingOrder = 92;
+
+ var fillObject = new GameObject("Fill");
+ fillObject.layer = LayerMask.NameToLayer("UI");
+ fillObject.transform.SetParent(_root.transform, false);
+ fillObject.transform.localPosition = new Vector3(-2.1f, 0f, -0.03f);
+ fillObject.transform.localScale = new Vector3(0f, 0.23f, 1f);
+
+ _fill = fillObject.AddComponent();
+ _fill.sprite = _sprite;
+ _fill.color = new Color(0.95f, 0.025f, 0.045f, 1f);
+ _fill.sortingOrder = 93;
+
+ var labelObject = new GameObject("Label");
+ labelObject.layer = LayerMask.NameToLayer("UI");
+ labelObject.transform.SetParent(_root.transform, false);
+ labelObject.transform.localPosition = new Vector3(0f, 0.49f, -0.04f);
+
+ _label = labelObject.AddComponent();
+ _label.font = HudManager.Instance.TaskPanel.taskText.font;
+ _label.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial;
+ _label.fontSize = 1.65f;
+ _label.fontStyle = FontStyles.Bold;
+ _label.alignment = TextAlignmentOptions.Center;
+ _label.enableWordWrapping = false;
+ _label.richText = true;
+ _label.rectTransform.sizeDelta = new Vector2(7f, 0.8f);
+ _label.renderer.sortingOrder = 94;
+
+ var percentageObject = new GameObject("Percentage");
+
+ percentageObject.layer = LayerMask.NameToLayer("UI");
+ percentageObject.transform.SetParent(_root.transform, false);
+
+ percentageObject.transform.localPosition = new Vector3(0f, -0.005f, -0.05f);
+
+ _percentage = percentageObject.AddComponent();
+
+ _percentage.font = HudManager.Instance.TaskPanel.taskText.font;
+
+ _percentage.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial;
+
+ _percentage.fontSize = 1.2f;
+ _percentage.fontStyle = FontStyles.Bold;
+ _percentage.alignment = TextAlignmentOptions.Center;
+ _percentage.enableWordWrapping = false;
+ _percentage.rectTransform.sizeDelta = new Vector2(7f, 0.8f);
+ _percentage.renderer.sortingOrder = 95;
+
+ ForceHide();
+ }
+
+ public void LateUpdate()
+ {
+ if (_state == State.Success && Time.time >= _successEndsAt)
+ {
+ ForceHide();
+ return;
+ }
+
+ if (!_root.activeSelf)
+ return;
+
+ _pulse = Mathf.MoveTowards(_pulse, 0f, Time.deltaTime * 5.5f);
+
+ var scale = 1f + _pulse * 0.065f;
+
+ _root.transform.localScale = new Vector3(scale, scale, 1f);
+
+ _glow.color = new Color(0.95f, 0.02f, 0.04f, 0.14f + _pulse * 0.28f);
+ }
+
+ public void OnDestroy()
+ {
+ if (Instance == this)
+ Instance = null;
+
+ Destroy(_sprite);
+ Destroy(_texture);
+ }
+
+ public void SetProgress(float progress)
+ {
+ if (_state != State.Mashing)
+ {
+ _state = State.Mashing;
+
+ _label.text = Application.platform == RuntimePlatform.Android ? "TAP RAPIDLY TO BREAK FREE!" : "MASH [SPACE] TO BREAK FREE!";
+
+ _label.color = new Color(0.95f, 0.025f, 0.045f, 1f);
+ }
+
+ _root.SetActive(true);
+
+ progress = Mathf.Clamp01(progress);
+
+ var width = 4.2f * progress;
+
+ _fill.transform.localScale = new Vector3(width, 0.23f, 1f);
+
+ _fill.transform.localPosition = new Vector3(-2.1f + width * 0.5f, 0f, -0.03f);
+
+ _fill.color = Color.Lerp(new Color(0.58f, 0.01f, 0.025f, 1f), new Color(0.95f, 0.025f, 0.045f, 1f), progress);
+
+ var percentage = Mathf.RoundToInt(progress * 100f);
+
+ if (percentage != _lastPercentage)
+ {
+ _lastPercentage = percentage;
+ _percentage.text = percentage + "%";
+ }
+ }
+
+ public void Pulse()
+ {
+ _pulse = 1f;
+ }
+
+ public void ShowBreakingFree()
+ {
+ _state = State.Escaping;
+ _root.SetActive(true);
+
+ _label.text = "BREAKING FREE!";
+ _label.color = new Color(1f, 0.72f, 0.15f, 1f);
+
+ _percentage.text = string.Empty;
+
+ _fill.transform.localScale = new Vector3(4.2f, 0.23f, 1f);
+
+ _fill.transform.localPosition = new Vector3(0f, 0f, -0.03f);
+
+ _fill.color = new Color(1f, 0.72f, 0.15f, 1f);
+
+ _pulse = 1f;
+ }
+
+ public void ShowSuccess()
+ {
+ _state = State.Success;
+ _successEndsAt = Time.time + 0.8f;
+ _root.SetActive(true);
+
+ _label.text = "ESCAPED!";
+ _label.color = new Color(0.22f, 1f, 0.64f, 1f);
+
+ _percentage.text = string.Empty;
+
+ _fill.transform.localScale = new Vector3(4.2f, 0.23f, 1f);
+
+ _fill.transform.localPosition = new Vector3(0f, 0f, -0.03f);
+
+ _fill.color = new Color(0.22f, 1f, 0.64f, 1f);
+
+ _pulse = 1f;
+ }
+
+ public void Hide()
+ {
+ if (_state == State.Success && Time.time < _successEndsAt) return;
+
+ ForceHide();
+ }
+
+ public void ForceHide()
+ {
+ _state = State.Hidden;
+ _pulse = 0f;
+ _successEndsAt = 0f;
+ _lastPercentage = -1;
+ _root.transform.localScale = Vector3.one;
+ _root.SetActive(false);
+ }
+
+ private enum State
+ {
+ Hidden,
+ Mashing,
+ Escaping,
+ Success
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Components/CrimsonVortexPhysics.cs b/NewMod/Components/CrimsonVortexPhysics.cs
new file mode 100644
index 0000000..e7f7c23
--- /dev/null
+++ b/NewMod/Components/CrimsonVortexPhysics.cs
@@ -0,0 +1,349 @@
+using System.Linq;
+using MiraAPI.GameOptions;
+using MiraAPI.Modifiers;
+using MiraAPI.Networking;
+using NewMod.Achievements;
+using NewMod.GeneralEvents.Season1;
+using NewMod.Modifiers.S1;
+using NewMod.Options;
+using NewMod.Roles.NeutralRoles.S1;
+using NewMod.Seasons;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components;
+
+[RegisterInIl2Cpp]
+public class CrismonVortexPhysics(nint ptr) : MonoBehaviour(ptr)
+{
+ private bool _collisionDisabled;
+ private bool _deathRequested;
+
+ private float _escapeProgress;
+ private bool _escapedThisEntry;
+ private bool _escaping;
+ private float _lastPressTime;
+ private Vector2 _lastSafePosition;
+ private bool _lastSafePositionReady;
+ private PlayerPhysics _physics;
+ private PlayerControl _player;
+ private bool _writingVelocity;
+
+ public void Awake()
+ {
+ _physics = GetComponent();
+ _player = GetComponent();
+ _lastPressTime = float.NegativeInfinity;
+ }
+
+ public void Update()
+ {
+ if (!_physics.AmOwner)
+ return;
+
+ if (!CrismonVortexGE.Active || !CrismonVortexGE.PositionReady || MeetingHud.Instance || ExileController.Instance || _player.Data.IsDead || _player.HasModifier())
+ {
+ if (!CrismonVortexGE.Active && _physics.AmOwner && !_player.Data.IsDead && _player.CanMove && !_player.inVent)
+ {
+ _lastSafePosition = _player.GetTruePosition();
+ _lastSafePositionReady = true;
+ }
+
+ _escapeProgress = 0f;
+ _lastPressTime = float.NegativeInfinity;
+
+ if (CrimsonVortexEscapeHud.Instance)
+ CrimsonVortexEscapeHud.Instance.ForceHide();
+
+ return;
+ }
+
+ if (_escaping)
+ return;
+
+ var options = OptionGroupSingleton.Instance;
+ var distance = Vector2.Distance(_player.GetTruePosition(), CrismonVortexGE.VortexPosition);
+
+ var killRadius = Mathf.Clamp(options.CrimsonRadius.Value * 0.12f, 0.45f, 0.9f);
+ var escapeRadius = options.CrimsonRadius.Value * 0.75f;
+ var progressPerPress = options.CrimsonEscapeDifficulty.Value switch
+ {
+ GEOptions.EscapeDifficulty.Easy => 0.12f,
+ GEOptions.EscapeDifficulty.Standard => 0.09f,
+ GEOptions.EscapeDifficulty.Hard => 0.07f,
+ _ => 0.09f
+ };
+ var decayDelay = options.CrimsonEscapeDifficulty.Value switch
+ {
+ GEOptions.EscapeDifficulty.Easy => 0.25f,
+ GEOptions.EscapeDifficulty.Standard => 0.15f,
+ GEOptions.EscapeDifficulty.Hard => 0.1f,
+ _ => 0.15f
+ };
+ var decayRate = options.CrimsonEscapeDifficulty.Value switch
+ {
+ GEOptions.EscapeDifficulty.Easy => 0.35f,
+ GEOptions.EscapeDifficulty.Standard => 0.55f,
+ GEOptions.EscapeDifficulty.Hard => 0.75f,
+ _ => 0.55f
+ };
+
+ if (_escapedThisEntry || !_player.CanMove || _player.inVent || distance > escapeRadius || distance <= killRadius)
+ {
+ _escapeProgress = 0f;
+ _lastPressTime = float.NegativeInfinity;
+
+ if (CrimsonVortexEscapeHud.Instance)
+ CrimsonVortexEscapeHud.Instance.Hide();
+
+ return;
+ }
+
+ var inputBlocked = HudManager.Instance.Chat.IsOpenOrOpening || Minigame.Instance || (MapBehaviour.Instance && MapBehaviour.Instance.gameObject.activeSelf) || (MatchInfoGuide.Instance && MatchInfoGuide.Instance.IsActive);
+
+ var pressed = Input.GetKeyDown(KeyCode.Space);
+
+ if (Application.platform == RuntimePlatform.Android)
+ for (var touchIndex = 0; touchIndex < Input.touchCount; touchIndex++)
+ {
+ var touch = Input.GetTouch(touchIndex);
+
+ if (touch.phase != TouchPhase.Began)
+ continue;
+
+ pressed = true;
+ break;
+ }
+
+ if (!inputBlocked && pressed)
+ {
+ _escapeProgress = Mathf.Clamp01(_escapeProgress + progressPerPress);
+
+ _lastPressTime = Time.time;
+
+ if (CrimsonVortexEscapeHud.Instance)
+ CrimsonVortexEscapeHud.Instance.Pulse();
+ }
+ else if (Time.time - _lastPressTime > decayDelay)
+ {
+ _escapeProgress = Mathf.Max(0f, _escapeProgress - decayRate * Time.deltaTime);
+ }
+
+ if (CrimsonVortexEscapeHud.Instance) CrimsonVortexEscapeHud.Instance.SetProgress(_escapeProgress);
+
+ if (_escapeProgress >= 1f)
+ CrismonVortexGE.RpcEscapeVortex(_player);
+ }
+
+ public void FixedUpdate()
+ {
+ if (!CrismonVortexGE.Active || !CrismonVortexGE.PositionReady || MeetingHud.Instance || ExileController.Instance || _player.Data.IsDead || _player.HasModifier())
+ {
+ _escaping = false;
+ _escapedThisEntry = false;
+ _deathRequested = false;
+ _escapeProgress = 0f;
+ _lastPressTime = float.NegativeInfinity;
+
+ if (_collisionDisabled)
+ {
+ _player.Collider.enabled = true;
+ _collisionDisabled = false;
+ }
+
+ return;
+ }
+
+ var options = OptionGroupSingleton.Instance;
+ var distance = Vector2.Distance(_player.GetTruePosition(), CrismonVortexGE.VortexPosition);
+ var killRadius = Mathf.Clamp(options.CrimsonRadius.Value * 0.12f, 0.45f, 0.9f);
+
+ if (distance > killRadius)
+ _deathRequested = false;
+
+ if (distance >= options.CrimsonRadius.Value)
+ {
+ if (_physics.AmOwner && _player.CanMove && !_player.inVent)
+ {
+ _lastSafePosition = _player.GetTruePosition();
+ _lastSafePositionReady = true;
+ }
+
+ _escaping = false;
+ _escapedThisEntry = false;
+
+ if (_collisionDisabled)
+ {
+ _player.Collider.enabled = true;
+ _collisionDisabled = false;
+ }
+ }
+
+ if (AmongUsClient.Instance.AmHost && !_deathRequested && !_escaping && !_player.inVent && distance <= killRadius)
+ {
+ _deathRequested = true;
+ Collector.MarkEnvironmental(_player.PlayerId);
+
+ _player.RpcAdvancedCustomMurder(_player, MeetingCheck.OutsideMeeting, true, true, resetKillTimer: false, teleportMurderer: false, showKillAnim: false, playKillSound: false);
+
+ return;
+ }
+
+ if (_player.isDummy && AmongUsClient.Instance.AmHost)
+ {
+ ApplyVortex();
+ }
+ else if (_physics.AmOwner && !_escaping && !_player.CanMove && _collisionDisabled)
+ {
+ _player.Collider.enabled = true;
+ _collisionDisabled = false;
+ }
+ }
+
+ public void OnDestroy()
+ {
+ if (_collisionDisabled)
+ _player.Collider.enabled = true;
+
+ if (_physics.AmOwner && CrimsonVortexEscapeHud.Instance) CrimsonVortexEscapeHud.Instance.ForceHide();
+ }
+
+ public void ApplyVortex()
+ {
+ if (_writingVelocity || !CrismonVortexGE.Active || !CrismonVortexGE.PositionReady || MeetingHud.Instance || ExileController.Instance || _player.Data.IsDead || _player.HasModifier()) return;
+
+ if (_escaping)
+ return;
+
+ var options = OptionGroupSingleton.Instance;
+ var fromCenter = _player.GetTruePosition() - CrismonVortexGE.VortexPosition;
+
+ var distance = fromCenter.magnitude;
+
+ if (_player.inVent)
+ {
+ if (_collisionDisabled)
+ {
+ _player.Collider.enabled = true;
+ _collisionDisabled = false;
+ }
+
+ if (_physics.AmOwner && Vent.currentVent)
+ _physics.RpcBootFromVent(Vent.currentVent.Id);
+
+ return;
+ }
+
+ if (distance >= options.CrimsonRadius.Value)
+ {
+ if (_collisionDisabled)
+ {
+ _player.Collider.enabled = true;
+ _collisionDisabled = false;
+ }
+
+ return;
+ }
+
+ if (!_player.CanMove)
+ {
+ if (_collisionDisabled)
+ {
+ _player.Collider.enabled = true;
+ _collisionDisabled = false;
+ }
+
+ return;
+ }
+
+ if (!_collisionDisabled && _player.Collider.enabled)
+ {
+ _player.Collider.enabled = false;
+ _collisionDisabled = true;
+ }
+
+ if (distance <= 0.001f)
+ return;
+
+ var outward = fromCenter / distance;
+ var inward = -outward;
+ var tangent = new Vector2(-outward.y, outward.x);
+
+ var proximity = Mathf.Clamp01(1f - distance / options.CrimsonRadius.Value);
+
+ var pullProgress = proximity * proximity;
+ var intensity = options.CrimsonIntensity.Value switch
+ {
+ GEOptions.VortexIntensity.Gentle => 0.75f,
+ GEOptions.VortexIntensity.Standard => 1f,
+ GEOptions.VortexIntensity.Brutal => 1.3f,
+ _ => 1f
+ };
+ var pull = Mathf.Lerp(0.55f, 4.25f, pullProgress) * intensity;
+ var orbit = 2.25f * intensity * (1f - pullProgress);
+
+ var currentDirection = Vector2.zero;
+
+ if (!_player.isDummy) currentDirection = _physics.GetVelocity() / Mathf.Max(_physics.TrueSpeed, 0.01f);
+
+ var direction = currentDirection + inward * pull + tangent * orbit;
+
+ _writingVelocity = true;
+
+ _physics.SetNormalizedVelocity(Vector2.ClampMagnitude(direction, 2.1f * intensity));
+
+ _writingVelocity = false;
+ }
+
+ public bool CanAcceptEscape()
+ {
+ if (!CrismonVortexGE.Active || !CrismonVortexGE.PositionReady || MeetingHud.Instance || ExileController.Instance || _player.Data.IsDead || _player.HasModifier() || !_player.CanMove || _player.inVent || _escaping || _escapedThisEntry) return false;
+
+ var options = OptionGroupSingleton.Instance;
+ var distance = Vector2.Distance(_player.GetTruePosition(), CrismonVortexGE.VortexPosition);
+ var killRadius = Mathf.Clamp(options.CrimsonRadius.Value * 0.12f, 0.45f, 0.9f);
+
+ return distance < options.CrimsonRadius.Value && distance > killRadius;
+ }
+
+ public void BeginEscape()
+ {
+ if (_escaping || _escapedThisEntry || _player.HasModifier())
+ return;
+
+ _escaping = true;
+ _escapedThisEntry = true;
+ _escapeProgress = 0f;
+ _lastPressTime = float.NegativeInfinity;
+
+ if (!_physics.AmOwner)
+ return;
+
+ var escapePosition = _lastSafePositionReady ? _lastSafePosition : ShipStatus.Instance.InitialSpawnCenter;
+ _player.NetTransform.RpcSnapTo(escapePosition);
+ if (SeasonManager.AvailableAchievementTabTypes.Contains(typeof(PreseasonAchievementsTab))) PreseasonAchievementsTab.EventHorizonDenied.Unlock();
+
+ if (_collisionDisabled)
+ {
+ _player.Collider.enabled = true;
+ _collisionDisabled = false;
+ }
+
+ if (CrimsonVortexEscapeHud.Instance)
+ CrimsonVortexEscapeHud.Instance.ShowSuccess();
+ }
+
+ public bool ShouldConsumeEscapeInput()
+ {
+ if (!CrismonVortexGE.Active || !CrismonVortexGE.PositionReady || MeetingHud.Instance || ExileController.Instance || _player.Data.IsDead || _player.HasModifier()) return false;
+
+ if (_escaping)
+ return true;
+
+ var options = OptionGroupSingleton.Instance;
+ var distance = Vector2.Distance(_player.GetTruePosition(), CrismonVortexGE.VortexPosition);
+ var killRadius = Mathf.Clamp(options.CrimsonRadius.Value * 0.12f, 0.45f, 0.9f);
+
+ return !_escapedThisEntry && distance <= options.CrimsonRadius.Value * 0.75f && distance > killRadius;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Components/EnergyPowerNode.cs b/NewMod/Components/EnergyPowerNode.cs
new file mode 100644
index 0000000..821f27f
--- /dev/null
+++ b/NewMod/Components/EnergyPowerNode.cs
@@ -0,0 +1,104 @@
+using NewMod.Roles.NeutralRoles;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components;
+
+[RegisterInIl2Cpp]
+public sealed class EnergyPowerNode(nint ptr) : MonoBehaviour(ptr)
+{
+ public static EnergyPowerNode Instance;
+ public static int RenderQueue = 4000;
+ public static string SpriteShader = "Sprites/Default";
+
+ public SpriteRenderer _renderer;
+ public Material _material;
+ public ArrowBehaviour _arrow;
+ public Vector3 _baseScale;
+ public bool _overcharged;
+
+ public static void Create()
+ {
+ DestroyCurrent();
+
+ var node = new GameObject("EnergyThief_PowerNode") { layer = 5 };
+ node.transform.SetParent(ShipStatus.Instance.transform, true);
+ node.transform.position = EnergyThief.GetNodePosition();
+ Instance = node.AddComponent();
+ }
+
+ public static void DestroyCurrent()
+ {
+ if (Instance)
+ Destroy(Instance.gameObject);
+
+ Instance = null;
+ }
+
+ public void Awake()
+ {
+ _baseScale = new Vector3(0.25f, 0.25f, 1f);
+ transform.localScale = _baseScale;
+
+ _renderer = gameObject.AddComponent();
+ _overcharged = EnergyThief.BreachActive;
+ _renderer.sprite = (_overcharged ? NewModAsset.PowerNodeOvercharged : NewModAsset.PowerNodeActive).LoadAsset();
+ _material = new Material(Shader.Find(SpriteShader)) { hideFlags = HideFlags.DontSave, renderQueue = RenderQueue };
+ _renderer.sharedMaterial = _material;
+ _renderer.maskInteraction = SpriteMaskInteraction.None;
+ _renderer.color = Color.white;
+ _renderer.sortingLayerID = HudManager.Instance.ShadowQuad.sortingLayerID;
+ _renderer.sortingOrder = HudManager.Instance.ShadowQuad.sortingOrder + 1;
+ }
+
+ public void Update()
+ {
+ var localPlayer = PlayerControl.LocalPlayer;
+ var isOwner = localPlayer && localPlayer.PlayerId == EnergyThief.NodeOwnerId;
+ _renderer.enabled = isOwner || EnergyThief.BreachActive;
+
+ if (_overcharged != EnergyThief.BreachActive)
+ {
+ _overcharged = EnergyThief.BreachActive;
+ _renderer.sprite = (_overcharged ? NewModAsset.PowerNodeOvercharged : NewModAsset.PowerNodeActive).LoadAsset();
+ }
+
+ if (_renderer.enabled)
+ {
+ var pulse = 1f + Mathf.Sin(Time.time * (EnergyThief.BreachActive ? 12f : 5f)) * (EnergyThief.BreachActive ? 0.12f : 0.05f);
+ transform.localScale = _baseScale * pulse;
+ }
+
+ if (isOwner && !EnergyThief.BreachActive)
+ {
+ if (!_arrow)
+ {
+ var arrowObject = new GameObject("EnergyThief_NodeArrow") { layer = 5 };
+ var arrowRenderer = arrowObject.AddComponent();
+ arrowRenderer.sprite = NewModAsset.Arrow.LoadAsset();
+ arrowRenderer.color = new Color(0.86f, 0.3f, 1f);
+ _arrow = arrowObject.AddComponent();
+ _arrow.image = arrowRenderer;
+ }
+
+ _arrow.target = transform.position;
+ }
+ else if (_arrow)
+ {
+ Destroy(_arrow.gameObject);
+ _arrow = null;
+ }
+ }
+
+ public void OnDestroy()
+ {
+ if (_material)
+ Destroy(_material);
+
+ if (_arrow)
+ Destroy(_arrow.gameObject);
+
+ if (Instance == this)
+ Instance = null;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Components/FearPulseArea.cs b/NewMod/Components/FearPulseArea.cs
index 989b31f..530e8c4 100644
--- a/NewMod/Components/FearPulseArea.cs
+++ b/NewMod/Components/FearPulseArea.cs
@@ -1,151 +1,159 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using MiraAPI.Utilities;
using NewMod.Utilities;
using Reactor.Utilities;
using Reactor.Utilities.Attributes;
using UnityEngine;
-namespace NewMod.Components
+namespace NewMod.Components;
+
+[RegisterInIl2Cpp]
+public class FearPulseArea(IntPtr ptr) : MonoBehaviour(ptr)
{
- [RegisterInIl2Cpp]
- public class FearPulseArea(IntPtr ptr) : MonoBehaviour(ptr)
+ public static readonly HashSet AffectedPlayers = [];
+
+ public static readonly Dictionary ActivePulseCounts = [];
+ public static readonly Dictionary OriginalSpeeds = [];
+
+ public AreaBubble bubble;
+ public byte ownerId;
+
+ public AudioClip _enterClip;
+ public AudioClip _heartbeatClip;
+ public byte _affectedPlayerId = byte.MaxValue;
+ public float _duration;
+ public float _elapsed;
+ public float _radius;
+ public bool _affectingLocalPlayer;
+ public bool _restored;
+ public float _speedMultiplier;
+
+ public void Update()
{
- public byte ownerId;
- float _radius, _duration, _speedMul, _t;
- readonly Dictionary _origSpeed = new();
- readonly HashSet _insideNow = new();
- public static readonly HashSet AffectedPlayers = new();
- public static readonly HashSet _speedNotifShown = new();
- public static readonly HashSet _visionNotifShown = new();
- public AudioClip _enterClip;
- public AudioClip _heartbeatClip;
- public bool _pulsingHb;
-
- public void Init(byte ownerId, float radius, float duration, float speedMul)
+ if (_restored)
+ return;
+
+ _elapsed += Time.deltaTime;
+
+ if (_elapsed >= _duration || MeetingHud.Instance || ExileController.Instance)
{
- this.ownerId = ownerId;
- _radius = radius;
- _duration = duration;
- _speedMul = Mathf.Max(0f, 1f - (speedMul / 100f));
- _enterClip = NewModAsset.FearSound.LoadAsset();
- _heartbeatClip = NewModAsset.HeartbeatSound.LoadAsset();
+ RestoreAll();
+ Destroy(gameObject);
+ return;
}
- public void Update()
- {
- _t += Time.deltaTime;
- if (_t > _duration)
- {
- RestoreAll();
- Destroy(gameObject);
- return;
- }
+ var localPlayer = PlayerControl.LocalPlayer;
- var center = (Vector2)transform.position;
+ if (!localPlayer || localPlayer.Data == null || localPlayer.PlayerId == ownerId)
+ return;
- var nearby = PlayerControl.AllPlayerControls
- .ToArray()
- .Where(p => !p.Data.IsDead && !p.Data.Disconnected && p.PlayerId != ownerId)
- .Where(p => Vector2.Distance(p.GetTruePosition(), center) <= _radius)
- .ToList();
+ var inside = !localPlayer.Data.IsDead && !localPlayer.Data.Disconnected && Vector2.Distance(localPlayer.GetTruePosition(), transform.position) <= _radius;
- _insideNow.Clear();
+ if (inside && !_affectingLocalPlayer)
+ {
+ _affectingLocalPlayer = true;
+ _affectedPlayerId = localPlayer.PlayerId;
- foreach (var p in nearby)
- {
- _insideNow.Add(p.PlayerId);
-
- if (!_origSpeed.ContainsKey(p.PlayerId))
- {
- _origSpeed[p.PlayerId] = p.MyPhysics.Speed;
- p.MyPhysics.Speed = _origSpeed[p.PlayerId] * _speedMul;
-
- if (p.AmOwner && !_speedNotifShown.Contains(p.PlayerId))
- {
- _speedNotifShown.Add(p.PlayerId);
- var notif = Helpers.CreateAndShowNotification(
- "You have entered the Fear Pulse Area. Your speed is reduced!",
- Color.red,
- spr: NewModAsset.SpeedDebuff.LoadAsset()
- );
- notif.Text.SetOutlineThickness(0.36f);
- }
- }
-
- if (p.AmOwner)
- {
- if (!AffectedPlayers.Contains(p.PlayerId))
- {
- AffectedPlayers.Add(p.PlayerId);
- p.lightSource.lightChild.SetActive(false);
- }
-
- if (!p.Data.IsDead && Constants.ShouldPlaySfx() && !SoundManager.Instance.SoundIsPlaying(_enterClip))
- SoundManager.Instance.PlaySound(_enterClip, false, 1f);
-
- if (!_visionNotifShown.Contains(p.PlayerId))
- {
- _visionNotifShown.Add(p.PlayerId);
- var notif = Helpers.CreateAndShowNotification(
- "You have entered the Fear Pulse Area. Your vision is reduced!",
- new Color(1f, 0.8f, 0.2f),
- spr: NewModAsset.VisionDebuff.LoadAsset()
- );
- notif.Text.SetOutlineThickness(0.36f);
- }
- Coroutines.Start(Utils.CoShakeCamera(Camera.main.GetComponent(), 0.5f));
- }
-
- if (p.MyPhysics.Velocity.sqrMagnitude > 0.0001f)
- {
- _pulsingHb = true;
- if (!p.Data.IsDead && Constants.ShouldPlaySfx() && !SoundManager.Instance.SoundIsPlaying(_heartbeatClip))
- SoundManager.Instance.PlaySound(_heartbeatClip, false, 1f);
- _pulsingHb = false;
- }
- }
+ ActivePulseCounts.TryGetValue(localPlayer.PlayerId, out var activePulses);
+ ActivePulseCounts[localPlayer.PlayerId] = activePulses + 1;
- if (_origSpeed.Count > 0)
+ if (activePulses == 0)
{
- var toRestore = _origSpeed.Keys.Where(id => !_insideNow.Contains(id)).ToList();
- foreach (var id in toRestore)
- {
- var p = Utils.PlayerById(id);
- if (p) p.MyPhysics.Speed = _origSpeed[id];
- _origSpeed.Remove(id);
-
- if (p.AmOwner)
- {
- AffectedPlayers.Remove(p.PlayerId);
- p.lightSource.lightChild.SetActive(true);
- _speedNotifShown.Remove(p.PlayerId);
- _visionNotifShown.Remove(p.PlayerId);
- Helpers.CreateAndShowNotification("Your vision is restored.", new Color(0.8f, 1f, 0.8f));
- }
- }
+ OriginalSpeeds[localPlayer.PlayerId] = localPlayer.MyPhysics.Speed;
+ localPlayer.MyPhysics.Speed *= _speedMultiplier;
+ AffectedPlayers.Add(localPlayer.PlayerId);
+
+ var speedNotification = Helpers.CreateAndShowNotification("Intimidated: leave the area to restore your speed.", Color.red, spr: NewModAsset.SpeedDebuff.LoadAsset());
+ speedNotification.Text.SetOutlineThickness(0.36f);
+
+ if (Constants.ShouldPlaySfx())
+ SoundManager.Instance.PlaySound(_enterClip, false);
}
}
- public void RestoreAll()
+ else if (!inside && _affectingLocalPlayer)
{
- foreach (var kv in _origSpeed)
- {
- var p = Utils.PlayerById(kv.Key);
- if (p) p.MyPhysics.Speed = kv.Value;
- }
- _origSpeed.Clear();
- AffectedPlayers.Clear();
+ RestorePlayer(_affectedPlayerId);
+ }
- var lp = PlayerControl.LocalPlayer;
+ if (inside && localPlayer.MyPhysics.Velocity.sqrMagnitude > 0.0001f && Constants.ShouldPlaySfx() && !SoundManager.Instance.SoundIsPlaying(_heartbeatClip))
+ SoundManager.Instance.PlaySound(_heartbeatClip, false);
+ }
- if (AffectedPlayers.Contains(lp.PlayerId))
- AffectedPlayers.Remove(lp.PlayerId);
+ public void OnDestroy()
+ {
+ RestoreAll();
+ if (bubble)
+ bubble.Break();
+ }
- lp.lightSource.lightChild.SetActive(true);
+ public void Init(byte ownerId, float radius, float duration, float speedMul)
+ {
+ this.ownerId = ownerId;
+ _radius = radius;
+ _duration = duration;
+ _speedMultiplier = Mathf.Max(0f, 1f - speedMul / 100f);
+ _enterClip = NewModAsset.FearSound.LoadAsset();
+ _heartbeatClip = NewModAsset.HeartbeatSound.LoadAsset();
+ }
- _speedNotifShown.Remove(lp.PlayerId);
- _visionNotifShown.Remove(lp.PlayerId);
+ public void RestorePlayer(byte playerId)
+ {
+ if (!_affectingLocalPlayer || playerId != _affectedPlayerId)
+ return;
+
+ _affectingLocalPlayer = false;
+ _affectedPlayerId = byte.MaxValue;
+
+ if (!ActivePulseCounts.TryGetValue(playerId, out var activePulses))
+ return;
+
+ if (activePulses > 1)
+ {
+ ActivePulseCounts[playerId] = activePulses - 1;
+ return;
}
+
+ ActivePulseCounts.Remove(playerId);
+ AffectedPlayers.Remove(playerId);
+
+ if (!OriginalSpeeds.Remove(playerId, out var originalSpeed))
+ return;
+
+ var player = Utils.PlayerById(playerId);
+
+ if (!player || !player.MyPhysics)
+ return;
+
+ player.MyPhysics.Speed = originalSpeed;
+
+ if (!player.AmOwner)
+ return;
+
+ SoundManager.Instance.StopSound(_enterClip);
+ SoundManager.Instance.StopSound(_heartbeatClip);
+ Helpers.CreateAndShowNotification("Your speed is restored.", new Color(0.8f, 1f, 0.8f));
+ }
+
+ public void RestoreAll()
+ {
+ if (_restored)
+ return;
+
+ _restored = true;
+
+ if (_affectingLocalPlayer)
+ RestorePlayer(_affectedPlayerId);
+ }
+
+ public static void ResetState()
+ {
+ var localPlayer = PlayerControl.LocalPlayer;
+
+ if (localPlayer && OriginalSpeeds.Remove(localPlayer.PlayerId, out var originalSpeed)) localPlayer.MyPhysics.Speed = originalSpeed;
+
+ ActivePulseCounts.Clear();
+ OriginalSpeeds.Clear();
+ AffectedPlayers.Clear();
}
-}
+}
\ No newline at end of file
diff --git a/NewMod/Components/GeneralNPC.cs b/NewMod/Components/GeneralNPC.cs
index f55065a..ec144f6 100644
--- a/NewMod/Components/GeneralNPC.cs
+++ b/NewMod/Components/GeneralNPC.cs
@@ -56,7 +56,7 @@ public void Initialize(PlayerControl owner)
isActive = true;
Coroutines.Start(WalkStopLoop());
}
-
+
[HideFromIl2Cpp]
public IEnumerator WalkStopLoop()
{
@@ -66,19 +66,19 @@ public IEnumerator WalkStopLoop()
{
var pos = npc.GetTruePosition();
var current = RoomPathfinding.GetCurrentRoom(pos);
- NewMod.Instance.Log.LogMessage($"[NPC] pos={pos} room={(current ? current.name : "none")}");
+ Message($"[NPC] pos={pos} room={(current ? current.name : "none")}");
var target = RoomPathfinding.PickRandomOtherRoom(current);
if (!target)
{
- NewMod.Instance.Log.LogWarning("[NPC] no target room, waiting");
+ Warning("[NPC] no target room, waiting");
npc.MyPhysics.SetNormalizedVelocity(Vector2.zero);
yield return new WaitForSeconds(stopTime);
continue;
}
var path = RoomPathfinding.FindRoomPath(current, target);
- NewMod.Instance.Log.LogMessage($"[NPC] target={target.name} pathLen={path?.Count ?? 0}");
+ Message($"[NPC] target={target.name} pathLen={path?.Count ?? 0}");
if (path == null || path.Count == 0)
{
@@ -94,7 +94,7 @@ public IEnumerator WalkStopLoop()
if (!RoomPathfinding.TryPickWaypointInside(room.roomArea, out var wp))
wp = (Vector2)room.roomArea.bounds.center;
- NewMod.Instance.Log.LogMessage($"[NPC] moving to room={room.name} wp={wp}");
+ Message($"[NPC] moving to room={room.name} wp={wp}");
float timer = 0f;
while (isActive && !MeetingHud.Instance &&
@@ -107,7 +107,7 @@ public IEnumerator WalkStopLoop()
if (timer >= runTime)
{
npc.MyPhysics.SetNormalizedVelocity(Vector2.zero);
- NewMod.Instance.Log.LogMessage($"[NPC] burst stop {stopTime}s");
+ Message($"[NPC] burst stop {stopTime}s");
yield return new WaitForSeconds(stopTime);
timer = 0f;
}
@@ -116,7 +116,7 @@ public IEnumerator WalkStopLoop()
}
npc.MyPhysics.SetNormalizedVelocity(Vector2.zero);
- NewMod.Instance.Log.LogMessage($"[NPC] arrived {room.name}, visiting {stopTime}s");
+ Message($"[NPC] arrived {room.name}, visiting {stopTime}s");
yield return new WaitForSeconds(stopTime);
}
}
@@ -140,3 +140,4 @@ public void Dispose()
}
}
}*/
+
diff --git a/NewMod/Components/NMOverlay.cs b/NewMod/Components/NMOverlay.cs
new file mode 100644
index 0000000..e606eb4
--- /dev/null
+++ b/NewMod/Components/NMOverlay.cs
@@ -0,0 +1,84 @@
+using HarmonyLib;
+using Reactor.Utilities.Attributes;
+using TMPro;
+using UnityEngine;
+
+namespace NewMod.Components;
+
+[RegisterInIl2Cpp]
+public class NMOverlay(nint ptr) : MonoBehaviour(ptr)
+{
+ public TextMeshPro Label;
+ public float StartedAt;
+ public int StartedFrame;
+ public float FramesPerSecond;
+ public float WarningUntil;
+ public bool Has;
+
+ public void Awake()
+ {
+ var labelObject = new GameObject("NewModOverlay") { layer = LayerMask.NameToLayer("UI") };
+ labelObject.transform.SetParent(transform, false);
+ labelObject.transform.localPosition = new Vector3(0f, 2.22f, -20f);
+
+ Label = labelObject.AddComponent();
+ Label.font = HudManager.Instance.TaskPanel.taskText.font;
+ Label.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial;
+ Label.fontSize = HudManager.Instance.TaskPanel.taskText.fontSize;
+ Label.alignment = TextAlignmentOptions.Top;
+ Label.enableWordWrapping = false;
+ Label.rectTransform.sizeDelta = new Vector2(8f, 0.7f);
+ StartedAt = Time.unscaledTime;
+ StartedFrame = Time.frameCount;
+ }
+
+ public void Update()
+ {
+ var client = AmongUsClient.Instance;
+ var visible = client && PlayerControl.LocalPlayer;
+ Label.gameObject.SetActive(visible);
+ if (!visible)
+ {
+ StartedAt = Time.unscaledTime;
+ StartedFrame = Time.frameCount;
+ Has = false;
+ WarningUntil = 0f;
+ return;
+ }
+
+ var now = Time.unscaledTime;
+ var elapsed = now - StartedAt;
+ if (elapsed is >= 0.5f and > 0f)
+ {
+ var fps = (Time.frameCount - StartedFrame) / elapsed;
+ if (Has && fps < FramesPerSecond * (1f - 0.25f))
+ WarningUntil = now + 1.5f;
+ FramesPerSecond = fps;
+ Has = true;
+ StartedAt = now;
+ StartedFrame = Time.frameCount;
+ }
+
+ var fpsColor = FramesPerSecond < 30f ? "#FF6060" : FramesPerSecond < 60f || now < WarningUntil ? "#FFD45C" : "#75E6A5";
+ Label.text = Has ? $"FPS: {Mathf.RoundToInt(FramesPerSecond)}" : "FPS: ...";
+
+ if (!client.IsGameStarted && client.NetworkMode != NetworkModes.FreePlay)
+ {
+ var local = client.NetworkMode == NetworkModes.LocalGame;
+ var region = local ? "Local" : ServerManager.Instance.CurrentRegion.Name;
+ var regionColor = local || client.Ping <= 60f ? "#75E6A5" : client.Ping <= 200f ? "#FFD45C" : "#FF6060";
+ Label.text += $" | REGION: {region}";
+ }
+ }
+}
+
+[HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))]
+public static class NMOverlayHudPatch
+{
+ [HarmonyPostfix]
+ public static void Postfix(HudManager __instance)
+ {
+ if (!__instance.GetComponent())
+ __instance.gameObject.AddComponent();
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Components/ScreenEffects/CrimsonVortexEffect.cs b/NewMod/Components/ScreenEffects/CrimsonVortexEffect.cs
new file mode 100644
index 0000000..b91a81f
--- /dev/null
+++ b/NewMod/Components/ScreenEffects/CrimsonVortexEffect.cs
@@ -0,0 +1,149 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Modifiers;
+using NewMod.GeneralEvents.Season1;
+using NewMod.Modifiers.S1;
+using NewMod.Options;
+using UnityEngine;
+
+namespace NewMod.Components.ScreenEffects;
+
+public class CrimsonVortexEffect : ScreenEffect
+{
+ public Color tint = new(0.85f, 0.015f, 0.025f, 1f);
+ public Color hotTint = new(1f, 0.18f, 0.055f, 1f);
+ public Color deepTint = new(0.16f, 0f, 0.015f, 1f);
+ public Color coreTint = new(0.015f, 0f, 0.008f, 1f);
+
+ public float amount = 1f;
+ public float opacity = 0.9f;
+ public float tiling = 1.35f;
+ public float detailTiling = 3.15f;
+ public float flowSpeed = 0.22f;
+ public float vortexStrength = 2.35f;
+ public float vortexSpeed = 0.5f;
+ public float distortStrength = 0.025f;
+ public float suctionStrength = 0.018f;
+ public float chromaticShift = 0.0022f;
+ public float density = 1.25f;
+ public float contrast = 1.7f;
+ public float filamentStrength = 1.35f;
+ public float filamentSharpness = 5.5f;
+ public float edgeSoftness = 0.18f;
+ public float coreSoftness = 0.3f;
+ public float coreDarkness = 0.94f;
+ public float coreGlow = 1.9f;
+ public float pulseStrength = 0.12f;
+ public float pulseSpeed = 1.5f;
+ public float vignette = 0.08f;
+ public Vector2 scrollDir = new(1f, 0f);
+ public float frontWidth = 0.75f;
+ public float frontSoftness = 0.18f;
+ public float frontTravel = 0.38f;
+ public float leadingEdgeWidth = 0.045f;
+ public float leadingEdgeStrength = 2.2f;
+
+ public Camera _camera;
+
+ public override void Initialize()
+ {
+ _camera = Owner.GetComponent();
+
+ var shader = NewModAsset.CrismonVortexShader.LoadAsset();
+
+ var texture = NewModAsset.CrismonTexture.LoadAsset();
+
+ if (!shader || !texture)
+ {
+ Active = false;
+ return;
+ }
+
+ _mat = new Material(shader) { hideFlags = HideFlags.DontSave };
+
+ _mat.SetTexture("_CrimsonTex", texture);
+ }
+
+ public override void Render(RenderTexture src, RenderTexture dst)
+ {
+ if (!_mat || !CrismonVortexGE.Active || !CrismonVortexGE.PositionReady || MeetingHud.Instance || ExileController.Instance || PlayerControl.LocalPlayer.HasModifier())
+ {
+ Graphics.Blit(src, dst);
+ return;
+ }
+
+ var options = OptionGroupSingleton.Instance;
+
+ var worldCenter = new Vector3(CrismonVortexGE.VortexPosition.x, CrismonVortexGE.VortexPosition.y, 0f);
+
+ var center = _camera.WorldToViewportPoint(worldCenter);
+
+ var radius = options.CrimsonRadius.Value / (_camera.orthographicSize * 2f);
+
+ var killRadius = Mathf.Clamp(options.CrimsonRadius.Value * 0.12f, 0.45f, 0.9f);
+ var coreRadius = killRadius / (_camera.orthographicSize * 2f);
+
+ _mat.SetFloat("_Amount", amount);
+
+ _mat.SetColor("_Tint", tint);
+ _mat.SetColor("_HotTint", hotTint);
+ _mat.SetColor("_DeepTint", deepTint);
+ _mat.SetColor("_CoreTint", coreTint);
+
+ _mat.SetFloat("_Opacity", opacity);
+ _mat.SetFloat("_Tiling", tiling);
+ _mat.SetFloat("_DetailTiling", detailTiling);
+
+ _mat.SetFloat("_FlowSpeed", flowSpeed);
+ _mat.SetFloat("_VortexStrength", vortexStrength);
+ _mat.SetFloat("_VortexSpeed", vortexSpeed);
+
+ _mat.SetFloat("_DistortStrength", distortStrength);
+
+ _mat.SetFloat("_SuctionStrength", suctionStrength);
+
+ _mat.SetFloat("_ChromaticShift", chromaticShift);
+
+ _mat.SetFloat("_Density", density);
+ _mat.SetFloat("_Contrast", contrast);
+
+ _mat.SetFloat("_FilamentStrength", filamentStrength);
+
+ _mat.SetFloat("_FilamentSharpness", filamentSharpness);
+
+ _mat.SetVector("_Center", new Vector4(center.x, center.y, 0f, 0f));
+
+ _mat.SetFloat("_Radius", radius);
+
+ _mat.SetFloat("_EdgeSoftness", radius * edgeSoftness);
+
+ _mat.SetFloat("_CoreRadius", coreRadius);
+
+ _mat.SetFloat("_CoreSoftness", coreRadius * coreSoftness);
+
+ _mat.SetFloat("_CoreDarkness", coreDarkness);
+
+ _mat.SetFloat("_CoreGlow", coreGlow);
+
+ _mat.SetFloat("_PulseStrength", pulseStrength);
+
+ _mat.SetFloat("_PulseSpeed", pulseSpeed);
+
+ _mat.SetFloat("_Vignette", vignette);
+
+ _mat.SetVector("_ScrollDir", new Vector4(scrollDir.x, scrollDir.y, 0f, 0f));
+
+ _mat.SetFloat("_FrontWidth", frontWidth);
+
+ _mat.SetFloat("_FrontSoftness", frontSoftness);
+
+ _mat.SetFloat("_FrontTravel", frontTravel);
+
+ _mat.SetFloat("_LeadingEdgeWidth", leadingEdgeWidth);
+
+ _mat.SetFloat("_LeadingEdgeStrength", leadingEdgeStrength);
+
+ _mat.SetFloat("_UseCircular", 1f);
+
+ Graphics.Blit(src, dst, _mat);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Components/ScreenEffects/DistorationWaveEffect.cs b/NewMod/Components/ScreenEffects/DistorationWaveEffect.cs
index 1c6aaaf..7a75ca1 100644
--- a/NewMod/Components/ScreenEffects/DistorationWaveEffect.cs
+++ b/NewMod/Components/ScreenEffects/DistorationWaveEffect.cs
@@ -1,42 +1,39 @@
-using System;
-using Reactor.Utilities.Attributes;
using UnityEngine;
-namespace NewMod.Components.ScreenEffects
+namespace NewMod.Components.ScreenEffects;
+
+public class DistorationWaveEffect : ScreenEffect
{
- [RegisterInIl2Cpp]
- public class DistorationWaveEffect(IntPtr ptr) : MonoBehaviour(ptr)
- {
- public float amplitude = 0.05f;
- public float frequency = 5f;
- public float speed = 1f;
- public float radius = 0.25f;
- public float falloff = 1f;
- public Vector2 center = new Vector2(0.5f, 0.5f);
- public Color tint = Color.white;
- private readonly Shader _shader = NewModAsset.DistorationWaveShader.LoadAsset();
- public Material _mat;
+ public float amplitude = 0.05f;
+ public float frequency = 5f;
+ public float speed = 1f;
+ public float radius = 0.25f;
+ public float falloff = 1f;
+ public Vector2 center = new(0.5f, 0.5f);
+ public Color tint = Color.white;
+ public float expiresAt = float.PositiveInfinity;
+ public Shader _shader = NewModAsset.DistorationWaveShader.LoadAsset();
- public void OnEnable()
- {
- _mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
- }
+ public override void Initialize()
+ {
+ _mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
+ }
- public void OnDisable()
- {
- Destroy(_mat);
- }
+ public override void Tick()
+ {
+ if (Time.time >= expiresAt)
+ Remove();
+ }
- public void OnRenderImage(RenderTexture src, RenderTexture dst)
- {
- _mat.SetFloat("_Amplitude", amplitude);
- _mat.SetFloat("_Frequency", frequency);
- _mat.SetFloat("_Speed", speed);
- _mat.SetFloat("_Radius", radius);
- _mat.SetFloat("_Falloff", falloff);
- _mat.SetVector("_Center", center);
- _mat.SetColor("_Tint", tint);
- Graphics.Blit(src, dst, _mat);
- }
+ public override void Render(RenderTexture src, RenderTexture dst)
+ {
+ _mat.SetFloat("_Amplitude", amplitude);
+ _mat.SetFloat("_Frequency", frequency);
+ _mat.SetFloat("_Speed", speed);
+ _mat.SetFloat("_Radius", radius);
+ _mat.SetFloat("_Falloff", falloff);
+ _mat.SetVector("_Center", center);
+ _mat.SetColor("_Tint", tint);
+ Graphics.Blit(src, dst, _mat);
}
-}
+}
\ No newline at end of file
diff --git a/NewMod/Components/ScreenEffects/EarthquackEffect.cs b/NewMod/Components/ScreenEffects/EarthquackEffect.cs
index 1138a8f..22818e6 100644
--- a/NewMod/Components/ScreenEffects/EarthquackEffect.cs
+++ b/NewMod/Components/ScreenEffects/EarthquackEffect.cs
@@ -1,35 +1,28 @@
-using System;
-using Reactor.Utilities.Attributes;
using UnityEngine;
-namespace NewMod.Components.ScreenEffects
+namespace NewMod.Components.ScreenEffects;
+
+public class EarthquakeEffect : ScreenEffect
{
- [RegisterInIl2Cpp]
- public class EarthquakeEffect(IntPtr ptr) : MonoBehaviour(ptr)
+ public float amplitude = 2.5f;
+ public float frequency = 14f;
+ public float jitter = 0.6f;
+ public float ghost = 0.3f;
+ public float warp = 0.015f;
+ public Shader _shader = NewModAsset.EarthquakeShader.LoadAsset();
+
+ public override void Initialize()
+ {
+ _mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
+ }
+
+ public override void Render(RenderTexture src, RenderTexture dst)
{
- public float amplitude = 2.5f;
- public float frequency = 14f;
- public float jitter = 0.6f;
- public float ghost = 0.3f;
- public float warp = 0.015f;
- private readonly Shader _shader = NewModAsset.EarthquakeShader.LoadAsset();
- private Material _mat;
- public void OnEnable()
- {
- _mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
- }
- public void OnDisable()
- {
- Destroy(_mat);
- }
- public void OnRenderImage(RenderTexture src, RenderTexture dst)
- {
- _mat.SetFloat("_Amplitude", amplitude);
- _mat.SetFloat("_Frequency", frequency);
- _mat.SetFloat("_Jitter", jitter);
- _mat.SetFloat("_Ghost", ghost);
- _mat.SetFloat("_Warp", warp);
- Graphics.Blit(src, dst, _mat);
- }
+ _mat.SetFloat("_Amplitude", amplitude);
+ _mat.SetFloat("_Frequency", frequency);
+ _mat.SetFloat("_Jitter", jitter);
+ _mat.SetFloat("_Ghost", ghost);
+ _mat.SetFloat("_Warp", warp);
+ Graphics.Blit(src, dst, _mat);
}
-}
+}
\ No newline at end of file
diff --git a/NewMod/Components/ScreenEffects/GlitchEffect.cs b/NewMod/Components/ScreenEffects/GlitchEffect.cs
index 0efbe74..22ec370 100644
--- a/NewMod/Components/ScreenEffects/GlitchEffect.cs
+++ b/NewMod/Components/ScreenEffects/GlitchEffect.cs
@@ -1,36 +1,28 @@
-using System;
-using Reactor.Utilities.Attributes;
using UnityEngine;
-namespace NewMod.Components.ScreenEffects
+namespace NewMod.Components.ScreenEffects;
+
+public class GlitchEffect : ScreenEffect
{
- [RegisterInIl2Cpp]
- public class GlitchEffect(IntPtr ptr) : MonoBehaviour(ptr)
+ public float intensity = 0.45f;
+ public float blockSize = 64f;
+ public float colorSplit = 1.2f;
+ public float scanline = 0.15f;
+ public float speed = 4f;
+ public Shader _shader = NewModAsset.GlitchShader.LoadAsset();
+
+ public override void Initialize()
{
- public float intensity = 0.45f;
- public float blockSize = 64f;
- public float colorSplit = 1.2f;
- public float scanline = 0.15f;
- public float speed = 4f;
- private readonly Shader _shader = NewModAsset.GlitchShader.LoadAsset();
- public Material _mat;
+ _mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
+ }
- public void OnEnable()
- {
- _mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
- }
- public void OnDisable()
- {
- Destroy(_mat);
- }
- public void OnRenderImage(RenderTexture src, RenderTexture dst)
- {
- _mat.SetFloat("_Intensity", intensity);
- _mat.SetFloat("_BlockSize", blockSize);
- _mat.SetFloat("_ColorSplit", colorSplit);
- _mat.SetFloat("_Scanline", scanline);
- _mat.SetFloat("_Speed", speed);
- Graphics.Blit(src, dst, _mat);
- }
+ public override void Render(RenderTexture src, RenderTexture dst)
+ {
+ _mat.SetFloat("_Intensity", intensity);
+ _mat.SetFloat("_BlockSize", blockSize);
+ _mat.SetFloat("_ColorSplit", colorSplit);
+ _mat.SetFloat("_Scanline", scanline);
+ _mat.SetFloat("_Speed", speed);
+ Graphics.Blit(src, dst, _mat);
}
-}
+}
\ No newline at end of file
diff --git a/NewMod/Components/ScreenEffects/NegativeRealityEffect.cs b/NewMod/Components/ScreenEffects/NegativeRealityEffect.cs
new file mode 100644
index 0000000..07b1cc6
--- /dev/null
+++ b/NewMod/Components/ScreenEffects/NegativeRealityEffect.cs
@@ -0,0 +1,98 @@
+using MiraAPI.Modifiers;
+using NewMod.Modifiers.S1;
+using UnityEngine;
+
+namespace NewMod.Components.ScreenEffects;
+
+public class NegativeRealityEffect : ScreenEffect
+{
+ public float amount = 1f;
+ public float invertStrength = 1f;
+ public float contrast = 2.05f;
+ public float crushBlack = 0.08f;
+ public float crushWhite = 0.07f;
+ public float desaturate = 0.18f;
+
+ public float cyanBleed = 0.85f;
+ public Color ghostTint = new(0.68f, 1f, 1f, 1f);
+ public float whiteGlow = 1.25f;
+ public float glowThreshold = 0.42f;
+ public float glowRadius = 2.2f;
+
+ public float edgeStrength = 0.95f;
+ public Color edgeTint = new(0.95f, 1f, 1f, 1f);
+
+ public float vignetteStrength = 0.58f;
+ public float vignetteSize = 0.42f;
+ public float vignetteSoftness = 0.55f;
+ public Color vignetteColor = new(0.01f, 0f, 0.025f, 1f);
+
+ public float pulseStrength = 0.18f;
+ public float pulseSpeed = 1.65f;
+ public float pulseSharpness = 7.5f;
+
+ public float filmBurn = 0.28f;
+ public float burnSpeed = 0.35f;
+ public float noiseStrength = 0.11f;
+ public float noiseScale = 260f;
+
+ public float chromaticOffset = 0.0035f;
+ public float scanlineStrength = 0.08f;
+
+ public override void Initialize()
+ {
+ var shader = NewModAsset.NegativeRealityShader.LoadAsset();
+
+ if (shader == null)
+ {
+ Active = false;
+ return;
+ }
+
+ _mat = new Material(shader) { hideFlags = HideFlags.DontSave };
+ }
+
+ public override void Render(RenderTexture src, RenderTexture dst)
+ {
+ if (_mat == null || PlayerControl.LocalPlayer.HasModifier())
+ {
+ Graphics.Blit(src, dst);
+ return;
+ }
+
+ _mat.SetFloat("_Amount", amount);
+ _mat.SetFloat("_InvertStrength", invertStrength);
+ _mat.SetFloat("_Contrast", contrast);
+ _mat.SetFloat("_CrushBlack", crushBlack);
+ _mat.SetFloat("_CrushWhite", crushWhite);
+ _mat.SetFloat("_Desaturate", desaturate);
+
+ _mat.SetFloat("_CyanBleed", cyanBleed);
+ _mat.SetColor("_GhostTint", ghostTint);
+ _mat.SetFloat("_WhiteGlow", whiteGlow);
+ _mat.SetFloat("_GlowThreshold", glowThreshold);
+ _mat.SetFloat("_GlowRadius", glowRadius);
+
+ _mat.SetFloat("_EdgeStrength", edgeStrength);
+ _mat.SetColor("_EdgeTint", edgeTint);
+
+ _mat.SetFloat("_VignetteStrength", vignetteStrength);
+ _mat.SetFloat("_VignetteSize", vignetteSize);
+ _mat.SetFloat("_VignetteSoftness", vignetteSoftness);
+ _mat.SetColor("_VignetteColor", vignetteColor);
+
+ _mat.SetFloat("_PulseStrength", pulseStrength);
+ _mat.SetFloat("_PulseSpeed", pulseSpeed);
+ _mat.SetFloat("_PulseSharpness", pulseSharpness);
+
+ _mat.SetFloat("_FilmBurn", filmBurn);
+ _mat.SetFloat("_BurnSpeed", burnSpeed);
+ _mat.SetFloat("_NoiseStrength", noiseStrength);
+ _mat.SetFloat("_NoiseScale", noiseScale);
+
+ _mat.SetFloat("_ChromaticOffset", chromaticOffset);
+ _mat.SetFloat("_ScanlineStrength", scanlineStrength);
+
+ Graphics.Blit(src, dst, _mat);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Components/ScreenEffects/ScrDesyncEffect.cs b/NewMod/Components/ScreenEffects/ScrDesyncEffect.cs
new file mode 100644
index 0000000..aef7319
--- /dev/null
+++ b/NewMod/Components/ScreenEffects/ScrDesyncEffect.cs
@@ -0,0 +1,89 @@
+using System;
+using MiraAPI.Modifiers;
+using NewMod.Modifiers.S1;
+using UnityEngine;
+
+namespace NewMod.Components.ScreenEffects;
+
+public class ScrDesyncEffect : ScreenEffect
+{
+ public float duration = 24f;
+ public float blockSize = 48f;
+ public float colorSplit = 1.15f;
+ public float scanline = 0.13f;
+ public float speed = 4f;
+ public float tearStrength = 0.62f;
+ public float datamosh = 0.48f;
+ public float edgeGlitch = 0.85f;
+ public float syncFailure = 0.68f;
+ public float blackout = 0.36f;
+ public float flashStrength = 1.15f;
+ public float verticalTear = 0.38f;
+ public float microJitter = 0.42f;
+ public float freezeStrength = 0.38f;
+ public Color corruptTint = new(1f, 0.08f, 0.72f, 1f);
+ public Color coldTint = new(0.08f, 0.78f, 1f, 1f);
+ public Color blackTint = new(0.008f, 0.002f, 0.012f, 1f);
+
+ public float _startedAt;
+
+ public override void Initialize()
+ {
+ _mat = new Material(NewModAsset.GlitchScreenV2.LoadAsset()) { hideFlags = HideFlags.DontSave };
+ _startedAt = Time.time;
+ }
+
+ public override void Render(RenderTexture source, RenderTexture destination)
+ {
+ if (MeetingHud.Instance || ExileController.Instance || PlayerControl.LocalPlayer.HasModifier())
+ {
+ Graphics.Blit(source, destination);
+ return;
+ }
+
+ var frame = Sample(Time.time - _startedAt, duration);
+
+ if (frame.Finished)
+ {
+ Graphics.Blit(source, destination);
+ return;
+ }
+
+ _mat.SetFloat("_Intensity", frame.Intensity);
+ _mat.SetFloat("_BlockSize", blockSize);
+ _mat.SetFloat("_ColorSplit", colorSplit);
+ _mat.SetFloat("_Scanline", scanline);
+ _mat.SetFloat("_Speed", speed);
+ _mat.SetFloat("_Burst", frame.Burst);
+ _mat.SetFloat("_TearStrength", tearStrength);
+ _mat.SetFloat("_Datamosh", datamosh);
+ _mat.SetFloat("_EdgeGlitch", edgeGlitch);
+ _mat.SetFloat("_SyncFailure", syncFailure);
+ _mat.SetFloat("_Blackout", blackout);
+ _mat.SetFloat("_FlashStrength", flashStrength);
+ _mat.SetFloat("_VerticalTear", verticalTear);
+ _mat.SetFloat("_MicroJitter", microJitter);
+ _mat.SetFloat("_FreezeStrength", freezeStrength);
+ _mat.SetColor("_CorruptTint", corruptTint);
+ _mat.SetColor("_ColdTint", coldTint);
+ _mat.SetColor("_BlackTint", blackTint);
+ Graphics.Blit(source, destination, _mat);
+ }
+
+ public static (float Intensity, float Burst, bool Finished) Sample(float elapsed, float duration)
+ {
+ var time = Math.Clamp(elapsed, 0f, duration);
+ var visibility = Math.Min(Math.Clamp(time / 0.25f, 0f, 1f), Math.Clamp((duration - time) / 0.35f, 0f, 1f));
+ var opening = Pulse(time, 1f);
+ var recurring = time < 1f ? 0f : Pulse((time - 1f) % 4.5f, 0.6f);
+ var closing = time < duration - 1f ? 0f : Pulse(time - duration + 1f, 1f);
+ var burst = Math.Max(opening, Math.Max(recurring, closing)) * visibility;
+
+ return ((0.42f + burst * 0.18f) * visibility, burst, elapsed >= duration);
+ }
+
+ public static float Pulse(float time, float duration)
+ {
+ return time <= 0f || time >= duration ? 0f : MathF.Sin(MathF.PI * time / duration);
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Components/ScreenEffects/ScreenEffect.cs b/NewMod/Components/ScreenEffects/ScreenEffect.cs
new file mode 100644
index 0000000..e624d31
--- /dev/null
+++ b/NewMod/Components/ScreenEffects/ScreenEffect.cs
@@ -0,0 +1,33 @@
+using UnityEngine;
+
+namespace NewMod.Components.ScreenEffects;
+
+public abstract class ScreenEffect
+{
+ public ScreenEffectSystem Owner;
+ public Material _mat;
+ public bool Active = true;
+
+ public virtual void Initialize()
+ {
+ }
+
+ public virtual void Tick()
+ {
+ }
+
+ public abstract void Render(RenderTexture source, RenderTexture destination);
+
+ public void Remove()
+ {
+ Owner.Remove(this);
+ }
+
+ public virtual void Dispose()
+ {
+ Active = false;
+ if (_mat)
+ Object.Destroy(_mat);
+ _mat = null;
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Components/ScreenEffects/ScreenEffectSystem.cs b/NewMod/Components/ScreenEffects/ScreenEffectSystem.cs
new file mode 100644
index 0000000..fb1585b
--- /dev/null
+++ b/NewMod/Components/ScreenEffects/ScreenEffectSystem.cs
@@ -0,0 +1,115 @@
+using System.Collections.Generic;
+using Il2CppInterop.Runtime.Attributes;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components.ScreenEffects;
+
+[RegisterInIl2Cpp]
+public class ScreenEffectSystem(nint ptr) : MonoBehaviour(ptr)
+{
+ public readonly List Effects = new();
+
+ [HideFromIl2Cpp]
+ public T Get() where T : ScreenEffect
+ {
+ foreach (var effect in Effects)
+ if (effect is T match && effect.Active)
+ return match;
+ return null;
+ }
+
+ [HideFromIl2Cpp]
+ public T Add() where T : ScreenEffect, new()
+ {
+ var existing = Get