diff --git a/NewMod/Buttons/Shade/DeployShadow.cs b/NewMod/Buttons/Shade/DeployShadow.cs
new file mode 100644
index 0000000..b14db52
--- /dev/null
+++ b/NewMod/Buttons/Shade/DeployShadow.cs
@@ -0,0 +1,68 @@
+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/Components/ScreenEffects/DistorationWaveEffect.cs b/NewMod/Components/ScreenEffects/DistorationWaveEffect.cs
new file mode 100644
index 0000000..1c6aaaf
--- /dev/null
+++ b/NewMod/Components/ScreenEffects/DistorationWaveEffect.cs
@@ -0,0 +1,42 @@
+using System;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components.ScreenEffects
+{
+ [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 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("_Speed", speed);
+ _mat.SetFloat("_Radius", radius);
+ _mat.SetFloat("_Falloff", falloff);
+ _mat.SetVector("_Center", center);
+ _mat.SetColor("_Tint", tint);
+ Graphics.Blit(src, dst, _mat);
+ }
+ }
+}
diff --git a/NewMod/Components/ScreenEffects/GlitchEffect.cs b/NewMod/Components/ScreenEffects/GlitchEffect.cs
index 23db7e6..0efbe74 100644
--- a/NewMod/Components/ScreenEffects/GlitchEffect.cs
+++ b/NewMod/Components/ScreenEffects/GlitchEffect.cs
@@ -14,6 +14,7 @@ public class GlitchEffect(IntPtr ptr) : MonoBehaviour(ptr)
public float speed = 4f;
private readonly Shader _shader = NewModAsset.GlitchShader.LoadAsset();
public Material _mat;
+
public void OnEnable()
{
_mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
diff --git a/NewMod/Components/ScreenEffects/ShadowFluxEffect.cs b/NewMod/Components/ScreenEffects/ShadowFluxEffect.cs
new file mode 100644
index 0000000..e55085e
--- /dev/null
+++ b/NewMod/Components/ScreenEffects/ShadowFluxEffect.cs
@@ -0,0 +1,42 @@
+using System;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components.ScreenEffects
+{
+ [RegisterInIl2Cpp]
+ public class ShadowFluxEffect(IntPtr ptr) : MonoBehaviour(ptr)
+ {
+ public Texture2D texture = NewModAsset.NoiseTex.LoadAsset();
+ public float noiseScale = 2f;
+ public float speed = 0.3f;
+ public float edgeWidth = 0.25f;
+ public float threshold = 0.55f;
+ public float opacity = 0.75f;
+ public float darkness = 0.8f;
+ public Color tint = Color.white;
+ public static Shader _shader = NewModAsset.ShadowFluxShader.LoadAsset();
+ public Material _mat;
+
+ public void OnEnable()
+ {
+ _mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
+ _mat.SetTexture("_NoiseTex", texture);
+ }
+ public void OnDisable()
+ {
+ Destroy(_mat);
+ }
+ public void OnRenderImage(RenderTexture src, RenderTexture dst)
+ {
+ _mat.SetFloat("_NoiseScale", noiseScale);
+ _mat.SetFloat("_Speed", speed);
+ _mat.SetFloat("_EdgeWidth", edgeWidth);
+ _mat.SetFloat("_Threshold", threshold);
+ _mat.SetFloat("_Opacity", opacity);
+ _mat.SetFloat("_Darkness", darkness);
+ _mat.SetColor("_Tint", tint);
+ Graphics.Blit(src, dst, _mat);
+ }
+ }
+}
diff --git a/NewMod/Components/ShadowZone.cs b/NewMod/Components/ShadowZone.cs
new file mode 100644
index 0000000..9a5cb36
--- /dev/null
+++ b/NewMod/Components/ShadowZone.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MiraAPI.GameOptions;
+using NewMod.Components.ScreenEffects;
+using NewMod.Options.Roles.ShadeOptions;
+using NewMod.Roles.NeutralRoles;
+using NewMod.Utilities;
+using Reactor.Networking.Attributes;
+using Reactor.Utilities;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components
+{
+ [RegisterInIl2Cpp]
+ public class ShadowZone(IntPtr ptr) : MonoBehaviour(ptr)
+ {
+ public byte shadeId;
+ public float radius;
+ public float duration;
+ private float timer;
+ private bool active;
+ public static readonly List zones = new();
+
+ public void Awake()
+ {
+ if (!zones.Contains(this))
+ zones.Add(this);
+ }
+
+ public void OnDestroy()
+ {
+ zones.Remove(this);
+ }
+
+ private bool Contains(Vector2 pos)
+ {
+ return Vector2.Distance(pos, (Vector2)transform.position) <= radius;
+ }
+
+ public void Update()
+ {
+ timer += Time.deltaTime;
+ if (timer >= duration)
+ {
+ Coroutines.Start(CoroutinesHelper.RemoveCameraEffect(Camera.main, 0f));
+ active = false;
+ Destroy(gameObject);
+ return;
+ }
+
+ var lp = PlayerControl.LocalPlayer;
+ var hud = HudManager.Instance;
+ var killButton = hud.KillButton;
+
+ bool inside = Contains(lp.GetTruePosition());
+ var mode = OptionGroupSingleton.Instance.Behavior;
+ var cam = Camera.main;
+
+ if (inside && !active)
+ {
+ cam.gameObject.AddComponent();
+ if (lp.PlayerId == shadeId && lp.Data.Role is Shade)
+ {
+ if (mode is ShadeOptions.ShadowMode.Invisible or ShadeOptions.ShadowMode.Both)
+ {
+ lp.cosmetics.SetPhantomRoleAlpha(0);
+ lp.cosmetics.nameText.gameObject.SetActive(false);
+ }
+
+ killButton.gameObject.SetActive(true);
+ killButton.currentTarget = null;
+ }
+ active = true;
+ }
+
+ if (inside && active && lp.PlayerId == shadeId && lp.Data.Role is Shade)
+ {
+ if (mode is ShadeOptions.ShadowMode.KillEnabled or ShadeOptions.ShadowMode.Both)
+ {
+ var list = new Il2CppSystem.Collections.Generic.List();
+ lp.Data.Role.GetPlayersInAbilityRangeSorted(list);
+ var players = list.ToArray().Where(p => p.PlayerId != lp.PlayerId && !p.Data.IsDead).ToList();
+ var closest = players.Count > 0 ? players[0] : null;
+
+ if (killButton.currentTarget && killButton.currentTarget != closest)
+ killButton.currentTarget.ToggleHighlight(false, RoleTeamTypes.Impostor);
+
+ killButton.currentTarget = closest;
+ if (closest != null)
+ closest.ToggleHighlight(true, RoleTeamTypes.Impostor);
+ }
+ }
+ else if (!inside && active)
+ {
+ Coroutines.Start(CoroutinesHelper.RemoveCameraEffect(cam, 0f));
+ if (lp.PlayerId == shadeId)
+ {
+ lp.cosmetics.SetPhantomRoleAlpha(1);
+ lp.cosmetics.nameText.gameObject.SetActive(true);
+
+ if (killButton.currentTarget)
+ {
+ killButton.currentTarget.ToggleHighlight(false, RoleTeamTypes.Impostor);
+ killButton.currentTarget = null;
+ }
+ killButton.gameObject.SetActive(false);
+ }
+ active = false;
+ }
+ }
+
+ public static ShadowZone Create(byte id, Vector2 pos, float r, float dur)
+ {
+ var go = new GameObject("ShadowZone");
+ var z = go.AddComponent();
+ z.shadeId = id;
+ z.radius = r;
+ z.duration = dur;
+ go.transform.position = pos;
+ return z;
+ }
+
+ [MethodRpc((uint)CustomRPC.DeployZone)]
+ public static void RpcDeployZone(PlayerControl source, Vector2 pos, float radius, float duration)
+ {
+ Create(source.PlayerId, pos, radius, duration);
+ }
+
+ public static bool IsInsideAny(Vector2 pos)
+ {
+ return zones.Any(z => z && z.Contains(pos));
+ }
+ }
+}
diff --git a/NewMod/CustomRPC.cs b/NewMod/CustomRPC.cs
index db951e5..88d1919 100644
--- a/NewMod/CustomRPC.cs
+++ b/NewMod/CustomRPC.cs
@@ -14,5 +14,7 @@ public enum CustomRPC
SuppressionDome,
WitnessTrap,
NotifyChampion,
- SummonNPC
+ SummonNPC,
+ BeaconPulse,
+ DeployZone
}
\ No newline at end of file
diff --git a/NewMod/DebugWindow.cs b/NewMod/DebugWindow.cs
index 8bf96cb..9afd784 100644
--- a/NewMod/DebugWindow.cs
+++ b/NewMod/DebugWindow.cs
@@ -1,5 +1,4 @@
using UnityEngine;
-using System.Linq;
using System;
using MiraAPI.Hud;
using MiraAPI.Modifiers;
@@ -18,126 +17,163 @@
namespace NewMod
{
- [RegisterInIl2Cpp]
- public class DebugWindow(nint ptr) : MonoBehaviour(ptr)
- {
- [HideFromIl2Cpp] public bool EnableDebugger { get; set; } = false;
- public float Zoom = 3f;
- public const float ZoomMin = 3f;
- public const float ZoomMax = 15f;
- public bool ScrollZoomWhileOpen = true;
- public static DebugWindow Instance;
-
- public void ApplyZoom(float size)
- {
- size = Mathf.Clamp(size, ZoomMin, ZoomMax);
- if (Camera.main) Camera.main.orthographicSize = size;
- foreach (var cam in Camera.allCameras) if (cam) cam.orthographicSize = size;
- ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen);
- if (HudManager.Instance && HudManager.Instance.ShadowQuad)
- {
- bool zoomingOut = size > 3f;
- HudManager.Instance.ShadowQuad.gameObject.SetActive(!zoomingOut);
- }
- }
-
- private static bool AllowDebug()
- {
- return AmongUsClient.Instance && AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay;
- }
-
- public readonly DragWindow DebuggingWindow = new(new Rect(10, 10, 0, 0), "NewMod Debug Window", () =>
- {
- bool allow = AllowDebug();
-
- GUILayout.BeginVertical(GUI.skin.box);
- GUILayout.Label("Camera Zoom");
- var newZoom = GUILayout.HorizontalSlider(Instance.Zoom, ZoomMin, ZoomMax, GUILayout.Width(220f));
- GUILayout.BeginHorizontal();
- if (GUILayout.Button("-", GUILayout.Width(28f)) && allow) newZoom = Mathf.Clamp(Instance.Zoom / 1.25f, ZoomMin, ZoomMax);
- if (GUILayout.Button("+", GUILayout.Width(28f)) && allow) newZoom = Mathf.Clamp(Instance.Zoom * 1.25f, ZoomMin, ZoomMax);
- if (GUILayout.Button("Reset", GUILayout.Width(64f)) && allow) newZoom = 3f;
- Instance.ScrollZoomWhileOpen = GUILayout.Toggle(Instance.ScrollZoomWhileOpen, "Scroll-wheel zoom");
- GUILayout.EndHorizontal();
- if (!Mathf.Approximately(newZoom, Instance.Zoom) && allow)
- {
- Instance.Zoom = newZoom;
- Instance.ApplyZoom(Instance.Zoom);
- }
- GUILayout.Label($"Size: {Instance.Zoom:0.00}");
- GUILayout.EndVertical();
-
- GUILayout.Space(6);
-
- if (GUILayout.Button("Become Explosive Modifier") && allow) PlayerControl.LocalPlayer.RpcAddModifier();
- if (GUILayout.Button("Remove Explosive Modifier") && allow) PlayerControl.LocalPlayer.RpcRemoveModifier();
- if (GUILayout.Button("Become Necromancer") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
- if (GUILayout.Button("Become DoubleAgent") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
- if (GUILayout.Button("Become EnergyThief") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
- if (GUILayout.Button("Become SpecialAgent") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
- if (GUILayout.Button("Force Start Game") && allow) if (GameOptionsManager.Instance.CurrentGameOptions.NumImpostors is not 1) AmongUsClient.Instance.StartGame();
- if (GUILayout.Button("Increases Uses by 3") && allow) foreach (var button in CustomButtonManager.Buttons) button.SetUses(3);
- if (GUILayout.Button("Randomly Cast a Vote") && allow && MeetingHud.Instance)
- {
- var randPlayer = Utils.GetRandomPlayer(p => !p.Data.IsDead && !p.Data.Disconnected);
- MeetingHud.Instance.CmdCastVote(PlayerControl.LocalPlayer.PlayerId, randPlayer.PlayerId);
- }
- if (GUILayout.Button("End Meeting") && allow && MeetingHud.Instance)
- {
- MeetingHud.Instance.Close();
- }
- if (GUILayout.Button("Apply Glitch Effect to Main Camera") && allow)
- {
- Camera.main.gameObject.AddComponent();
- }
- if (GUILayout.Button("Apply Earthquake Effect to Main Camera") && allow)
- {
- Camera.main.gameObject.AddComponent();
- }
- if (GUILayout.Button("Apply Slow Hue Pulse Effect to Main Camera") && allow)
- {
- Camera.main.gameObject.AddComponent();
- }
- if (GUILayout.Button("Reset Camera Effects") && allow)
- {
- Coroutines.Start(CoroutinesHelper.RemoveCameraEffect(Camera.main, 1f));
- }
- if (GUILayout.Button("Show Toast") && LobbyBehaviour.Instance)
- {
- var toast = Toast.CreateToast();
- toast.ShowToast(string.Empty, "NewMod v1.2.6", Color.red, 5f);
- }
- /*if (GUILayout.Button("Spawn General NPC") && allow)
- {
- var npc = new GameObject("GeneralNPC").AddComponent();
- npc.Initialize(PlayerControl.LocalPlayer);
- }*/
- });
-
- public void OnGUI()
- {
- if (EnableDebugger) DebuggingWindow.OnGUI();
- }
-
- public void Start()
- {
- Instance = this;
- if (Camera.main) Zoom = Mathf.Clamp(Camera.main.orthographicSize, ZoomMin, ZoomMax);
- }
-
- public void Update()
- {
- if (Input.GetKeyDown(KeyCode.F3)) EnableDebugger = !EnableDebugger;
- if (EnableDebugger && ScrollZoomWhileOpen && AllowDebug())
- {
- float wheel = Input.GetAxis("Mouse ScrollWheel");
- if (Mathf.Abs(wheel) > 0.0001f)
+ [RegisterInIl2Cpp]
+ public class DebugWindow(nint ptr) : MonoBehaviour(ptr)
+ {
+ [HideFromIl2Cpp] public bool EnableDebugger { get; set; } = false;
+ public float Zoom = 3f;
+ public const float ZoomMin = 3f;
+ public const float ZoomMax = 15f;
+ public bool ScrollZoomWhileOpen = true;
+ public static DebugWindow Instance;
+ public int tab;
+
+ //ShadowFlux
+ public float noiseScale = 2f;
+ public float fluxSpeed = 0.3f;
+ public float edgeWidth = 0.25f;
+ public float threshold = 0.55f;
+ public float opacity = 0.75f;
+ public float darkness = 0.8f;
+
+ //DistorationWave
+ public float amplitude = 0.05f;
+ public float frequency = 5f;
+ public float distoSpeed = 1f;
+ public float radius = 0.25f;
+ public float falloff = 1f;
+
+ public void ApplyZoom(float size)
+ {
+ size = Mathf.Clamp(size, ZoomMin, ZoomMax);
+ if (Camera.main) Camera.main.orthographicSize = size;
+ foreach (var cam in Camera.allCameras) if (cam) cam.orthographicSize = size;
+ ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen);
+ if (HudManager.Instance && HudManager.Instance.ShadowQuad)
+ HudManager.Instance.ShadowQuad.gameObject.SetActive(size <= 3f);
+ }
+
+ public static bool AllowDebug() => AmongUsClient.Instance && AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay;
+ public readonly DragWindow DebuggingWindow = new(new Rect(10, 10, 0, 0), "NewMod Debug Window", () =>
+ {
+ bool allow = AllowDebug();
+
+ GUILayout.BeginHorizontal();
+ if (GUILayout.Toggle(Instance.tab == 0, "Game", GUI.skin.button)) Instance.tab = 0;
+ if (GUILayout.Toggle(Instance.tab == 1, "Effects", GUI.skin.button)) Instance.tab = 1;
+ GUILayout.EndHorizontal();
+
+ if (Instance.tab == 0)
+ {
+ GUILayout.BeginVertical(GUI.skin.box);
+ GUILayout.Label("Camera Zoom");
+ var newZoom = GUILayout.HorizontalSlider(Instance.Zoom, ZoomMin, ZoomMax, GUILayout.Width(220f));
+ if (!Mathf.Approximately(newZoom, Instance.Zoom) && allow)
+ {
+ Instance.Zoom = newZoom;
+ Instance.ApplyZoom(Instance.Zoom);
+ }
+ GUILayout.Label($"Size: {Instance.Zoom:0.00}");
+ GUILayout.EndVertical();
+
+ GUILayout.Space(6);
+
+ if (GUILayout.Button("Become Necromancer") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ if (GUILayout.Button("Become DoubleAgent") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ if (GUILayout.Button("Become EnergyThief") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ if (GUILayout.Button("Become SpecialAgent") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ if (GUILayout.Button("Increase Uses by 3") && allow) foreach (var b in CustomButtonManager.Buttons) b.SetUses(3);
+ if (GUILayout.Button("Random Vote") && allow && MeetingHud.Instance)
+ {
+ var p = Utils.GetRandomPlayer(x => !x.Data.IsDead && !x.Data.Disconnected);
+ MeetingHud.Instance.CmdCastVote(PlayerControl.LocalPlayer.PlayerId, p.PlayerId);
+ }
+ if (GUILayout.Button("End Meeting") && allow && MeetingHud.Instance) MeetingHud.Instance.Close();
+ if (GUILayout.Button("Apply Glitch Effect") && allow) Camera.main.gameObject.AddComponent();
+ if (GUILayout.Button("Apply Earthquake Effect") && allow) Camera.main.gameObject.AddComponent();
+ if (GUILayout.Button("Apply PulseHue Effect") && allow) Camera.main.gameObject.AddComponent();
+ if (GUILayout.Button("Apply DistortionWave Effect") && allow) Camera.main.gameObject.AddComponent();
+ if (GUILayout.Button("Apply ShadowFlux Effect") && allow) Camera.main.gameObject.AddComponent();
+ if (GUILayout.Button("Reset Effects") && allow) Coroutines.Start(CoroutinesHelper.RemoveCameraEffect(Camera.main, 1f));
+ }
+
+ if (Instance.tab == 1)
+ {
+ var cam = Camera.main;
+
+ if (cam.gameObject.TryGetComponent(out ShadowFluxEffect flux) && flux._mat)
+ {
+ GUILayout.Label("ShadowFlux");
+ Instance.noiseScale = Slider("NoiseScale", Instance.noiseScale, 0f, 5f);
+ Instance.fluxSpeed = Slider("Speed", Instance.fluxSpeed, 0f, 3f);
+ Instance.edgeWidth = Slider("EdgeWidth", Instance.edgeWidth, 0f, 1f);
+ Instance.threshold = Slider("Threshold", Instance.threshold, 0f, 1f);
+ Instance.opacity = Slider("Opacity", Instance.opacity, 0f, 1f);
+ Instance.darkness = Slider("Darkness", Instance.darkness, 0f, 1f);
+
+ var m = flux._mat;
+ m.SetFloat("_NoiseScale", Instance.noiseScale);
+ m.SetFloat("_Speed", Instance.fluxSpeed);
+ m.SetFloat("_EdgeWidth", Instance.edgeWidth);
+ m.SetFloat("_Threshold", Instance.threshold);
+ m.SetFloat("_Opacity", Instance.opacity);
+ m.SetFloat("_Darkness", Instance.darkness);
+ }
+
+ GUILayout.Space(8);
+
+ if (cam.gameObject.TryGetComponent(out DistorationWaveEffect disto) && disto._mat)
+ {
+ GUILayout.Label("DistortionWave");
+ Instance.amplitude = Slider("Amplitude", Instance.amplitude, 0f, 1f);
+ Instance.frequency = Slider("Frequency", Instance.frequency, 0f, 10f);
+ Instance.distoSpeed = Slider("Speed", Instance.distoSpeed, 0f, 5f);
+ Instance.radius = Slider("Radius", Instance.radius, 0f, 1f);
+ Instance.falloff = Slider("Falloff", Instance.falloff, 0f, 5f);
+
+ var m = disto._mat;
+ m.SetFloat("_Amplitude", Instance.amplitude);
+ m.SetFloat("_Frequency", Instance.frequency);
+ m.SetFloat("_Speed", Instance.distoSpeed);
+ m.SetFloat("_Radius", Instance.radius);
+ m.SetFloat("_Falloff", Instance.falloff);
+ }
+ }
+ });
+
+ public static float Slider(string name, float value, float min, float max)
+ {
+ GUILayout.BeginHorizontal();
+ GUILayout.Label(name, GUILayout.Width(100f));
+ value = GUILayout.HorizontalSlider(value, min, max, GUILayout.Width(200f));
+ GUILayout.EndHorizontal();
+ return value;
+ }
+
+ public void OnGUI()
+ {
+ if (EnableDebugger) DebuggingWindow.OnGUI();
+ }
+
+ public void Start()
+ {
+ Instance = this;
+ if (Camera.main) Zoom = Mathf.Clamp(Camera.main.orthographicSize, ZoomMin, ZoomMax);
+ }
+
+ public void Update()
+ {
+ if (Input.GetKeyDown(KeyCode.F3)) EnableDebugger = !EnableDebugger;
+ if (EnableDebugger && ScrollZoomWhileOpen && AllowDebug())
{
- var factor = wheel > 0 ? 1f / 1.25f : 1.25f;
- Zoom = Mathf.Clamp(Zoom * factor, ZoomMin, ZoomMax);
- ApplyZoom(Zoom);
+ float wheel = Input.GetAxis("Mouse ScrollWheel");
+ if (Mathf.Abs(wheel) > 0.0001f)
+ {
+ var factor = wheel > 0 ? 1f / 1.25f : 1.25f;
+ Zoom = Mathf.Clamp(Zoom * factor, ZoomMin, ZoomMax);
+ ApplyZoom(Zoom);
+ }
}
- }
- }
- }
+ }
+ }
}
diff --git a/NewMod/DiscordStatus.cs b/NewMod/DiscordStatus.cs
index c392733..68d7854 100644
--- a/NewMod/DiscordStatus.cs
+++ b/NewMod/DiscordStatus.cs
@@ -6,7 +6,7 @@
using UnityEngine;
using UnityEngine.SceneManagement;
-namespace NewMod.Patches
+namespace NewMod
{
[HarmonyPatch]
public static class NewModDiscordPatch
@@ -18,9 +18,8 @@ public static class NewModDiscordPatch
[HarmonyPatch(typeof(DiscordManager), nameof(DiscordManager.Start))]
public static bool StartPrefix(DiscordManager __instance)
{
-#if ANDROID
- return true;
-#else
+ if (Application.platform == RuntimePlatform.Android) return true;
+
const long clientId = 1405946628115791933;
discord = new Discord.Discord(clientId, (ulong)CreateFlags.Default);
@@ -37,13 +36,13 @@ public static bool StartPrefix(DiscordManager __instance)
__instance.SetInMenus();
return false;
-#endif
}
[HarmonyPrefix]
[HarmonyPatch(typeof(ActivityManager), nameof(ActivityManager.UpdateActivity))]
public static void UpdateActivityPrefix([HarmonyArgument(0)] ref Activity activity)
{
+ if (Application.platform == RuntimePlatform.Android) return;
if (activity == null) return;
var isBeta = false;
diff --git a/NewMod/LocalSettings/NewModLocalSettings.cs b/NewMod/LocalSettings/NewModLocalSettings.cs
index 387378d..3d52b8c 100644
--- a/NewMod/LocalSettings/NewModLocalSettings.cs
+++ b/NewMod/LocalSettings/NewModLocalSettings.cs
@@ -1,6 +1,8 @@
using BepInEx.Configuration;
+using MiraAPI.Hud;
using MiraAPI.LocalSettings;
using MiraAPI.LocalSettings.Attributes;
+using NewMod.Patches;
using UnityEngine;
namespace NewMod.LocalSettings
@@ -23,6 +25,22 @@ public class NewModLocalSettings(ConfigFile config) : LocalSettingsTab(config)
"Frames per second limit"
);
+ [LocalToggleSetting("Enable Custom Cursor", "Enable the custom cursor from the birthday update")]
+ public ConfigEntry EnableCustomCursor { get; } = config.Bind(
+ "Features",
+ "Cursor",
+ true,
+ "Enable the custom cursor from the birthday update"
+ );
+
+ [LocalToggleSetting("Always Buttons on Left (Only for Android)", "Places all custom buttons on the left side (recommended for Android devices)")]
+ public ConfigEntry AlwaysButtonsLeft { get; } = config.Bind(
+ "Features",
+ "AlwaysButtonsLeft",
+ true,
+ "Place all custom buttons on the left side"
+ );
+
public override void OnOptionChanged(ConfigEntryBase configEntry)
{
base.OnOptionChanged(configEntry);
@@ -31,6 +49,36 @@ public override void OnOptionChanged(ConfigEntryBase configEntry)
{
Application.targetFrameRate = (int)FrameRateLimit.Value;
}
+
+ if (configEntry == AlwaysButtonsLeft && Application.platform == RuntimePlatform.Android)
+ {
+ foreach (var btn in CustomButtonManager.Buttons)
+ {
+ btn.SetButtonLocation(ButtonLocation.BottomLeft);
+ }
+ }
+ if (configEntry == EnableCustomCursor)
+ {
+ if (EnableCustomCursor.Value)
+ {
+ var cur = NewModAsset.CustomCursor.LoadAsset();
+ var tex = cur?.texture;
+
+ if (tex != null)
+ {
+ Cursor.SetCursor(tex, Vector2.zero, CursorMode.Auto);
+ Cursor.visible = true;
+ Cursor.lockState = CursorLockMode.None;
+ MainMenuPatch._cachedCursor = tex;
+ }
+ }
+ else
+ {
+ Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
+ Cursor.visible = true;
+ Cursor.lockState = CursorLockMode.None;
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/NewMod/Modifiers/AdrenalineModifier.cs b/NewMod/Modifiers/AdrenalineModifier.cs
index 18a71ee..a994cc6 100644
--- a/NewMod/Modifiers/AdrenalineModifier.cs
+++ b/NewMod/Modifiers/AdrenalineModifier.cs
@@ -11,7 +11,7 @@ public class AdrenalineModifier : GameModifier
public override bool HideOnUi => false;
public override int GetAssignmentChance() => (int)OptionGroupSingleton.Instance.AdrenalineChance.Value;
public override int GetAmountPerGame() => (int)OptionGroupSingleton.Instance.AdrenalineAmount;
-
+ public override string GetDescription() => $"Move faster (x{OptionGroupSingleton.Instance.SpeedMultiplier:0.##}).";
public override void OnActivate()
{
Player.MyPhysics.Speed *= OptionGroupSingleton.Instance.SpeedMultiplier;
diff --git a/NewMod/Modifiers/DrowsyModifier.cs b/NewMod/Modifiers/DrowsyModifier.cs
index c48b1b7..d6c98af 100644
--- a/NewMod/Modifiers/DrowsyModifier.cs
+++ b/NewMod/Modifiers/DrowsyModifier.cs
@@ -14,7 +14,7 @@ public sealed class DrowsyModifier : GameModifier
public override bool ShowInFreeplay => true;
public override Color FreeplayFileColor => new(0.6f, 0.7f, 1f);
public override string GetDescription() =>
- $"Move slower (x{OptionGroupSingleton.Instance.DrowsyAmount:0.##}).";
+ $"Move slower (x{OptionGroupSingleton.Instance.SpeedMultiplier:0.##}).";
public override int GetAssignmentChance() => (int)OptionGroupSingleton.Instance.DrowsyChance.Value;
diff --git a/NewMod/Modifiers/FalseFormModifier.cs b/NewMod/Modifiers/FalseFormModifier.cs
index 860d1bf..0a0582d 100644
--- a/NewMod/Modifiers/FalseFormModifier.cs
+++ b/NewMod/Modifiers/FalseFormModifier.cs
@@ -7,21 +7,14 @@
namespace NewMod.Modifiers
{
- public class FalseFormModifier : GameModifier
+ public class FalseFormModifier : TimedModifier
{
public override string ModifierName => "FalseForm";
public override bool ShowInFreeplay => true;
public override bool HideOnUi => false;
+ public override float Duration => (int)OptionGroupSingleton.Instance.FalseFormDuration;
private float timer;
private AppearanceBackup oldAppearance;
- public override int GetAssignmentChance()
- {
- return OptionGroupSingleton.Instance.FalseFormChance;
- }
- public override int GetAmountPerGame()
- {
- return (int)OptionGroupSingleton.Instance.FalseFormAmount;
- }
public override void OnActivate()
{
oldAppearance = new AppearanceBackup
diff --git a/NewMod/Networking/BeaconPulseRpc.cs b/NewMod/Networking/BeaconPulseRpc.cs
new file mode 100644
index 0000000..8503c1c
--- /dev/null
+++ b/NewMod/Networking/BeaconPulseRpc.cs
@@ -0,0 +1,43 @@
+using Hazel;
+using NewMod.Components.ScreenEffects;
+using NewMod.Roles.CrewmateRoles;
+using NewMod.Utilities;
+using Reactor.Networking.Attributes;
+using Reactor.Networking.Rpc;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace NewMod.Networking
+{
+ [RegisterCustomRpc((uint)CustomRPC.BeaconPulse)]
+ public class BeaconPulseRpc : PlayerCustomRpc
+ {
+ public BeaconPulseRpc(NewMod plugin, uint id) : base(plugin, id) { }
+
+ public readonly record struct Data(float Duration);
+
+ public override RpcLocalHandling LocalHandling => RpcLocalHandling.After;
+
+ public override void Write(MessageWriter writer, Data data)
+ {
+ writer.Write(data.Duration);
+ }
+
+ public override Data Read(MessageReader reader)
+ {
+ return new Data(reader.ReadSingle());
+ }
+
+ public override void Handle(PlayerControl sender, Data data)
+ {
+ var cam = Camera.main;
+ if (cam && !cam.GetComponent())
+ {
+ cam.gameObject.AddComponent();
+ }
+ Beacon.pulseUntil = Time.time + data.Duration;
+
+ Logger.Instance.LogMessage($"Beacon pulse triggered by {sender.Data.PlayerName} for {data.Duration}s");
+ }
+ }
+}
diff --git a/NewMod/NewMod.cs b/NewMod/NewMod.cs
index 15c6060..681d7f9 100644
--- a/NewMod/NewMod.cs
+++ b/NewMod/NewMod.cs
@@ -37,11 +37,11 @@ namespace NewMod;
public partial class NewMod : BasePlugin, IMiraPlugin
{
public const string Id = "com.callofcreator.newmod";
- public const string ModVersion = "1.2.7";
+ public const string ModVersion = "1.2.8";
public Harmony Harmony { get; } = new Harmony(Id);
public static BasePlugin Instance;
public static Minigame minigame;
- public const string SupportedAmongUsVersion = "2025.9.9";
+ public const string SupportedAmongUsVersion = "2025.10.14";
public static ConfigEntry ShouldEnableBepInExConsole { get; set; }
public ConfigFile GetConfigFile() => Config;
public string OptionsTitleText => "NewMod";
@@ -62,6 +62,16 @@ public override void Load()
ShouldEnableBepInExConsole = Config.Bind("NewMod", "Console", true, "Whether to enable BepInEx Console for debugging");
if (!ShouldEnableBepInExConsole.Value) ConsoleManager.DetachConsole();
+ var bundle = NewModAsset.Bundle;
+ var assetNames = bundle.GetAllAssetNames();
+
+ Instance.Log.LogMessage($"AssetBundle '{bundle.name}' contains {assetNames.Length} assets");
+
+ foreach (var name in assetNames)
+ {
+ Instance.Log.LogMessage($"{name}");
+ }
+
Instance.Log.LogMessage($"Loaded Successfully NewMod v{ModVersion} With MiraAPI Version : {MiraApiPlugin.Version}");
}
public static void CheckVersionCompatibility()
diff --git a/NewMod/NewMod.csproj b/NewMod/NewMod.csproj
index fcfa021..16f74ed 100644
--- a/NewMod/NewMod.csproj
+++ b/NewMod/NewMod.csproj
@@ -1,6 +1,6 @@
- 1.2.7
+ 1.2.8
dev
NewMod is a mod for Among Us that introduces a variety of new roles, unique abilities
CallofCreator
@@ -15,25 +15,19 @@
- $(RestoreSources);$(MSBuildProjectDirectory)\..\libs\Android
TRACE;ANDROID_BUILD
-
-
+
+
+
-
-
- ..\libs\MiraAPI.dll
-
-
-
diff --git a/NewMod/NewModAsset.cs b/NewMod/NewModAsset.cs
index 2636ef6..0e92036 100644
--- a/NewMod/NewModAsset.cs
+++ b/NewMod/NewModAsset.cs
@@ -41,6 +41,7 @@ public static class NewModAsset
public static LoadableResourceAsset CallWraith { get; } = new("NewMod.Resources.callwraith.png");
public static LoadableResourceAsset Shield { get; } = new("NewMod.Resources.Shield.png");
public static LoadableResourceAsset Slash { get; } = new("NewMod.Resources.Slash.png");
+ public static LoadableResourceAsset DeployZone { get; } = new("NewMod.Resources.deployzone.png");
// SFX
public static LoadableAudioResourceAsset ReviveSound { get; } = new("NewMod.Resources.Sounds.revive.wav");
@@ -61,6 +62,7 @@ public static class NewModAsset
public static LoadableResourceAsset ShieldIcon { get; } = new("NewMod.Resources.RoleIcons.ShieldIcon.png");
public static LoadableResourceAsset RadarIcon { get; } = new("NewMod.Resources.RoleIcons.RadarIcon.png");
public static LoadableResourceAsset SlashIcon { get; } = new("NewMod.Resources.RoleIcons.SlashIcon.png");
+ public static LoadableResourceAsset DeployZoneIcon { get; } = new("NewMod.Resources.RoleIcons.DeployzoneIcon.png");
// Notif Icons
public static LoadableResourceAsset VisionDebuff { get; } = new("NewMod.Resources.NotifIcons.vision_debuff.png");
@@ -71,4 +73,9 @@ public static class NewModAsset
public static LoadableAsset GlitchShader { get; } = new LoadableBundleAsset("GlitchFullScreen.shader", Bundle);
public static LoadableAsset EarthquakeShader { get; } = new LoadableBundleAsset("EarthquakeFullScreen.shader", Bundle);
public static LoadableAsset SlowPulseHueShader { get; } = new LoadableBundleAsset("SlowPulseHue.shader", Bundle);
+ public static LoadableAsset DistorationWaveShader { get; } = new LoadableBundleAsset("DistorationWave.shader", Bundle);
+ public static LoadableAsset ShadowFluxShader { get; } = new LoadableBundleAsset("ShadowFlux.shader", Bundle);
+
+ // Textures
+ public static LoadableAsset NoiseTex { get; } = new LoadableBundleAsset("noise.png", Bundle);
}
\ No newline at end of file
diff --git a/NewMod/NewModEndReasons.cs b/NewMod/NewModEndReasons.cs
index 06443ea..fea1f8e 100644
--- a/NewMod/NewModEndReasons.cs
+++ b/NewMod/NewModEndReasons.cs
@@ -12,6 +12,7 @@ public enum NewModEndReasons
InjectorWin = 117,
PulseBladeWin = 118,
TyrantWin = 119,
- WraithCallerWin = 120
+ WraithCallerWin = 120,
+ ShadeWin = 121
}
}
\ No newline at end of file
diff --git a/NewMod/Options/ModifiersOptions.cs b/NewMod/Options/ModifiersOptions.cs
index f4aae1f..6bccf5a 100644
--- a/NewMod/Options/ModifiersOptions.cs
+++ b/NewMod/Options/ModifiersOptions.cs
@@ -20,14 +20,6 @@ public class ModifiersOptions : AbstractOptionGroup
Visible = () => OptionGroupSingleton.Instance.StickyAmount > 0
};
- [ModdedNumberOption("FalseForm Amount", min:0, max:6)]
- public float FalseFormAmount { get; set; } = 10f;
-
- public ModdedNumberOption FalseFormChance { get; } = new("FalseForm Chance", 50f, 0, 100f, 10f, MiraNumberSuffixes.Percent)
- {
- Visible = () => OptionGroupSingleton.Instance.FalseFormAmount > 0
- };
-
[ModdedNumberOption("Drowsy Amount", min:0f, max:6f)]
public float DrowsyAmount { get; set; } = 10f;
diff --git a/NewMod/Options/Roles/ShadeOptions.cs b/NewMod/Options/Roles/ShadeOptions.cs
new file mode 100644
index 0000000..23d5856
--- /dev/null
+++ b/NewMod/Options/Roles/ShadeOptions.cs
@@ -0,0 +1,37 @@
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.Attributes;
+using MiraAPI.Utilities;
+using NewMod.Roles.NeutralRoles;
+
+namespace NewMod.Options.Roles.ShadeOptions
+{
+ public class ShadeOptions : AbstractOptionGroup
+ {
+ public override string GroupName => "Shade Options";
+
+ [ModdedNumberOption("Shadow Cooldown", min: 5f, max: 60f, suffixType: MiraNumberSuffixes.Seconds)]
+ public float Cooldown { get; set; } = 25f;
+
+ [ModdedNumberOption("Max Shadow Uses", min: 1f, max: 5f, suffixType: MiraNumberSuffixes.None)]
+ public float MaxUses { get; set; } = 2f;
+
+ [ModdedNumberOption("Shadow Duration", min: 5f, max: 40f, suffixType: MiraNumberSuffixes.Seconds)]
+ public float Duration { get; set; } = 20f;
+
+ [ModdedNumberOption("Shadow Radius", min: 1f, max: 6f, suffixType: MiraNumberSuffixes.None)]
+ public float Radius { get; set; } = 3f;
+
+ [ModdedNumberOption("Required Kills To Win", min: 1f, max: 5f, suffixType: MiraNumberSuffixes.None)]
+ public float RequiredKills { get; set; } = 3f;
+
+ [ModdedEnumOption("Shadow Behavior", typeof(ShadowMode))]
+ public ShadowMode Behavior { get; set; } = ShadowMode.Invisible;
+
+ public enum ShadowMode
+ {
+ Invisible,
+ KillEnabled,
+ Both
+ }
+ }
+}
diff --git a/NewMod/Patches/EndGamePatch.cs b/NewMod/Patches/EndGamePatch.cs
index 3ca6b59..c42a5f4 100644
--- a/NewMod/Patches/EndGamePatch.cs
+++ b/NewMod/Patches/EndGamePatch.cs
@@ -19,6 +19,7 @@
using MiraAPI.Utilities;
using NewMod.Options.Roles.EnergyThiefOptions;
using NewMod.Options.Roles.WraithCallerOptions;
+using NewMod.Options.Roles.ShadeOptions;
namespace NewMod.Patches
{
@@ -131,6 +132,11 @@ public static void OnGameEnd(GameEndEvent evt)
customWinColor = GetRoleColor(GetRoleType());
endGameManager.BackgroundBar.material.SetColor("_Color", customWinColor);
break;
+ case (GameOverReason)NewModEndReasons.ShadeWin:
+ customWinText = "Darkness Consumes All";
+ customWinColor = GetRoleColor(GetRoleType());
+ endGameManager.BackgroundBar.material.SetColor("_Color", customWinColor);
+ break;
default:
customWinText = string.Empty;
customWinColor = Color.white;
@@ -218,6 +224,7 @@ public static bool Prefix(ShipStatus __instance)
if (DestroyableSingleton.InstanceExists) return true;
if (Time.timeSinceLevelLoad < 2f) return true;
if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.WraithCallerWin)) return false;
+ if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.ShadeWin)) return false;
if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.PulseBladeWin)) return false;
if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.TyrantWin)) return false;
if (CheckEndGameForRole(__instance, (GameOverReason)NewModEndReasons.DoubleAgentWin)) return false;
@@ -277,6 +284,12 @@ public static bool CheckForEndGameFaction(ShipStatus __instance, GameO
int current = WraithCallerUtilities.GetKillsNPC(player.PlayerId);
shouldEndGame = current >= required;
}
+ if (typeof(TFaction) == typeof(Shade))
+ {
+ Shade.ShadeKills.TryGetValue(player.PlayerId, out var count);
+ int required = (int)OptionGroupSingleton.Instance.RequiredKills;
+ shouldEndGame = count >= required;
+ }
if (shouldEndGame)
{
GameManager.Instance.RpcEndGame(winReason, false);
diff --git a/NewMod/Patches/HudPatch.cs b/NewMod/Patches/HudPatch.cs
new file mode 100644
index 0000000..4bc3769
--- /dev/null
+++ b/NewMod/Patches/HudPatch.cs
@@ -0,0 +1,15 @@
+using HarmonyLib;
+using NewMod.Components.ScreenEffects;
+
+namespace NewMod.Patches
+{
+ [HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))]
+ public static class HudPatch
+ {
+ public static void Postfix(HudManager __instance)
+ {
+ // For some reason the effect gets destroyed, so the best way to keep it persistent is to reassign it here
+ ShadowFluxEffect._shader = NewModAsset.ShadowFluxShader.LoadAsset();
+ }
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Patches/MainMenuPatch.cs b/NewMod/Patches/MainMenuPatch.cs
index 8684b7c..0e62cb4 100644
--- a/NewMod/Patches/MainMenuPatch.cs
+++ b/NewMod/Patches/MainMenuPatch.cs
@@ -9,6 +9,8 @@
using System.Collections.Generic;
using System.Reflection;
using System;
+using NewMod.LocalSettings;
+using MiraAPI.LocalSettings;
namespace NewMod.Patches
{
@@ -31,7 +33,7 @@ public static void StartPostfix(MainMenuManager __instance)
var cur = NewModAsset.CustomCursor.LoadAsset();
_cachedCursor = cur != null ? cur.texture : null;
}
- if (_cachedCursor != null)
+ if (_cachedCursor != null && LocalSettingsTabSingleton.Instance.EnableCustomCursor.Value)
{
Cursor.SetCursor(_cachedCursor, Vector2.zero, CursorMode.Auto);
}
diff --git a/NewMod/Patches/RolePatch.cs b/NewMod/Patches/RolePatch.cs
new file mode 100644
index 0000000..64f4099
--- /dev/null
+++ b/NewMod/Patches/RolePatch.cs
@@ -0,0 +1,242 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AmongUs.GameOptions;
+using HarmonyLib;
+using MiraAPI.GameOptions;
+using MiraAPI.Roles;
+using MiraAPI.Utilities;
+using NewMod;
+using NewMod.Options;
+using NewMod.Roles;
+using Reactor.Utilities;
+using UnityEngine;
+using Hazel;
+
+namespace NewMod.Patches
+{
+ [HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SelectRoles))]
+ public static class SelectRolePatch
+ {
+ public static bool Prefix(RoleManager __instance)
+ {
+ if (!AmongUsClient.Instance.AmHost)
+ return true;
+
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: START --------------");
+ Logger.Instance.LogMessage($"Running as host (clientId={AmongUsClient.Instance.ClientId})");
+
+ var opts = OptionGroupSingleton.Instance;
+ int target = Mathf.RoundToInt(opts.TotalNeutrals);
+ Logger.Instance.LogMessage($"Settings -> TotalNeutrals={opts.TotalNeutrals} → target={target}, KeepCrewMajority={opts.KeepCrewMajority}, PreferVariety={opts.PreferVariety}");
+
+ var allPlayers = GameData.Instance.AllPlayers.ToArray()
+ .Where(p => p?.Object && !p.IsDead && !p.Disconnected)
+ .ToList();
+
+ Logger.Instance.LogMessage($"Eligible players: {allPlayers.Count}");
+
+ var neutrals = allPlayers
+ .Where(p => p?.Object?.Data?.Role is ICustomRole cr && cr is INewModRole nm &&
+ (nm.Faction == NewModFaction.Apex || nm.Faction == NewModFaction.Entropy))
+ .Select(p => p.Object)
+ .ToList();
+
+ Logger.Instance.LogMessage($"Currently neutral (Apex/Entropy): {neutrals.Count}");
+
+ if (opts.KeepCrewMajority)
+ {
+ int crewCount = allPlayers.Count(p =>
+ {
+ var rb = p?.Object?.Data?.Role;
+ if (rb == null) return false;
+ return rb is CrewmateRole || (!rb.IsImpostor && rb.TeamType == RoleTeamTypes.Crewmate);
+ });
+
+ int maxAllowed = Math.Max(0, (int)Math.Floor((crewCount - 1) / 2.0));
+ int before = target;
+ target = Math.Min(target, maxAllowed);
+ Logger.Instance.LogMessage($"KeepCrewMajority applied -> crewCount={crewCount}, maxNeutrals={maxAllowed}, adjustedTarget={target} (was {before})");
+ }
+
+ int have = neutrals.Count;
+ if (have == target)
+ {
+ Logger.Instance.LogMessage("No change needed; exiting cleanly.");
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: END (no-op) --------------");
+ return false;
+ }
+
+ if (have > target)
+ {
+ int remove = have - target;
+ Logger.Instance.LogMessage($"Too many neutrals; demoting {remove}");
+ neutrals.Shuffle();
+
+ for (int i = 0; i < remove && i < neutrals.Count; i++)
+ {
+ var ply = neutrals[i];
+ if (ply == null) continue;
+
+ Logger.Instance.LogMessage($"→ Demoting {ply.Data.PlayerName} to Crewmate");
+ ply.RpcSetRole(RoleTypes.Crewmate);
+ }
+
+ Logger.Instance.LogMessage("Demotion complete.");
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: END (demotions) --------------");
+ return false;
+ }
+
+ int need = target - have;
+ Logger.Instance.LogMessage($"Need to assign {need} more neutrals.");
+
+ var crewElig = allPlayers
+ .Where(p =>
+ {
+ var rb = p?.Object?.Data?.Role;
+ if (rb == null) return false;
+ bool isCrew = rb is CrewmateRole || (!rb.IsImpostor && rb.TeamType == RoleTeamTypes.Crewmate);
+ if (!isCrew) return false;
+
+ if (rb is ICustomRole cr && cr is INewModRole nm2)
+ return nm2.Faction != NewModFaction.Apex && nm2.Faction != NewModFaction.Entropy;
+
+ return true;
+ })
+ .Select(p => p.Object)
+ .ToList();
+
+ Logger.Instance.LogMessage($"Crew eligible for neutral conversion: {crewElig.Count}");
+
+ if (crewElig.Count == 0)
+ {
+ Logger.Instance.LogMessage("No eligible crew found; aborting neutral assignment.");
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: END (no candidates) --------------");
+ return false;
+ }
+
+ var active = CustomRoleUtils.GetActiveRoles().ToList();
+ var candidates = new List();
+
+ foreach (var r in CustomRoleManager.CustomMiraRoles)
+ {
+ if (r is not INewModRole nm) continue;
+ if (nm.Faction != NewModFaction.Apex && nm.Faction != NewModFaction.Entropy) continue;
+
+ var roleType = (RoleTypes)RoleId.Get(r.GetType());
+ int already = active.Count(x => x && x.Role == roleType);
+ int left = r.Configuration.MaxRoleCount - already;
+ if (left <= 0) continue;
+
+ int chance = r.GetChance().Value;
+ if (chance <= 0) continue;
+
+ candidates.Add(new Candidate
+ {
+ Role = r,
+ Left = left,
+ Weight = chance,
+ RoleType = roleType
+ });
+ }
+
+ Logger.Instance.LogMessage($"Built neutral candidate list: {candidates.Count}");
+
+ if (candidates.Count == 0)
+ {
+ Logger.Instance.LogMessage("No candidates available; exiting.");
+ return false;
+ }
+
+ var picks = new List();
+ if (opts.PreferVariety)
+ {
+ var ordered = candidates.OrderByDescending(x => x.Weight).ToList();
+ for (int i = 0; i < ordered.Count && picks.Count < need; i++)
+ {
+ var c = ordered[i];
+ if (c.Left <= 0) continue;
+ picks.Add(c.Role);
+ c.Left--;
+ ordered[i] = c;
+ }
+ candidates = ordered;
+ }
+
+ while (picks.Count < need)
+ {
+ var available = candidates.Where(c => c.Left > 0 && c.Weight > 0f).ToList();
+ if (available.Count == 0) break;
+
+ float total = available.Sum(c => c.Weight);
+ float rnum = UnityEngine.Random.Range(0f, total);
+ float acc = 0f;
+ var chosen = available.First();
+
+ foreach (var c in available)
+ {
+ acc += c.Weight;
+ if (rnum <= acc)
+ {
+ chosen = c;
+ break;
+ }
+ }
+
+ picks.Add(chosen.Role);
+
+ int idx = candidates.FindIndex(x => x.RoleType == chosen.RoleType);
+ if (idx >= 0)
+ {
+ var tmp = candidates[idx];
+ tmp.Left--;
+ candidates[idx] = tmp;
+ }
+ }
+
+ Logger.Instance.LogMessage($"Assigning {picks.Count} neutrals...");
+
+ foreach (var role in picks)
+ {
+ if (crewElig.Count == 0) break;
+ int idx = HashRandom.FastNext(crewElig.Count);
+ var pc = crewElig[idx];
+ crewElig.RemoveAt(idx);
+
+ var rt = (RoleTypes)RoleId.Get(role.GetType());
+ Logger.Instance.LogMessage($"Assigning {role.GetType().Name} → {pc.Data.PlayerName}");
+ pc.RpcSetRole(rt);
+ }
+
+ Logger.Instance.LogMessage("Neutral assignment complete.");
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: END --------------");
+ return false;
+ }
+ public struct Candidate
+ {
+ public ICustomRole Role;
+ public int Left;
+ public float Weight;
+ public RoleTypes RoleType;
+ }
+ }
+
+ // Inspired by https://github.com/AU-Avengers/TOU-Mira/blob/main/TownOfUs/Patches/RoleManagerPatches.cs#L747C3-L768C1
+ [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcSetRole))]
+ public static class RpcSetRolePatch
+ {
+ public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] RoleTypes roleType, [HarmonyArgument(1)] bool canOverrideRole = false)
+ {
+ if (AmongUsClient.Instance.AmClient)
+ __instance.StartCoroutine(__instance.CoSetRole(roleType, canOverrideRole));
+
+ var writer = AmongUsClient.Instance.StartRpcImmediately(__instance.NetId, (byte)RpcCalls.SetRole, SendOption.Reliable);
+ writer.Write((ushort)roleType);
+ writer.Write(canOverrideRole);
+ AmongUsClient.Instance.FinishRpcImmediately(writer);
+
+ Logger.Instance.LogMessage($"RpcSetRole: {__instance.Data.PlayerName} ({roleType})");
+ return false;
+ }
+ }
+}
diff --git a/NewMod/Patches/Roles/Beacon/ShowMapPatch.cs b/NewMod/Patches/Roles/Beacon/ShowMapPatch.cs
index 4fb2654..e63fb2b 100644
--- a/NewMod/Patches/Roles/Beacon/ShowMapPatch.cs
+++ b/NewMod/Patches/Roles/Beacon/ShowMapPatch.cs
@@ -3,7 +3,9 @@
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Utilities;
+using NewMod.Components.ScreenEffects;
using NewMod.Options.Roles.BeaconOptions;
+using NewMod.Utilities;
using Reactor.Utilities;
using UnityEngine;
using BC = NewMod.Roles.CrewmateRoles.Beacon;
@@ -73,7 +75,11 @@ public static class BeaconOverlayTintKeeper
static void Postfix(MapCountOverlay __instance)
{
if (PlayerControl.LocalPlayer.Data.Role is not BC) return;
- if (Time.time >= BC.pulseUntil) return;
+ if (Time.time >= BC.pulseUntil)
+ {
+ var effect = Camera.main.GetComponent();
+ Object.Destroy(effect); ;
+ }
var map = MapBehaviour.Instance;
if (!map || !map.IsOpen) return;
diff --git a/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs b/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs
index 7e989b8..08f5635 100644
--- a/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs
+++ b/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs
@@ -1,6 +1,7 @@
using HarmonyLib;
using NewMod.Utilities;
using NewMod.Roles.ImpostorRoles;
+using NewMod.Roles.NeutralRoles;
namespace NewMod.Patches.Roles.EnergyThief;
@@ -17,6 +18,7 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)] EndGam
PranksterUtilities.ResetReportCount();
VisionaryUtilities.DeleteAllScreenshots();
WraithCallerUtilities.ClearAll();
+ Shade.ShadeKills.Clear();
Revenant.HasUsedFeignDeath = false;
Revenant.FeignDeathStates.Remove(PlayerControl.LocalPlayer.PlayerId);
Revenant.StalkingStates[PlayerControl.LocalPlayer.PlayerId] = false;
diff --git a/NewMod/Patches/SelectRolePatch.cs b/NewMod/Patches/SelectRolePatch.cs
deleted file mode 100644
index b41fb8b..0000000
--- a/NewMod/Patches/SelectRolePatch.cs
+++ /dev/null
@@ -1,220 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using AmongUs.GameOptions;
-using HarmonyLib;
-using MiraAPI.GameOptions;
-using MiraAPI.Roles;
-using MiraAPI.Utilities;
-using NewMod;
-using NewMod.Options;
-using NewMod.Roles;
-using Reactor.Utilities;
-using UnityEngine;
-
-namespace NewMod.Patches
-{
- [HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SelectRoles))]
- public static class SelectRolePatch
- {
- public static void Postfix(RoleManager __instance)
- {
- if (!AmongUsClient.Instance.AmHost) return;
-
- Logger.Instance.LogMessage("-------------- SELECT ROLES: START --------------");
- Logger.Instance.LogMessage(
- $"SelectRoles Postfix entered on {(AmongUsClient.Instance.AmHost ? "HOST" : "CLIENT")} (clientId={AmongUsClient.Instance.ClientId})");
-
-
- var opts = OptionGroupSingleton.Instance;
- int target = Mathf.RoundToInt(opts.TotalNeutrals);
- Logger.Instance.LogMessage($"Config -> TotalNeutrals={opts.TotalNeutrals} (target={target}), KeepCrewMajority={opts.KeepCrewMajority}, PreferVariety={opts.PreferVariety}");
-
- var all = GameData.Instance.AllPlayers.ToArray()
- .Where(p => !p.IsDead && !p.Disconnected && p.Object)
- .ToList();
- Logger.Instance.LogMessage($"Alive players eligible (all): {all.Count}");
-
- var neutrals = all.Where(p =>
- {
- var rb = p.Object.Data.Role;
- return rb is ICustomRole cr && cr is INewModRole nm &&
- (nm.Faction == NewModFaction.Apex || nm.Faction == NewModFaction.Entropy);
- }).Select(p => p.Object).ToList();
- Logger.Instance.LogMessage($"Current neutrals (Apex or Entropy): {neutrals.Count}");
-
- if (opts.KeepCrewMajority)
- {
- int crewCount = all.Count(p =>
- {
- var rb = p.Object.Data.Role;
- if (!rb) return false;
- return (rb is CrewmateRole) || (!rb.IsImpostor && rb.TeamType == RoleTeamTypes.Crewmate);
- });
-
- int maxAllowed = Math.Max(0, (int)Math.Floor((crewCount - 1) / 2.0));
- int before = target;
- target = Math.Min(target, maxAllowed);
- Logger.Instance.LogMessage($"KeepCrewMajority -> crewCount={crewCount}, maxAllowedNeutrals={maxAllowed}, target {before} => {target}");
- }
-
- int have = neutrals.Count;
- Logger.Instance.LogMessage($"Neutral count check -> have={have}, target={target}");
-
- if (have == target)
- {
- Logger.Instance.LogMessage("No changes needed, exiting early.");
- Logger.Instance.LogMessage("-------------- SELECT ROLES: END (no-op) --------------");
- return;
- }
-
- if (have > target)
- {
- int remove = have - target;
- Logger.Instance.LogMessage($"Too many neutrals -> remove={remove}. Shuffling current neutrals...");
- neutrals.Shuffle();
-
- for (int i = 0; i < remove && i < neutrals.Count; i++)
- {
- var ply = neutrals[i];
- Logger.Instance.LogMessage($"Demoting to Crewmate -> {ply.PlayerId}");
- ply.RpcSetRole(RoleTypes.Crewmate);
- }
-
- Logger.Instance.LogMessage("Demotion phase complete.");
- Logger.Instance.LogMessage("-------------- SELECT ROLES: END (demotions) --------------");
- return;
- }
-
- int need = target - have;
- Logger.Instance.LogMessage($"Need more neutrals -> need={need}");
-
- var crewElig = all.Where(p =>
- {
- var rb = p.Object.Data.Role;
- if (!rb) return false;
- bool isCrew = (rb is CrewmateRole) || (!rb.IsImpostor && rb.TeamType == RoleTeamTypes.Crewmate);
- if (!isCrew) return false;
-
- if (rb is ICustomRole cr && cr is INewModRole nm2)
- return nm2.Faction != NewModFaction.Apex && nm2.Faction != NewModFaction.Entropy;
-
- return true;
- }).Select(p => p.Object).ToList();
- Logger.Instance.LogMessage($"Crew eligible for conversion -> count={crewElig.Count}");
-
- if (crewElig.Count == 0)
- {
- Logger.Instance.LogMessage("No crew eligible to convert. Exiting.");
- Logger.Instance.LogMessage("-------------- SELECT ROLES: END (no elig crew) --------------");
- return;
- }
-
- var active = CustomRoleUtils.GetActiveRoles().ToList();
- Logger.Instance.LogMessage($"Active custom roles snapshot -> count={active.Count}");
-
- var candidates = new List();
-
- foreach (var r in CustomRoleManager.CustomMiraRoles)
- {
- if (r is not INewModRole nm) continue;
- if (nm.Faction != NewModFaction.Apex && nm.Faction != NewModFaction.Entropy) continue;
-
- var roleType = (RoleTypes)RoleId.Get(r.GetType());
- int already = active.Count(x => x && x.Role == roleType);
- int left = r.Configuration.MaxRoleCount - already;
- if (left <= 0) continue;
-
- int chance = r.GetChance() ?? r.Configuration.DefaultChance;
- float weight = chance;
- if (weight <= 0f) continue;
-
- candidates.Add(new Candidate { Role = r, Left = left, Weight = weight, RoleType = roleType });
- Logger.Instance.LogMessage($"Candidate -> {r.GetType().Name} type={(ushort)roleType} left={left} weight={weight} already={already} max={r.Configuration.MaxRoleCount}");
- }
-
- Logger.Instance.LogMessage($"Candidate pool built -> count={candidates.Count}");
- if (candidates.Count == 0)
- {
- Logger.Instance.LogMessage("No candidates to assign. Exiting.");
- Logger.Instance.LogMessage("-------------- SELECT ROLES: END (no candidates) --------------");
- return;
- }
-
- var picks = new List();
- if (opts.PreferVariety)
- {
- Logger.Instance.LogMessage("PreferVariety enabled -> taking one of each highest weight until need is met.");
- var ordered = candidates.OrderByDescending(x => x.Weight).ToList();
- for (int i = 0; i < ordered.Count && picks.Count < need; i++)
- {
- if (ordered[i].Left <= 0) continue;
- picks.Add(ordered[i].Role);
- Logger.Instance.LogMessage($"Variety pick -> {ordered[i].Role.GetType().Name}");
- var e = ordered[i]; e.Left -= 1; ordered[i] = e;
- }
- candidates = ordered;
- }
-
- while (picks.Count < need)
- {
- var avail = candidates.Where(c => c.Left > 0 && c.Weight > 0f).ToList();
- if (avail.Count == 0)
- {
- Logger.Instance.LogMessage("No more available candidates with slots and weight. Breaking.");
- break;
- }
-
- float total = avail.Sum(c => c.Weight);
- float rnum = UnityEngine.Random.Range(0f, total);
- float acc = 0f;
- var chosen = avail[0];
-
- for (int i = 0; i < avail.Count; i++)
- {
- acc += avail[i].Weight;
- if (rnum <= acc) { chosen = avail[i]; break; }
- }
-
- picks.Add(chosen.Role);
- Logger.Instance.LogMessage($"Weighted pick -> {chosen.Role.GetType().Name} (roll={rnum:F2} / total={total:F2})");
-
- int gi = candidates.FindIndex(x => x.RoleType == chosen.RoleType);
- if (gi >= 0)
- {
- candidates[gi] = new Candidate
- {
- Role = candidates[gi].Role,
- Left = candidates[gi].Left - 1,
- Weight = candidates[gi].Weight,
- RoleType = candidates[gi].RoleType
- };
- Logger.Instance.LogMessage($"Decrement slot -> {candidates[gi].Role.GetType().Name} now left={candidates[gi].Left}");
- }
- }
-
- Logger.Instance.LogMessage($"Final picks -> count={picks.Count}. Starting assignment to crewElig={crewElig.Count}");
-
- for (int i = 0; i < picks.Count && crewElig.Count > 0; i++)
- {
- int idx = HashRandom.FastNext(crewElig.Count);
- var pc = crewElig[idx];
- crewElig.RemoveAt(idx);
-
- var rt = (RoleTypes)RoleId.Get(picks[i].GetType());
- Logger.Instance.LogMessage($"Assign -> playerId={pc.PlayerId} role={(ushort)rt} ({picks[i].GetType().Name})");
- pc.RpcSetRole(rt);
- }
-
- Logger.Instance.LogMessage("Assignment phase complete.");
- Logger.Instance.LogMessage("-------------- SELECT ROLES: END --------------");
- }
- struct Candidate
- {
- public ICustomRole Role;
- public int Left;
- public float Weight;
- public RoleTypes RoleType;
- }
- }
-}
diff --git a/NewMod/Resources/RoleIcons/DeployzoneIcon.png b/NewMod/Resources/RoleIcons/DeployzoneIcon.png
new file mode 100644
index 0000000..f7e6f09
Binary files /dev/null and b/NewMod/Resources/RoleIcons/DeployzoneIcon.png differ
diff --git a/NewMod/Resources/deployzone.png b/NewMod/Resources/deployzone.png
new file mode 100644
index 0000000..6d74d7d
Binary files /dev/null and b/NewMod/Resources/deployzone.png differ
diff --git a/NewMod/Resources/newmod-android.bundle b/NewMod/Resources/newmod-android.bundle
index 68cbb41..47bba7f 100644
Binary files a/NewMod/Resources/newmod-android.bundle and b/NewMod/Resources/newmod-android.bundle differ
diff --git a/NewMod/Resources/newmod-win.bundle b/NewMod/Resources/newmod-win.bundle
index a41b630..1d99211 100644
Binary files a/NewMod/Resources/newmod-win.bundle and b/NewMod/Resources/newmod-win.bundle differ
diff --git a/NewMod/Resources/noise.png b/NewMod/Resources/noise.png
new file mode 100644
index 0000000..bceb698
Binary files /dev/null and b/NewMod/Resources/noise.png differ
diff --git a/NewMod/Roles/CrewmateRoles/Beacon.cs b/NewMod/Roles/CrewmateRoles/Beacon.cs
index 15e06ab..7cb37ae 100644
--- a/NewMod/Roles/CrewmateRoles/Beacon.cs
+++ b/NewMod/Roles/CrewmateRoles/Beacon.cs
@@ -6,7 +6,12 @@
using MiraAPI.GameOptions;
using MiraAPI.Roles;
using MiraAPI.Utilities;
+using NewMod.Components.ScreenEffects;
+using NewMod.Networking;
using NewMod.Options.Roles.BeaconOptions;
+using NewMod.Utilities;
+using Reactor.Networking.Rpc;
+using Reactor.Utilities;
using UnityEngine;
namespace NewMod.Roles.CrewmateRoles
@@ -90,7 +95,9 @@ public static void UpdateChargesFromTasks()
grantedFromTasks = earned;
Helpers.CreateAndShowNotification(
$"+{delta} Beacon {(delta > 1 ? "charges" : "charge")} (tasks)",
- new Color(0.75f, 0.65f, 1f), spr:NewModAsset.RadarIcon.LoadAsset());
+ new Color(0.75f, 0.65f, 1f), spr: NewModAsset.RadarIcon.LoadAsset());
+
+ Rpc.Instance.Send(new BeaconPulseRpc.Data(settings.PulseDuration));
}
}
public static int GetCompletedTasks()
diff --git a/NewMod/Roles/NeutralRoles/Shade.cs b/NewMod/Roles/NeutralRoles/Shade.cs
new file mode 100644
index 0000000..0afc266
--- /dev/null
+++ b/NewMod/Roles/NeutralRoles/Shade.cs
@@ -0,0 +1,106 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Gameplay;
+using MiraAPI.GameOptions;
+using MiraAPI.Roles;
+using MiraAPI.Utilities;
+using NewMod.Components;
+using NewMod.Options.Roles.ShadeOptions;
+using NewMod.Utilities;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace NewMod.Roles.NeutralRoles
+{
+ public class Shade : ImpostorRole, INewModRole
+ {
+ public static readonly Dictionary ShadeKills = new();
+ public string RoleName => "Shade";
+ public string RoleDescription => "Lurk. Fade. Kill unseen.";
+ public string RoleLongDescription => "Deploy a shadow field that grants invisibility and lethal power within its darkness.";
+ public Color RoleColor => new(0.45f, 0f, 0.8f);
+ public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
+ public NewModFaction Faction => NewModFaction.Entropy;
+ public CustomRoleConfiguration Configuration => new(this)
+ {
+ AffectedByLightOnAirship = true,
+ CanUseSabotage = false,
+ CanUseVent = false,
+ UseVanillaKillButton = true,
+ TasksCountForProgress = false,
+ Icon = NewModAsset.DeployZoneIcon
+ };
+
+ [HideFromIl2Cpp]
+ public StringBuilder SetTabText()
+ {
+ var tabText = INewModRole.GetRoleTabText(this);
+
+ int zonesActive = ShadowZone.zones.Count;
+ int playersInZones = Helpers.GetAlivePlayers().Count(p => ShadowZone.IsInsideAny(p.GetTruePosition()));
+
+ var mode = OptionGroupSingleton.Instance.Behavior;
+
+ tabText.AppendLine($"Within the dark you are unseen.");
+ tabText.AppendLine("\n");
+ tabText.AppendLine($"Active Shadow Zones: {zonesActive}");
+ tabText.AppendLine($"Players inside zones: {playersInZones}");
+
+ string effectText = mode switch
+ {
+ ShadeOptions.ShadowMode.Invisible => "Enter a shadow zone to become invisible.",
+ ShadeOptions.ShadowMode.KillEnabled => "Enter a shadow zone to gain the power to kill once.",
+ ShadeOptions.ShadowMode.Both => "Enter a shadow zone to become invisible and gain the power to kill once.",
+ _ => "Enter a shadow zone to embrace the darkness."
+ };
+
+ tabText.AppendLine($"\n{effectText}");
+
+ return tabText;
+ }
+ public override bool DidWin(GameOverReason gameOverReason)
+ {
+ return gameOverReason == (GameOverReason)NewModEndReasons.ShadeWin;
+ }
+
+ [RegisterEvent]
+ public static void OnAfterMurder(AfterMurderEvent evt)
+ {
+ var killer = evt.Source;
+ var victim = evt.Target;
+
+ Utils.RecordOnKill(killer, victim);
+
+ if (killer.Data.Role is not Shade shadeRole)
+ return;
+
+ if (!ShadowZone.IsInsideAny(victim.GetTruePosition()))
+ return;
+
+ byte id = killer.PlayerId;
+ ShadeKills[id] = ShadeKills.GetValueOrDefault(id) + 1;
+
+ if (killer.AmOwner)
+ {
+ int required = (int)OptionGroupSingleton.Instance.RequiredKills;
+ Coroutines.Start(CoroutinesHelper.CoNotify(
+ $"Shadow Harvest\nKills: {ShadeKills[id]}/{required}"
+ ));
+ }
+ }
+ [RegisterEvent]
+ public static void OnShadeRoleAssigned(SetRoleEvent evt)
+ {
+ if (evt.Player.AmOwner && evt.Player.Data.Role is Shade)
+ {
+ var hud = HudManager.Instance;
+
+ hud.KillButton.currentTarget = null;
+ hud.KillButton.gameObject.SetActive(false);
+ }
+ }
+ }
+}
diff --git a/NewMod/Utilities/CoroutinesHelper.cs b/NewMod/Utilities/CoroutinesHelper.cs
index 3a48032..23f1be4 100644
--- a/NewMod/Utilities/CoroutinesHelper.cs
+++ b/NewMod/Utilities/CoroutinesHelper.cs
@@ -436,7 +436,7 @@ public static IEnumerator DespawnCircle(GameObject go, float duration)
///
/// Coroutine that waits for a given duration and then removes
- /// specific visual effects (Earthquake, Glitch, SlowPulseHue) from a Camera.
+ /// specific visual effects from a Camera.
///
/// The Camera to check for and remove effects from.
/// The time in seconds to wait before removing the effects.
@@ -451,6 +451,10 @@ public static IEnumerator RemoveCameraEffect(Camera cam, float duration)
Object.Destroy(ge);
if (cam.TryGetComponent(out var hue))
Object.Destroy(hue);
+ if (cam.TryGetComponent(out var dw))
+ Object.Destroy(dw);
+ if (cam.TryGetComponent(out var sf))
+ Object.Destroy(sf);
}
}
}
diff --git a/NewMod/Utilities/PranksterUtilities.cs b/NewMod/Utilities/PranksterUtilities.cs
index e6787c6..5b5312c 100644
--- a/NewMod/Utilities/PranksterUtilities.cs
+++ b/NewMod/Utilities/PranksterUtilities.cs
@@ -23,7 +23,7 @@ public static void CreatePranksterDeadBody(PlayerControl player, byte parentId)
{
NewMod.Instance.Log.LogError("[PranksterUtilities] CreatePranksterDeadBody: Failed to create dead body, random player is null.");
}
- var deadBody = Object.Instantiate(GameManager.Instance.deadBodyPrefab[0]);
+ var deadBody = Object.Instantiate(GameManager.Instance.GetDeadBody(player.Data.Role));
deadBody.name = PranksterBodyName;
deadBody.ParentId = parentId;
diff --git a/NewMod/Utilities/VisionaryUtilities.cs b/NewMod/Utilities/VisionaryUtilities.cs
index f088ed7..7759bc8 100644
--- a/NewMod/Utilities/VisionaryUtilities.cs
+++ b/NewMod/Utilities/VisionaryUtilities.cs
@@ -33,7 +33,7 @@ public static string ScreenshotDirectory
{
get
{
- string directory = Path.Combine(BepInEx.Paths.GameRootPath, "NewMod", "Screenshots");
+ string directory = Path.Combine(Application.persistentDataPath, "NewMod", "Screenshots");
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
diff --git a/README.md b/README.md
index 8b63c4d..1fb8be0 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@
| v1.2.5 | v16.1.0 (2025.6.10) | [Download](https://github.com/CallOfCreator/NewMod/releases/download/V1.2.5/NewMod.zip) |
| v1.2.6 | v16.1.0 (2025.6.10) | [Download](https://github.com/CallOfCreator/NewMod/releases/download/V1.2.6/NewMod.zip) |
| v1.2.7 | v17.0.0 (2025.9.9) | [Download](https://github.com/CallOfCreator/NewMod/releases/download/V1.2.7/NewMod.zip) |
+| v1.2.8 | v17.0.1 (2025.10.14) | [Download](https://github.com/CallOfCreator/NewMod/releases/download/V1.2.8/NewMod.zip) |
---
@@ -96,9 +97,10 @@ NewMod is compatible with the following mods, enabling an enhanced experience wi
| Mod ame | Mod Version | GitHub Link | Status |
|-------------------|-------------|------------------------------------------------------|-------------|
-| yanplaRoles | v0.1.6+ | [Download](https://github.com/yanpla/yanplaRoles) | ❌ Deprecated |
+| yanplaRoles | v0.2.1+ | [Download](https://github.com/yanpla/yanplaRoles) | ✅ Supported |
| LaunchpadReloaded | v0.3.4+ | [Download](https://github.com/All-Of-Us-Mods/LaunchpadReloaded) | ✅ Supported |
| LevelImposter | v0.20.3+ | [Download](https://github.com/DigiWorm0/LevelImposter) | ✅ Supported |
+| Submerged | v2025.10.22 | [Download](https://github.com/SubmergedAmongUs/Submerged) | ✅ Supported |
---
diff --git a/libs/Android/AmongUs.GameLibs.Android.2025.9.9.nupkg b/libs/Android/AmongUs.GameLibs.Android.2025.9.9.nupkg
deleted file mode 100644
index d09ec17..0000000
Binary files a/libs/Android/AmongUs.GameLibs.Android.2025.9.9.nupkg and /dev/null differ
diff --git a/libs/MiraAPI.dll b/libs/MiraAPI.dll
deleted file mode 100644
index d47ce6f..0000000
Binary files a/libs/MiraAPI.dll and /dev/null differ
diff --git a/nuget.config b/nuget.config
index 784b019..db78244 100644
--- a/nuget.config
+++ b/nuget.config
@@ -3,6 +3,5 @@
-
\ No newline at end of file