diff --git a/.github/assets/customLobby.png b/.github/assets/customLobby.png
new file mode 100644
index 0000000..79b8890
Binary files /dev/null and b/.github/assets/customLobby.png differ
diff --git a/NewMod/Buttons/Overload/FinalButton.cs b/NewMod/Buttons/Overload/FinalButton.cs
new file mode 100644
index 0000000..1e5d977
--- /dev/null
+++ b/NewMod/Buttons/Overload/FinalButton.cs
@@ -0,0 +1,74 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+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 KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.B;
+
+ ///
+ /// 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
index 908d6d6..e478b02 100644
--- a/NewMod/Buttons/Overload/OverloadButton.cs
+++ b/NewMod/Buttons/Overload/OverloadButton.cs
@@ -12,6 +12,7 @@ namespace NewMod.Buttons.Overload
///
public class OverloadButton : CustomActionButton
{
+ public CustomActionButton absorbed;
///
/// The display text shown on the button UI.
/// Set by the absorbed ability.
@@ -89,6 +90,8 @@ public class OverloadButton : CustomActionButton
/// The button to absorb.
public void Absorb(CustomActionButton target)
{
+ absorbed = target;
+
absorbedText = target.Name;
absorbedCooldown = target.Cooldown;
absorbedMaxUses = target.MaxUses;
@@ -99,8 +102,20 @@ public void Absorb(CustomActionButton target)
OverrideName(absorbedText);
OverrideSprite(absorbedSprite.LoadAsset());
- SetUses(absorbedMaxUses);
+
+ 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);
}
///
@@ -108,8 +123,8 @@ public void Absorb(CustomActionButton target)
///
protected override void OnClick()
{
+ NewMod.Instance.Log.LogError("Overload invoking absorbed action...");
absorbedOnClick?.Invoke();
-
}
///
@@ -120,7 +135,7 @@ protected override void OnClick()
/// True if Overload with a valid absorbed ability.
public override bool Enabled(RoleBehaviour role)
{
- return role is OverloadRole && absorbedOnClick != null;
+ return role is OverloadRole && absorbed != null;
}
///
@@ -129,7 +144,8 @@ public override bool Enabled(RoleBehaviour role)
/// True if usable.
public override bool CanUse()
{
- return base.CanUse() && absorbedOnClick != null;
+ absorbed?.FixedUpdateHandler(PlayerControl.LocalPlayer);
+ return base.CanUse() && absorbed != null;
}
}
}
diff --git a/NewMod/Buttons/WraithCaller/CallWraith.cs b/NewMod/Buttons/WraithCaller/CallWraith.cs
new file mode 100644
index 0000000..4ff742b
--- /dev/null
+++ b/NewMod/Buttons/WraithCaller/CallWraith.cs
@@ -0,0 +1,99 @@
+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 Rewired;
+using System.Collections.Generic;
+using System.Linq;
+
+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 KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.M;
+
+ ///
+ /// 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/Birthday/Toast.cs b/NewMod/Components/Birthday/Toast.cs
new file mode 100644
index 0000000..7b3efe4
--- /dev/null
+++ b/NewMod/Components/Birthday/Toast.cs
@@ -0,0 +1,67 @@
+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 Toast(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 Toast CreateToast()
+ {
+ 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/WraithCallerNpc.cs b/NewMod/Components/WraithCallerNpc.cs
new file mode 100644
index 0000000..b1d834f
--- /dev/null
+++ b/NewMod/Components/WraithCallerNpc.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Linq;
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.Networking;
+using NewMod.Utilities;
+using Reactor.Utilities;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components
+{
+ [RegisterInIl2Cpp]
+ public class WraithCallerNpc(IntPtr ptr) : MonoBehaviour(ptr)
+ {
+ public PlayerControl Owner { get; set; }
+ public PlayerControl Target { get; set; }
+ public PlayerControl npc;
+ public LightSource ownerLight;
+ public bool isActive = false;
+
+ [HideFromIl2Cpp]
+
+ // Inspired by: https://github.com/NuclearPowered/Reactor/blob/e27a79249ea706318f3c06f3dc56a5c42d65b1cf/Reactor.Debugger/Window/Tabs/GameTab.cs#L70
+ public void Initialize(PlayerControl wraith, PlayerControl target)
+ {
+ Owner = wraith;
+ Target = target;
+
+ var prefab = AmongUsClient.Instance.PlayerPrefab;
+ npc = Instantiate(prefab);
+ npc.PlayerId = (byte)GameData.Instance.GetAvailableId();
+
+ var npcData = GameData.Instance.AddDummy(npc);
+ AmongUsClient.Instance.Spawn(npcData);
+ AmongUsClient.Instance.Spawn(npc);
+
+ npc.isDummy = false;
+ npc.notRealPlayer = true;
+ KillAnimation.SetMovement(npc, true);
+ npc.NetTransform.RpcSnapTo(Owner.transform.position);
+
+ var color = (byte)(npc.PlayerId % Palette.PlayerColors.Length);
+ npc.RpcSetName("Wraith NPC");
+ npc.RpcSetColor(color);
+ npc.RpcSetHat("");
+ npc.RpcSetSkin("");
+ npc.RpcSetPet("");
+ npc.RpcSetVisor("");
+
+ npc.Collider.enabled = false;
+
+ var noShadow = npc.gameObject.AddComponent();
+ if (noShadow != null)
+ {
+ noShadow.rend = npc.cosmetics.currentBodySprite.BodySprite;
+ noShadow.hitOverride = npc.Collider;
+ }
+
+ ownerLight = Owner.lightSource;
+ ownerLight.transform.SetParent(npc.transform, false);
+ ownerLight.transform.localPosition = npc.Collider.offset;
+ Camera.main.GetComponent().SetTarget(npc);
+
+ npc.cosmetics.enabled = false;
+ npc.enabled = false;
+
+ isActive = true;
+
+ Coroutines.Start(WalkToTarget());
+
+ if (Target.AmOwner)
+ {
+ SoundManager.Instance.PlaySound(NewModAsset.HeartbeatSound.LoadAsset(), false, 1f);
+ }
+ }
+ public void Update()
+ {
+ if (MeetingHud.Instance)
+ Dispose();
+ }
+ [HideFromIl2Cpp]
+ public System.Collections.IEnumerator WalkToTarget()
+ {
+ if (Target.Data.IsDead || Target.Data.Disconnected)
+ {
+ Dispose();
+ }
+ while (isActive && !MeetingHud.Instance)
+ {
+ Vector2 npcPos = npc.GetTruePosition();
+ Vector2 targetPos = Target.GetTruePosition();
+ Vector2 dir = (targetPos - npcPos).normalized;
+
+ npc.MyPhysics.SetNormalizedVelocity(dir);
+
+ float distance = Vector2.Distance(npcPos, targetPos);
+
+ if (distance <= 0.1f)
+ {
+ npc.MyPhysics.SetNormalizedVelocity(Vector2.zero);
+
+ Owner.RpcCustomMurder(Target, true, teleportMurderer: false);
+
+ if (Target.AmOwner)
+ {
+ CoroutinesHelper.CoNotify("Oops! The Wraith NPC got you...");
+ }
+ WraithCallerUtilities.AddKillNPC(Owner.PlayerId);
+
+ Dispose();
+ yield break;
+ }
+ yield return new WaitForFixedUpdate();
+ }
+ npc.MyPhysics.SetNormalizedVelocity(Vector2.zero);
+ Dispose();
+ }
+
+ [HideFromIl2Cpp]
+ public void Dispose()
+ {
+ if (!isActive) return;
+ isActive = false;
+
+ if (npc != null)
+ {
+ var cam = Camera.main.GetComponent();
+ cam.SetTarget(Owner);
+
+ ownerLight.transform.SetParent(Owner.transform, false);
+ ownerLight.transform.localPosition = Owner.Collider.offset;
+
+ var info = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(d => d.PlayerId == npc.PlayerId);
+ if (info != null) GameData.Instance.RemovePlayer(info.PlayerId);
+
+ Destroy(npc.gameObject);
+ npc = null;
+ }
+ Destroy(gameObject);
+ }
+ }
+}
diff --git a/NewMod/CustomRPC.cs b/NewMod/CustomRPC.cs
index 45aed72..db951e5 100644
--- a/NewMod/CustomRPC.cs
+++ b/NewMod/CustomRPC.cs
@@ -13,5 +13,6 @@ public enum CustomRPC
FearPulse,
SuppressionDome,
WitnessTrap,
- NotifyChampion
+ NotifyChampion,
+ SummonNPC
}
\ No newline at end of file
diff --git a/NewMod/DebugWindow.cs b/NewMod/DebugWindow.cs
index 294e2f3..6f2bc4f 100644
--- a/NewMod/DebugWindow.cs
+++ b/NewMod/DebugWindow.cs
@@ -9,11 +9,6 @@
using NewMod.Roles.CrewmateRoles;
using NewMod.Roles.ImpostorRoles;
using NewMod.Roles.NeutralRoles;
-using NewMod.Buttons.EnergyThief;
-using NewMod.Buttons.SpecialAgent;
-using NewMod.Buttons.Visionary;
-using NewMod.Buttons.Prankster;
-using NewMod.Buttons.Necromancer;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.ImGui;
using UnityEngine;
@@ -42,16 +37,6 @@ public class DebugWindow(nint ptr) : MonoBehaviour(ptr)
if (!isFreeplay) return;
PlayerControl.LocalPlayer.RpcRemoveModifier();
}
- if (GUILayout.Button("Disable Collider"))
- {
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.Collider.enabled = false;
- }
- if (GUILayout.Button("Enable Collider"))
- {
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.Collider.enabled = true;
- }
if (GUILayout.Button("Become Necromancer"))
{
if (!isFreeplay) return;
@@ -79,27 +64,9 @@ public class DebugWindow(nint ptr) : MonoBehaviour(ptr)
}
if (GUILayout.Button("Increases Uses by 3"))
{
- var player = PlayerControl.LocalPlayer;
- if (player.Data.Role is NecromancerRole)
- {
- CustomButtonSingleton.Instance.IncreaseUses(3);
- }
- else if (player.Data.Role is EnergyThief)
+ foreach (var button in CustomButtonManager.Buttons)
{
- CustomButtonSingleton.Instance.IncreaseUses(3);
- }
- else if (player.Data.Role is SpecialAgent)
- {
- CustomButtonSingleton.Instance.IncreaseUses(3);
- }
- else if (player.Data.Role is Prankster)
- {
- CustomButtonSingleton.Instance.IncreaseUses(3);
- }
- else
- {
- CustomButtonSingleton.Instance.IncreaseUses(3);
- CustomButtonSingleton.Instance.IncreaseUses(3);
+ button.SetUses(3);
}
}
if (GUILayout.Button("Randomly Cast a Vote"))
@@ -112,10 +79,6 @@ public class DebugWindow(nint ptr) : MonoBehaviour(ptr)
GUILayout.Label("Overload button tests", GUI.skin.box);
- if (GUILayout.Button("Test Overload Finale"))
- {
- OverloadRole.UnlockFinalAbility();
- }
if (GUILayout.Button("Test Absorb"))
{
var prey = Utils.GetRandomPlayer(p =>
@@ -136,7 +99,7 @@ public class DebugWindow(nint ptr) : MonoBehaviour(ptr)
{
CustomButtonSingleton.Instance.Absorb(button);
}
- Debug.Log($"[Overload] Successfully absorbed ability: {button.Name}");
+ Debug.Log($"[Overload] Successfully absorbed ability: {button.Name}, CanUse:{button.CanUse()}");
}
}
else if (prey.Data.Role.Ability != null)
diff --git a/NewMod/Features/CustomPlayerTag.cs b/NewMod/Features/CustomPlayerTag.cs
new file mode 100644
index 0000000..b06306e
--- /dev/null
+++ b/NewMod/Features/CustomPlayerTag.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Collections.Generic;
+using HarmonyLib;
+using Hazel;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Meeting;
+using Reactor.Utilities;
+
+namespace NewMod.Features
+{
+ public static class CustomPlayerTag
+ {
+ public enum TagType : byte
+ {
+ Player,
+ NPC,
+ Dev,
+ Creator,
+ Tester,
+ Staff,
+ Contributor,
+ Host,
+ AOUDev
+ }
+
+ public static readonly Dictionary DefaultHex = new()
+ {
+ { TagType.Player, "c0c0c0" },
+ { TagType.NPC, "7D3C98"},
+ { TagType.Dev, "ff4d4d" },
+ { TagType.Creator, "ffb000" },
+ { TagType.Tester, "00e0ff" },
+ { TagType.Staff, "9b59b6" },
+ { TagType.Contributor, "7ee081" },
+ { TagType.Host, "ff7f50" },
+ { TagType.AOUDev, "00ffb3" }
+ };
+
+ public static string DisplayName(TagType t) => t switch
+ {
+ TagType.Player => "Player",
+ TagType.NPC => "NPC",
+ TagType.Dev => "Developer",
+ TagType.Creator => "Creator",
+ TagType.Tester => "Tester",
+ TagType.Staff => "Staff",
+ TagType.Contributor => "Contributor",
+ TagType.Host => "Host",
+ TagType.AOUDev => "AOU Dev",
+ _ => ""
+ };
+
+ public static string Format(TagType t, string hex)
+ {
+ string color = string.IsNullOrWhiteSpace(hex)
+ ? (DefaultHex.TryGetValue(t, out var h) ? h : "ffffff")
+ : hex;
+ string label = DisplayName(t);
+ return $"\n{label}";
+ }
+ public static TagType GetTag(string friendCode)
+ {
+ if (string.Equals(friendCode, "puncool#9009", StringComparison.OrdinalIgnoreCase)) return TagType.Creator;
+ if (string.Equals(friendCode, "peaktipple#8186", StringComparison.OrdinalIgnoreCase)) return TagType.Dev;
+ if (string.Equals(friendCode, "shinyrake#9382", StringComparison.OrdinalIgnoreCase)) return TagType.Dev;
+ if (string.Equals(friendCode, "dimpledue#6629", StringComparison.OrdinalIgnoreCase)) return TagType.AOUDev;
+ return TagType.Player;
+ }
+ public static TagType GetTagFor(PlayerControl pc)
+ {
+ if (pc.isDummy || pc.notRealPlayer) return TagType.NPC; // Will affect dummies in Freeplay mode
+ return GetTag(pc.FriendCode);
+ }
+
+ [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcSetName))]
+ public static class RpcSetNamePatch
+ {
+ public static bool Prefix(PlayerControl __instance, ref string name)
+ {
+ var friendCode = __instance.FriendCode;
+
+ TagType tag = GetTagFor(__instance);
+
+ var host = GameData.Instance.GetHost();
+ bool isHost = host.PlayerId == __instance.PlayerId;
+
+ string baseName = name.Split('\n')[0];
+
+ string newName = baseName;
+ if (isHost)
+ newName += Format(TagType.Host, DefaultHex[TagType.Host]);
+ if (tag != TagType.Player)
+ newName += Format(tag, DefaultHex[tag]);
+ else
+ newName += Format(TagType.Player, DefaultHex[TagType.Player]);
+
+ Logger.Instance.LogInfo($"Player {__instance.PlayerId} '{baseName}' " + $"FriendCode={friendCode}, Host={isHost}, Tag={DisplayName(tag)}");
+
+ __instance.SetName(newName);
+
+ var writer = AmongUsClient.Instance.StartRpcImmediately(__instance.NetId, (byte)RpcCalls.SetName, SendOption.Reliable, -1);
+ writer.Write(__instance.Data.NetId);
+ writer.Write(newName);
+ AmongUsClient.Instance.FinishRpcImmediately(writer);
+
+ return false;
+ }
+ }
+
+ [RegisterEvent]
+ public static void OnMeetingStart(StartMeetingEvent evt)
+ {
+ var host = GameData.Instance.GetHost();
+
+ foreach (var ps in evt.MeetingHud.playerStates)
+ {
+ string baseName = ps.NameText.text.Split('\n')[0];
+ bool isHost = ps.TargetPlayerId == host.PlayerId;
+
+ TagType tag = GetTag(GameData.Instance.GetPlayerById(ps.TargetPlayerId).FriendCode);
+ string newName = baseName;
+
+ if (isHost)
+ newName += Format(TagType.Host, DefaultHex[TagType.Host]);
+
+ if (tag != TagType.Player)
+ newName += Format(tag, DefaultHex[tag]);
+ else
+ newName += Format(TagType.Player, DefaultHex[TagType.Player]);
+
+ ps.NameText.text = newName;
+ }
+ }
+ }
+}
diff --git a/NewMod/NewMod.cs b/NewMod/NewMod.cs
index 46aa10a..9578725 100644
--- a/NewMod/NewMod.cs
+++ b/NewMod/NewMod.cs
@@ -18,7 +18,6 @@
using MiraAPI.Events.Vanilla.Gameplay;
using NewMod.Roles.NeutralRoles;
using MiraAPI.Roles;
-using System;
using MiraAPI.Hud;
using UnityEngine.Events;
using NewMod.Options.Roles.OverloadOptions;
@@ -37,7 +36,7 @@ namespace NewMod;
public partial class NewMod : BasePlugin, IMiraPlugin
{
public const string Id = "com.callofcreator.newmod";
- public const string ModVersion = "1.2.4";
+ public const string ModVersion = "1.2.5";
public Harmony Harmony { get; } = new Harmony(Id);
public static BasePlugin Instance;
public static Minigame minigame;
@@ -98,7 +97,7 @@ public static void InitializeKeyBinds()
var deadBodies = Helpers.GetNearestDeadBodies(PlayerControl.LocalPlayer.GetTruePosition(), 20f, Helpers.CreateFilter(Constants.NotShipMask));
if (deadBodies != null && deadBodies.Count > 0)
{
- var randomIndex = UnityEngine.Random.Range(0, deadBodies.Count);
+ var randomIndex = Random.Range(0, deadBodies.Count);
var randomBodyPosition = deadBodies[randomIndex].transform.position;
PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(randomBodyPosition);
}
@@ -109,6 +108,18 @@ public static void InitializeKeyBinds()
}
}
+ [RegisterEvent]
+ public static void OnBeforeMurder(BeforeMurderEvent evt)
+ {
+ if (evt.Target != OverloadRole.chosenPrey) return;
+
+ //TODO: Use the newest MiraAPI roles for button mapping
+ if (evt.Target.Data.Role is ICustomRole customRole && Utils.RoleToButtonsMap.TryGetValue(customRole.GetType(), out var buttonsType))
+ {
+ OverloadRole.CachedButtons = [.. CustomButtonManager.Buttons.Where(b => buttonsType.Contains(b.GetType()))];
+ Instance.Log.LogMessage($"CachedButton: {buttonsType.GetType().Name}");
+ }
+ }
[RegisterEvent]
public static void OnAfterMurder(AfterMurderEvent evt)
{
@@ -120,16 +131,12 @@ public static void OnAfterMurder(AfterMurderEvent evt)
foreach (var pc in PlayerControl.AllPlayerControls.ToArray().Where(p => p.AmOwner && p.Data.Role is OverloadRole))
{
- if (target.Data.Role is ICustomRole customRole && Utils.RoleToButtonsMap.TryGetValue(customRole.GetType(), out var buttonsType))
+ if (target.Data.Role is ICustomRole customRole)
{
- foreach (var buttonType in buttonsType)
+ foreach (var button in OverloadRole.CachedButtons)
{
- var button = CustomButtonManager.Buttons.FirstOrDefault(b => b.GetType() == buttonType);
-
- if (button != null)
- {
- CustomButtonSingleton.Instance.Absorb(button);
- }
+ CustomButtonSingleton.Instance.Absorb(button);
+ Debug.Log($"[Overload] Successfully absorbed ability: {button.Name}");
}
}
else if (target.Data.Role is not ICustomRole)
@@ -143,13 +150,14 @@ public static void OnAfterMurder(AfterMurderEvent evt)
pb.OnClick.AddListener((UnityAction)target.Data.Role.UseAbility);
}
}
+ OverloadRole.CachedButtons.Clear();
OverloadRole.AbsorbedAbilityCount++;
- Coroutines.Start(CoroutinesHelper.CoNotify($"Charge {OverloadRole.AbsorbedAbilityCount}/{OptionGroupSingleton.Instance.NeededCharge}"));
OverloadRole.chosenPrey = null;
+ Coroutines.Start(CoroutinesHelper.CoNotify($"Charge {OverloadRole.AbsorbedAbilityCount}/{OptionGroupSingleton.Instance.NeededCharge}"));
if (OverloadRole.AbsorbedAbilityCount >= OptionGroupSingleton.Instance.NeededCharge)
{
- OverloadRole.UnlockFinalAbility();
+ Coroutines.Start(CoroutinesHelper.CoNotify("Objective completed: Final Ability unlocked!"));
}
else
{
diff --git a/NewMod/NewMod.csproj b/NewMod/NewMod.csproj
index e6963d4..60a4dc4 100644
--- a/NewMod/NewMod.csproj
+++ b/NewMod/NewMod.csproj
@@ -21,6 +21,7 @@
+
@@ -31,10 +32,4 @@
-
-
-
- ..\libs\MiraAPI.dll
-
-
diff --git a/NewMod/NewModAsset.cs b/NewMod/NewModAsset.cs
index 2332d76..6d66272 100644
--- a/NewMod/NewModAsset.cs
+++ b/NewMod/NewModAsset.cs
@@ -1,13 +1,24 @@
+using Reactor.Utilities;
using MiraAPI.Utilities.Assets;
+using UnityEngine;
namespace NewMod;
public static class NewModAsset
{
+ public static AssetBundle Bundle = AssetBundleManager.Load("newmod");
+
// Miscellaneous
public static LoadableResourceAsset Banner { get; } = new("NewMod.Resources.optionImage.png");
public static LoadableResourceAsset Arrow { get; } = new("NewMod.Resources.Arrow.png");
public static LoadableResourceAsset ModLogo { get; } = new("NewMod.Resources.Logo.png");
+ public static LoadableResourceAsset CustomCursor { get; } = new("NewMod.Resources.cursor.png");
+
+ // NewMod's First Birthday Assets
+ public static LoadableResourceAsset MainMenuBG { get; } = new("NewMod.Resources.Birthday.newmod-birthday-v1.png");
+ public static LoadableAsset CustomLobby { get; } = new LoadableBundleAsset("CustomLobby", Bundle);
+ public static LoadableAsset Toast { get; } = new LoadableBundleAsset("Toast", Bundle);
+ public static LoadableAsset WraithCallerMinigame { get; } = new LoadableBundleAsset("WraithCallerMinigame", Bundle);
// Button icons
public static LoadableResourceAsset SpecialAgentButton { get; } = new("NewMod.Resources.givemission.png");
@@ -18,6 +29,8 @@ public static class NewModAsset
public static LoadableResourceAsset DeadBodySprite { get; } = new("NewMod.Resources.deadbody.png");
public static LoadableResourceAsset Camera { get; } = new("NewMod.Resources.cam.png");
public static LoadableResourceAsset StrikeButton { get; } = new("NewMod.Resources.Strike.png");
+ public static LoadableResourceAsset FinalButton { get; } = new("NewMod.Resources.final.png");
+ public static LoadableResourceAsset CallWraith { get; } = new("NewMod.Resources.callwraith.png");
// SFX
public static LoadableAudioResourceAsset ReviveSound { get; } = new("NewMod.Resources.Sounds.revive.wav");
@@ -34,6 +47,7 @@ public static class NewModAsset
public static LoadableResourceAsset StrikeIcon { get; } = new("NewMod.Resources.RoleIcons.StrikeIcon.png");
public static LoadableResourceAsset InjectIcon { get; } = new("NewMod.Resources.RoleIcons.InjectIcon.png");
public static LoadableResourceAsset CrownIcon { get; } = new("NewMod.Resources.RoleIcons.crown.png");
+ public static LoadableResourceAsset WraithIcon { get; } = new("NewMod.Resources.RoleIcons.wraith.png");
// Notif Icons
public static LoadableResourceAsset VisionDebuff { get; } = new("NewMod.Resources.NotifIcons.vision_debuff.png");
diff --git a/NewMod/NewModDateTime.cs b/NewMod/NewModDateTime.cs
index 618f2b2..d1d1b4d 100644
--- a/NewMod/NewModDateTime.cs
+++ b/NewMod/NewModDateTime.cs
@@ -1,14 +1,27 @@
using System;
-namespace NewMod;
-public static class NewModDateTime
+namespace NewMod
{
- public static DateTime NewModBirthday
+ public static class NewModDateTime
{
- get
+ public static DateTime NewModBirthday
{
- var thisYear = new DateTime(DateTime.Now.Year, 8, 28);
- return DateTime.Now <= thisYear ? thisYear : new DateTime(DateTime.Now.Year + 1, 8, 28);
+ get
+ {
+ var thisYear = new DateTime(DateTime.Now.Year, 8, 28, 16, 0, 0);
+ return DateTime.Now <= thisYear ? thisYear : new DateTime(DateTime.Now.Year + 1, 8, 28);
+ }
}
+ public static DateTime NewModBirthdayWeekEnd
+ {
+ get
+ {
+ return NewModBirthday.AddDays(7);
+ }
+ }
+
+ public static bool IsNewModBirthdayWeek =>
+ DateTime.Now >= NewModBirthday && DateTime.Now <= NewModBirthdayWeekEnd;
+ public static bool IsWraithCallerUnlocked => DateTime.Now >= NewModBirthday;
}
}
diff --git a/NewMod/NewModEndReasons.cs b/NewMod/NewModEndReasons.cs
index ac39b71..06443ea 100644
--- a/NewMod/NewModEndReasons.cs
+++ b/NewMod/NewModEndReasons.cs
@@ -11,6 +11,7 @@ public enum NewModEndReasons
EgoistWin = 116,
InjectorWin = 117,
PulseBladeWin = 118,
- TyrantWin = 119
+ TyrantWin = 119,
+ WraithCallerWin = 120
}
}
\ No newline at end of file
diff --git a/NewMod/Options/CompatibilityOptions.cs b/NewMod/Options/CompatibilityOptions.cs
index 7db18a3..063e0ff 100644
--- a/NewMod/Options/CompatibilityOptions.cs
+++ b/NewMod/Options/CompatibilityOptions.cs
@@ -7,29 +7,8 @@ public class CompatibilityOptions : AbstractOptionGroup
{
public override string GroupName => "Mod Compatibility";
public override Func GroupVisible => ModCompatibility.IsLaunchpadLoaded;
- public ModdedToggleOption AllowRevenantHitmanCombo { get; } = new("Allow Revenant & Hitman in Same Match", false)
- {
- ChangedEvent = value =>
- {
- HudManager.Instance.ShowPopUp(value
- ? "You enabled the Revenant & Hitman combo. This may break game balance!"
- : "Revenant & Hitman combo disabled. Only one will be allowed per match.");
- }
- };
- public ModdedEnumOption Compatibility { get; } = new("Mod Compatibility", ModPriority.PreferNewMod)
- {
- ChangedEvent = value =>
- {
- HudManager.Instance.ShowPopUp(
- value switch
- {
- ModPriority.PreferNewMod => "You selected 'PreferNewMod'. Medic will be disabled.\n" +
- "Switch to 'Prefer LaunchpadReloaded' to enable Medic and disable Necromancer.",
- ModPriority.PreferLaunchpadReloaded => "You selected 'PreferLaunchpadReloaded'. Necromancer will be disabled.\n" +
- "Switch to 'PreferNewMod' to enable Necromancer and disable Medic.",
- });
- }
- };
+ public ModdedToggleOption AllowRevenantHitmanCombo { get; } = new("Allow Revenant & Hitman in Same Match", false);
+ public ModdedEnumOption Compatibility { get; } = new("Mod Compatibility", ModPriority.PreferNewMod);
public enum ModPriority
{
PreferNewMod,
diff --git a/NewMod/Options/Roles/WraithCallerOptions/WraithCallerOptions.cs b/NewMod/Options/Roles/WraithCallerOptions/WraithCallerOptions.cs
new file mode 100644
index 0000000..ea85122
--- /dev/null
+++ b/NewMod/Options/Roles/WraithCallerOptions/WraithCallerOptions.cs
@@ -0,0 +1,24 @@
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.Attributes;
+using MiraAPI.GameOptions.OptionTypes;
+using NewMod.Roles.NeutralRoles;
+
+namespace NewMod.Options.Roles.WraithCallerOptions
+{
+ public class WraithCallerOptions : AbstractOptionGroup
+ {
+ public override string GroupName => "Wraith Caller";
+
+ [ModdedNumberOption("Wraith Summon Cooldown", min: 5, max: 60)]
+ public float CallWraithCooldown { get; set; } = 20f;
+
+ [ModdedNumberOption("Max Wraith Summons", min: 1, max: 5)]
+ public float CallWraithMaxUses { get; set; } = 3f;
+
+ [ModdedNumberOption("Required NPCs to Send", min: 1, max: 5)]
+ public float RequiredNPCsToSend { get; set; } = 2f;
+
+ [ModdedToggleOption("Show Summon Warnings")]
+ public bool ShowSummonWarnings { get; set; } = true;
+ }
+}
diff --git a/NewMod/Patches/Birthday/LobbyPatch.cs b/NewMod/Patches/Birthday/LobbyPatch.cs
new file mode 100644
index 0000000..5044c86
--- /dev/null
+++ b/NewMod/Patches/Birthday/LobbyPatch.cs
@@ -0,0 +1,85 @@
+using UnityEngine;
+using HarmonyLib;
+using System;
+using Object = UnityEngine.Object;
+using Reactor.Utilities.Extensions;
+using System.IO;
+
+namespace NewMod.Patches.Birthday
+{
+ [HarmonyPatch(typeof(LobbyBehaviour))]
+ public static class LobbyPatch
+ {
+ public static GameObject CustomLobby;
+ public static Toast ToastObj;
+ public static readonly Vector2[] BirthdaySpawns =
+ [
+ new Vector2(-0.6738f, -2.5016f),
+ new Vector2(-0.7068f, -2.4353f),
+ new Vector2(-0.4568f, -2.4353f),
+ new Vector2( 0.8968f, -2.2000f),
+ new Vector2( 1.6468f, -1.9000f),
+ new Vector2( 1.5218f, -1.9139f),
+ new Vector2( 2.5000f, -1.5155f),
+ new Vector2( 3.0000f, -1.5000f),
+ new Vector2( 3.0000f, -1.1000f)
+ ];
+
+ [HarmonyPatch(nameof(LobbyBehaviour.Start))]
+ [HarmonyPrefix]
+ public static bool StartPrefix(LobbyBehaviour __instance)
+ {
+ if (!NewModDateTime.IsNewModBirthdayWeek) return true;
+
+ CustomLobby = Object.Instantiate(NewModAsset.CustomLobby.LoadAsset());
+ CustomLobby.transform.SetParent(__instance.transform, false);
+ CustomLobby.transform.localPosition = Vector3.zero;
+ return true;
+ }
+
+ [HarmonyPatch(nameof(LobbyBehaviour.Start))]
+ [HarmonyPostfix]
+ public static void Postfix(LobbyBehaviour __instance)
+ {
+ ToastObj = Toast.CreateToast();
+ ToastObj.transform.localPosition = new Vector3(-4.4217f, 2.2098f, 0f);
+
+ if (DateTime.Now < NewModDateTime.NewModBirthday)
+ {
+ TimeSpan countdown = NewModDateTime.NewModBirthday - DateTime.Now;
+ ToastObj.StartCountdown(countdown);
+ }
+
+ if (!NewModDateTime.IsNewModBirthdayWeek) return;
+
+ var originalLobby = "Lobby(Clone)";
+ GameObject.Find(originalLobby).GetComponent().Destroy();
+ GameObject.Find(originalLobby + "/Background").SetActive(false);
+ GameObject.Find(originalLobby + "/ShipRoom").SetActive(false);
+ GameObject.Find(originalLobby + "/RightEngine").SetActive(false);
+ GameObject.Find(originalLobby + "/LeftEngine").SetActive(false);
+ GameObject.Find(originalLobby + "/SmallBox").SetActive(false);
+ GameObject.Find(originalLobby + "/Leftbox").SetActive(false);
+ GameObject.Find(originalLobby + "/RightBox").SetActive(false);
+
+ var wardrobe = GameObject.Find(originalLobby + "/panel_Wardrobe");
+ if (wardrobe != null)
+ {
+ wardrobe.transform.localPosition = new Vector3(4.6701f, -0.0529f, 0f);
+ wardrobe.transform.localScale = new Vector3(0.7301f, 0.7f, 1f);
+ }
+ __instance.SpawnPositions = new Vector2[BirthdaySpawns.Length];
+
+ for (int i = 0; i < BirthdaySpawns.Length; i++)
+ {
+ __instance.SpawnPositions[i] = BirthdaySpawns[i];
+ }
+ }
+ [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.Start))]
+ public static void Prefix(ShipStatus __instance)
+ {
+ CustomLobby.DestroyImmediate();
+ ToastObj.gameObject.SetActive(false);
+ }
+ }
+}
diff --git a/NewMod/Patches/Compatibility/LaunchpadCompatibility.cs b/NewMod/Patches/Compatibility/LaunchpadCompatibility.cs
index e58a5a7..ad3d5ba 100644
--- a/NewMod/Patches/Compatibility/LaunchpadCompatibility.cs
+++ b/NewMod/Patches/Compatibility/LaunchpadCompatibility.cs
@@ -1,4 +1,3 @@
-using HarmonyLib;
using NewMod.Roles.ImpostorRoles;
using System.Reflection;
using TMPro;
@@ -33,6 +32,7 @@ static bool Prefix(object __instance)
return true;
}
}
+
public static class LaunchpadHackTextPatch
{
static MethodBase TargetMethod()
@@ -57,4 +57,28 @@ static void Postfix(object __instance)
}
}
}
+
+ public static class LaunchpadTagSpacingPatch
+ {
+ static MethodBase TargetMethod()
+ {
+ if (!ModCompatibility.LaunchpadLoaded(out var asm) || asm == null)
+ return null;
+
+ var type = asm.GetType("LaunchpadReloaded.Components.PlayerTagManager");
+ var method = type?.GetMethod("UpdatePosition", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ return method;
+ }
+
+ static void Postfix(object __instance)
+ {
+ var type = __instance.GetType();
+ var tagHolderObj = type.GetField("tagHolder", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(__instance);
+ if (tagHolderObj is Transform holder)
+ {
+ holder.localPosition = new Vector3(0f, 0.5491f, -0.35f);
+ holder.localScale = new Vector3(0.7455f, 1f, 1f);
+ }
+ }
+ }
}
diff --git a/NewMod/Patches/Compatibility/StartGamePatch.cs b/NewMod/Patches/Compatibility/StartGamePatch.cs
index d88e8be..e5fce8c 100644
--- a/NewMod/Patches/Compatibility/StartGamePatch.cs
+++ b/NewMod/Patches/Compatibility/StartGamePatch.cs
@@ -30,14 +30,6 @@ public static bool Prefix(AmongUsClient __instance)
}
}
}
- if (settings.Compatibility == CompatibilityOptions.ModPriority.PreferNewMod)
- {
- ModCompatibility.DisableRole("Medic", ModCompatibility.LaunchpadReloaded_GUID);
- }
- else
- {
- ModCompatibility.DisableRole("Necromancer", NewMod.Id);
- }
return true;
}
}
diff --git a/NewMod/Patches/CustomPlayerTagPatch.cs b/NewMod/Patches/CustomPlayerTagPatch.cs
new file mode 100644
index 0000000..6ddfa37
--- /dev/null
+++ b/NewMod/Patches/CustomPlayerTagPatch.cs
@@ -0,0 +1,61 @@
+using HarmonyLib;
+using UnityEngine;
+
+namespace NewMod.Patches
+{
+ [HarmonyPatch]
+ public static class CustomPlayerTagPatch
+ {
+ public const float Padding = 0.02f;
+ private static float targetY;
+
+ [HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetName))]
+ [HarmonyPostfix]
+ public static void SetNamePostfix(ChatBubble __instance, string playerName, bool isDead, bool voted, Color color)
+ {
+ __instance.NameText.ForceMeshUpdate();
+
+ float nameBottom = __instance.NameText.textBounds.min.y;
+ float nameLocalY = __instance.NameText.transform.localPosition.y;
+
+ targetY = nameLocalY + nameBottom - Padding;
+ }
+
+ [HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetText))]
+ [HarmonyPostfix]
+ public static void SetTextPostfix(ChatBubble __instance, string chatText)
+ {
+ var pos = __instance.TextArea.transform.localPosition;
+ pos.y = targetY;
+
+ __instance.TextArea.transform.localPosition = pos;
+ __instance.AlignChildren();
+ }
+ [HarmonyPatch(typeof(NotificationPopper), nameof(NotificationPopper.AddDisconnectMessage))]
+ [HarmonyPrefix]
+ public static void StartPrefix(NotificationPopper __instance, ref string item)
+ {
+ item = item.Replace("\r", "").Replace("\n", "");
+ while (item.Contains(" ")) item = item.Replace(" ", " ");
+
+ item = item.Replace("", "");
+ item = item.Replace("", "");
+
+ _ = item.TrimEnd();
+ }
+ [HarmonyPatch(typeof(ChatNotification), nameof(ChatNotification.SetUp))]
+ [HarmonyPrefix]
+ public static void StartPrefix(ChatNotification __instance, PlayerControl sender, ref string text)
+ {
+ if (text.Contains('\n'))
+ text = text.Replace("\r", "").Replace("\n", " ");
+
+ text = text.Replace("", "");
+
+ while (text.Contains(" "))
+ text = text.Replace(" ", " ");
+
+ text = text.TrimEnd();
+ }
+ }
+}
diff --git a/NewMod/Patches/EndGamePatch.cs b/NewMod/Patches/EndGamePatch.cs
index 99d848f..e27777c 100644
--- a/NewMod/Patches/EndGamePatch.cs
+++ b/NewMod/Patches/EndGamePatch.cs
@@ -18,6 +18,7 @@
using NewMod.Options.Roles.PulseBladeOptions;
using MiraAPI.Utilities;
using NewMod.Options.Roles.EnergyThiefOptions;
+using NewMod.Options.Roles.WraithCallerOptions;
namespace NewMod.Patches
{
@@ -125,6 +126,11 @@ public static void OnGameEnd(GameEndEvent evt)
customWinColor = GetRoleColor(GetRoleType());
endGameManager.BackgroundBar.material.SetColor("_Color", customWinColor);
break;
+ case (GameOverReason)NewModEndReasons.WraithCallerWin:
+ customWinText = "NPC Invasion Completed\nWraith Caller Win!";
+ customWinColor = GetRoleColor(GetRoleType());
+ endGameManager.BackgroundBar.material.SetColor("_Color", customWinColor);
+ break;
default:
customWinText = string.Empty;
customWinColor = Color.white;
@@ -159,7 +165,7 @@ public static string GetRoleName(CachedPlayerData playerData, out Color roleColo
if (customRole is INewModRole newmodRole)
{
- return $"{newmodRole.RoleName}\n{Utils.GetFactionDisplay()}";
+ return $"{newmodRole.RoleName}\n{Utils.GetFactionDisplay((INewModRole)customRole)}";
}
return customRole.RoleName;
}
@@ -210,6 +216,7 @@ public static class CheckGameEndPatch
public static bool Prefix(ShipStatus __instance)
{
if (DestroyableSingleton.InstanceExists) return true;
+ if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.WraithCallerWin)) 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;
@@ -263,6 +270,12 @@ public static bool CheckForEndGameFaction(ShipStatus __instance, GameO
}
}
}
+ if (typeof(TFaction) == typeof(WraithCaller))
+ {
+ int required = (int)OptionGroupSingleton.Instance.RequiredNPCsToSend;
+ int current = WraithCallerUtilities.GetKillsNPC(player.PlayerId);
+ shouldEndGame = current >= required;
+ }
if (shouldEndGame)
{
GameManager.Instance.RpcEndGame(winReason, false);
diff --git a/NewMod/Patches/GameOptionsMenu.cs b/NewMod/Patches/GameOptionsMenu.cs
new file mode 100644
index 0000000..90ff21d
--- /dev/null
+++ b/NewMod/Patches/GameOptionsMenu.cs
@@ -0,0 +1,27 @@
+using HarmonyLib;
+using MiraAPI.GameOptions;
+using NewMod.Options;
+
+namespace NewMod.Patches;
+
+[HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CloseMenu))]
+public static class GameOptionsMenu_CloseMenu_Patch
+{
+ public static void Postfix(GameOptionsMenu __instance)
+ {
+ var opts = OptionGroupSingleton.Instance;
+
+ if (opts.AllowRevenantHitmanCombo.Value)
+ {
+ HudManager.Instance.ShowPopUp(
+ "You enabled the Revenant & Hitman combo. This may break game balance!"
+ );
+ }
+ else
+ {
+ HudManager.Instance.ShowPopUp(
+ "Revenant & Hitman combo disabled. Only one will be allowed per match."
+ );
+ }
+ }
+}
diff --git a/NewMod/Patches/MainMenuPatch.cs b/NewMod/Patches/MainMenuPatch.cs
index e4cbe8b..d432547 100644
--- a/NewMod/Patches/MainMenuPatch.cs
+++ b/NewMod/Patches/MainMenuPatch.cs
@@ -1,25 +1,108 @@
using UnityEngine;
using HarmonyLib;
+using NewMod.Roles.NeutralRoles;
+using MiraAPI;
+using MiraAPI.PluginLoading;
+using MiraAPI.Roles;
+using System.Collections.Generic;
+using System.Reflection;
+using System;
namespace NewMod.Patches
{
- [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start))]
+ [HarmonyPatch(typeof(MainMenuManager))]
[HarmonyPriority(Priority.VeryHigh)]
public static class MainMenuPatch
{
public static SpriteRenderer LogoSprite;
+ public static Texture2D _cachedCursor;
+ public static Transform RightPanel;
+ public static bool _wraithRegistered = false;
+ [HarmonyPatch(nameof(MainMenuManager.Start))]
[HarmonyPostfix]
public static void StartPostfix(MainMenuManager __instance)
{
- var newparent = __instance.transform.FindChild("MainCanvas/MainPanel/RightPanel");
- var Logo = new GameObject("NewModLogo");
- Logo.transform.SetParent(newparent, false);
- Logo.transform.localPosition = new(2.34f, -0.7136f, 1f);
- LogoSprite = Logo.AddComponent();
- LogoSprite.sprite = NewModAsset.ModLogo.LoadAsset();
-
+ if (_cachedCursor == null)
+ {
+ _cachedCursor = NewModAsset.CustomCursor.LoadAsset().texture;
+ }
+ if (_cachedCursor != null)
+ {
+ Cursor.SetCursor(_cachedCursor, CursorMode.Auto);
+ }
+ RightPanel = __instance.transform.Find("MainUI/AspectScaler/RightPanel");
+
+ if ((NewModDateTime.IsNewModBirthdayWeek || NewModDateTime.IsWraithCallerUnlocked) && !_wraithRegistered)
+ {
+ RegisterWraithCaller();
+ _wraithRegistered = true;
+ }
+
+ if (NewModDateTime.IsNewModBirthdayWeek)
+ {
+ RightPanel.gameObject.SetActive(false);
+ __instance.screenTint.enabled = false;
+
+ var auLogo = GameObject.Find("LOGO-AU");
+ auLogo.transform.localPosition = new Vector3(-3.50f, 1.85f, 0f);
+ auLogo.transform.localScale = new Vector3(0.32f, 0.32f, 1f);
+
+ var newmodLogo = new GameObject("NewModLogo");
+ var parent = __instance.transform.Find("MainUI/AspectScaler/LeftPanel");
+ newmodLogo.transform.SetParent(parent, false);
+ newmodLogo.transform.localPosition = new Vector3(-0.1427f, 2.8094f, 0.7182f);
+ newmodLogo.transform.localScale = new Vector3(0.3711f, 0.4214f, 1.16f);
+ LogoSprite = newmodLogo.AddComponent();
+ LogoSprite.sprite = NewModAsset.ModLogo.LoadAsset();
+
+ var auBG = __instance.transform.Find("MainUI/AspectScaler/BackgroundTexture").GetComponent();
+ if (auBG != null)
+ {
+ auBG.sprite = NewModAsset.MainMenuBG.LoadAsset();
+ }
+ }
+ else
+ {
+ // Preserve the old layout when it's not the birthday update
+ var Logo = new GameObject("NewModLogo");
+ Logo.transform.SetParent(__instance.transform.Find("MainCanvas/MainPanel/RightPanel"), false);
+ Logo.transform.localPosition = new Vector3(2.34f, -0.7136f, 1f);
+ LogoSprite = Logo.AddComponent();
+ LogoSprite.sprite = NewModAsset.ModLogo.LoadAsset();
+ }
ModCompatibility.Initialize();
}
+ public static void RegisterWraithCaller()
+ {
+ var roleType = typeof(WraithCaller);
+ var customRoleManager = typeof(CustomRoleManager);
+ var registerTypes = customRoleManager.GetMethod("RegisterRoleTypes", BindingFlags.NonPublic | BindingFlags.Static);
+ var registerInManager = customRoleManager.GetMethod("RegisterInRoleManager", BindingFlags.NonPublic | BindingFlags.Static);
+ var plugin = MiraPluginManager.GetPluginByGuid(NewMod.Id);
+ registerTypes.Invoke(null, [new List { roleType }, plugin]);
+ registerInManager.Invoke(null, null);
+ }
+ [HarmonyPatch(nameof(MainMenuManager.OpenGameModeMenu))]
+ [HarmonyPatch(nameof(MainMenuManager.OpenCredits))]
+ [HarmonyPatch(nameof(MainMenuManager.OpenAccountMenu))]
+ [HarmonyPatch(nameof(MainMenuManager.OpenCreateGame))]
+ [HarmonyPatch(nameof(MainMenuManager.OpenEnterCodeMenu))]
+ [HarmonyPatch(nameof(MainMenuManager.OpenOnlineMenu))]
+ [HarmonyPatch(nameof(MainMenuManager.OpenFindGame))]
+ public static void Postfix(MainMenuManager __instance)
+ {
+ if (!NewModDateTime.IsNewModBirthdayWeek) return;
+
+ RightPanel.gameObject.SetActive(true);
+ }
+ [HarmonyPatch(nameof(MainMenuManager.ResetScreen))]
+ [HarmonyPostfix]
+ public static void ResetScreenPostfix(MainMenuManager __instance)
+ {
+ if (!NewModDateTime.IsNewModBirthdayWeek) return;
+
+ RightPanel.gameObject.SetActive(false);
+ }
}
}
diff --git a/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs b/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs
index 3e2653b..7e989b8 100644
--- a/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs
+++ b/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs
@@ -1,5 +1,4 @@
using HarmonyLib;
-using MiraAPI.Hud;
using NewMod.Utilities;
using NewMod.Roles.ImpostorRoles;
@@ -17,6 +16,7 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)] EndGam
Utils.ResetStrikeCount();
PranksterUtilities.ResetReportCount();
VisionaryUtilities.DeleteAllScreenshots();
+ WraithCallerUtilities.ClearAll();
Revenant.HasUsedFeignDeath = false;
Revenant.FeignDeathStates.Remove(PlayerControl.LocalPlayer.PlayerId);
Revenant.StalkingStates[PlayerControl.LocalPlayer.PlayerId] = false;
diff --git a/NewMod/Resources/Birthday/newmod-birthday-v1.png b/NewMod/Resources/Birthday/newmod-birthday-v1.png
new file mode 100644
index 0000000..58f17cf
Binary files /dev/null and b/NewMod/Resources/Birthday/newmod-birthday-v1.png differ
diff --git a/NewMod/Resources/Logo.png b/NewMod/Resources/Logo.png
index c7941e9..7d4c6bd 100644
Binary files a/NewMod/Resources/Logo.png and b/NewMod/Resources/Logo.png differ
diff --git a/NewMod/Resources/RoleIcons/wraith.png b/NewMod/Resources/RoleIcons/wraith.png
new file mode 100644
index 0000000..1869565
Binary files /dev/null and b/NewMod/Resources/RoleIcons/wraith.png differ
diff --git a/NewMod/Resources/callwraith.png b/NewMod/Resources/callwraith.png
new file mode 100644
index 0000000..9d1e3b1
Binary files /dev/null and b/NewMod/Resources/callwraith.png differ
diff --git a/NewMod/Resources/cursor.png b/NewMod/Resources/cursor.png
new file mode 100644
index 0000000..0591eaa
Binary files /dev/null and b/NewMod/Resources/cursor.png differ
diff --git a/NewMod/Resources/final.png b/NewMod/Resources/final.png
new file mode 100644
index 0000000..63ac2d8
Binary files /dev/null and b/NewMod/Resources/final.png differ
diff --git a/NewMod/Resources/newmod-android.bundle b/NewMod/Resources/newmod-android.bundle
new file mode 100644
index 0000000..961e761
Binary files /dev/null and b/NewMod/Resources/newmod-android.bundle differ
diff --git a/NewMod/Resources/newmod-win.bundle b/NewMod/Resources/newmod-win.bundle
new file mode 100644
index 0000000..d56ea00
Binary files /dev/null and b/NewMod/Resources/newmod-win.bundle differ
diff --git a/NewMod/Roles/INewModRole.cs b/NewMod/Roles/INewModRole.cs
index 584c83f..26201e0 100644
--- a/NewMod/Roles/INewModRole.cs
+++ b/NewMod/Roles/INewModRole.cs
@@ -5,13 +5,18 @@
namespace NewMod.Roles
{
+ #pragma warning disable CS0108
public interface INewModRole : ICustomRole
{
+ ///
+ /// The faction associated with the current role.
+ ///
+ public NewModFaction Faction { get; }
public static StringBuilder GetRoleTabText(ICustomRole role)
{
var sb = new StringBuilder();
sb.AppendLine($"{role.RoleColor.ToTextColor()}You are {role.RoleName}");
- sb.AppendLine($"Faction: {Utils.GetFactionDisplay()}");
+ sb.AppendLine($"Faction: {Utils.GetFactionDisplay((INewModRole)role)}");
sb.AppendLine($"{role.RoleLongDescription}");
return sb;
}
diff --git a/NewMod/Roles/NeutralRoles/Overload.cs b/NewMod/Roles/NeutralRoles/Overload.cs
index 8153e17..c0548ea 100644
--- a/NewMod/Roles/NeutralRoles/Overload.cs
+++ b/NewMod/Roles/NeutralRoles/Overload.cs
@@ -7,6 +7,8 @@
using MiraAPI.Hud;
using UnityEngine.Events;
using NewMod.Utilities;
+using NewMod.Buttons.Overload;
+using System.Collections.Generic;
namespace NewMod.Roles.NeutralRoles;
@@ -20,6 +22,7 @@ public class OverloadRole : ImpostorRole, ICustomRole
public RoleOptionsGroup RoleOptionsGroup { get; } = RoleOptionsGroup.Neutral;
public static int AbsorbedAbilityCount = 0;
public static PlayerControl chosenPrey;
+ public static List CachedButtons = new();
public CustomRoleConfiguration Configuration => new(this)
{
AffectedByLightOnAirship = false,
@@ -40,6 +43,7 @@ public static void OnRoundStart(RoundStartEvent evt)
if (evt.TriggeredByIntro)
{
+ CustomButtonSingleton.Instance.absorbed = null;
AbsorbedAbilityCount = 0;
chosenPrey = null;
@@ -65,18 +69,4 @@ public static IEnumerator CoShowMenu(float delay)
}
yield return null;
}
- public static void UnlockFinalAbility()
- {
- var btn = Instantiate(HudManager.Instance.AbilityButton, HudManager.Instance.AbilityButton.transform.parent);
- btn.name = "FinalButton";
- btn.transform.SetParent(HudManager.Instance.transform.Find("Buttons"), false);
- btn.GetComponent().anchorMin = btn.GetComponent().anchorMax = btn.GetComponent().pivot = new Vector2(0.5f, 0.5f);
- btn.GetComponent().anchoredPosition = Vector2.zero;
- btn.GetComponent().sizeDelta = Vector2.zero;
- btn.OverrideText("OVERLOAD");
- btn.transform.SetAsLastSibling();
- var passive = btn.GetComponent();
- passive.OnClick.RemoveAllListeners();
- passive.OnClick.AddListener((UnityAction)(() => GameManager.Instance.RpcEndGame((GameOverReason)NewModEndReasons.OverloadWin, false)));
- }
}
diff --git a/NewMod/Roles/NeutralRoles/Tyrant.cs b/NewMod/Roles/NeutralRoles/Tyrant.cs
index 4859715..ad99d10 100644
--- a/NewMod/Roles/NeutralRoles/Tyrant.cs
+++ b/NewMod/Roles/NeutralRoles/Tyrant.cs
@@ -31,7 +31,7 @@ public sealed class Tyrant : ImpostorRole, INewModRole
public Color RoleColor => new(0.78f, 0.10f, 0.16f, 1f);
public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
public RoleOptionsGroup RoleOptionsGroup { get; } = RoleOptionsGroup.Neutral;
- public NewModFaction Faction = NewModFaction.Apex;
+ public NewModFaction Faction => NewModFaction.Apex;
public CustomRoleConfiguration Configuration => new(this)
{
MaxRoleCount = 1,
diff --git a/NewMod/Roles/NeutralRoles/WraithCaller.cs b/NewMod/Roles/NeutralRoles/WraithCaller.cs
new file mode 100644
index 0000000..f51db32
--- /dev/null
+++ b/NewMod/Roles/NeutralRoles/WraithCaller.cs
@@ -0,0 +1,70 @@
+using System.Text;
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.GameOptions;
+using MiraAPI.PluginLoading;
+using MiraAPI.Roles;
+using NewMod.Options.Roles.WraithCallerOptions;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Roles.NeutralRoles
+{
+ [MiraIgnore]
+ public class WraithCaller : ImpostorRole, INewModRole
+ {
+ public string RoleName => "Wraith Caller";
+ public string RoleDescription => "Summon. Lurk. Reap.";
+ public string RoleLongDescription => "Summon spectral NPCs that slip through walls and hunt down your marked target.";
+ public Color RoleColor => new(0.58f, 0.20f, 0.90f);
+ public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
+ public NewModFaction Faction => NewModFaction.Entropy;
+ public CustomRoleConfiguration Configuration => new(this)
+ {
+ AffectedByLightOnAirship = false,
+ CanUseSabotage = false,
+ CanUseVent = false,
+ UseVanillaKillButton = false,
+ TasksCountForProgress = false,
+ Icon = NewModAsset.WraithIcon
+ };
+
+ [HideFromIl2Cpp]
+ public StringBuilder SetTabText()
+ {
+ var tab = INewModRole.GetRoleTabText(this);
+ var playerId = PlayerControl.LocalPlayer.PlayerId;
+
+ int sent = WraithCallerUtilities.GetSentNPC(playerId);
+ int kills = WraithCallerUtilities.GetKillsNPC(playerId);
+
+ int required = (int)OptionGroupSingleton.Instance.RequiredNPCsToSend;
+ bool showWarn = OptionGroupSingleton.Instance.ShowSummonWarnings;
+
+ string cyan = ColorUtility.ToHtmlStringRGBA(Color.cyan);
+ string yellow = ColorUtility.ToHtmlStringRGBA(Color.yellow);
+ string green = ColorUtility.ToHtmlStringRGBA(Palette.AcceptedGreen);
+
+ tab.AppendLine();
+
+ tab.AppendLine($"Sent: {sent}");
+ tab.AppendLine($"Kills: = required ? green : cyan)}>{kills}/{required}");
+
+ if (kills < required)
+ {
+ int left = required - kills;
+ tab.AppendLine($"{left} more successful kill{(left == 1 ? "" : "s")} to win.");
+ }
+ else
+ {
+ tab.AppendLine($"Win condition armed. Survive to claim victory.");
+ }
+
+ if (showWarn)
+ {
+ tab.AppendLine();
+ tab.AppendLine($"Tip: Time your summons. Meetings cancel hunts.");
+ }
+ return tab;
+ }
+ }
+}
diff --git a/NewMod/Utilities/Utils.cs b/NewMod/Utilities/Utils.cs
index 2aa4ea6..eca5f22 100644
--- a/NewMod/Utilities/Utils.cs
+++ b/NewMod/Utilities/Utils.cs
@@ -22,6 +22,7 @@
using NewMod.Options.Roles.InjectorOptions;
using MiraAPI.Hud;
using NewMod.Buttons.Pulseblade;
+using NewMod.Roles;
namespace NewMod.Utilities
{
@@ -75,10 +76,6 @@ public static class Utils
///
public static readonly Dictionary StrikeKills = new();
- ///
- /// The faction associated with the current role.
- ///
- public static NewModFaction Faction { get; }
///
/// Retrieves a PlayerControl instance by its player ID.
///
@@ -735,9 +732,9 @@ public static void RpcMissionFails(PlayerControl source, PlayerControl target)
}
}
- public static string GetFactionDisplay()
+ public static string GetFactionDisplay(INewModRole role)
{
- return Faction switch
+ return role.Faction switch
{
NewModFaction.Apex => $"Apex",
NewModFaction.Entropy => $"Entropy",
@@ -1072,6 +1069,19 @@ public static IEnumerator CoShakeCamera(FollowerCamera cam, float duration)
}
cam.transform.localPosition = originalPos;
}
+
+ ///
+ /// Formats a into a string with the format:
+ /// dd:hh:mm:ss.
+ ///
+ /// The to format.
+ public static string FormatSpan(System.TimeSpan t)
+ {
+ int dd = Mathf.Max(0, t.Days);
+ int hh = Mathf.Clamp(t.Hours, 0, 99);
+ int mm = Mathf.Clamp(t.Minutes, 0, 59);
+ int ss = Mathf.Clamp(t.Seconds, 0, 59);
+ return $"{dd:D1}:{hh:D2}:{mm:D2}:{ss:D2}";
+ }
}
}
-
diff --git a/NewMod/Utilities/WraithCallerUtilities.cs b/NewMod/Utilities/WraithCallerUtilities.cs
new file mode 100644
index 0000000..8323c7c
--- /dev/null
+++ b/NewMod/Utilities/WraithCallerUtilities.cs
@@ -0,0 +1,88 @@
+ using System.Collections.Generic;
+ using NewMod.Components;
+ using Reactor.Networking.Attributes;
+ using UnityEngine;
+
+ namespace NewMod.Utilities
+ {
+ public static class WraithCallerUtilities
+ {
+ ///
+ /// A dictionary holding the number of NPCs sent by each Wraith Caller.
+ ///
+ private static readonly Dictionary Sent = [];
+
+ ///
+ /// A dictionary holding the number of kills achieved by NPCs summoned by each Wraith Caller.
+ ///
+ private static readonly Dictionary Kills = [];
+
+ ///
+ /// Returns the number of NPCs sent by the specified owner.
+ ///
+ public static int GetSentNPC(byte ownerId)
+ {
+ return Sent.TryGetValue(ownerId, out var v) ? v : 0;
+ }
+
+ ///
+ /// Returns the number of successful kills credited to the ownerβs wraiths.
+ ///
+ public static int GetKillsNPC(byte ownerId)
+ {
+ return Kills.TryGetValue(ownerId, out var v) ? v : 0;
+ }
+
+ ///
+ /// Increments the number of NPCs sent by the owner.
+ ///
+ public static void AddSentNPC(byte ownerId, int amount = 1)
+ {
+ Sent[ownerId] = GetSentNPC(ownerId) + amount;
+ }
+
+ ///
+ /// Increments the number of kills credited to the ownerβs wraiths.
+ ///
+ public static void AddKillNPC(byte ownerId, int amount = 1)
+ {
+ Kills[ownerId] = GetKillsNPC(ownerId) + amount;
+ }
+
+ ///
+ /// Clears all counters for both NPCs sent and kills achieved.
+ ///
+ public static void ClearAll()
+ {
+ Sent.Clear();
+ Kills.Clear();
+ }
+
+ ///
+ /// RPC for . Runs on all clients to keep state in sync.
+ ///
+ /// The Wraith Caller owner who summoned the NPC.
+ /// The intended target player.
+
+ [MethodRpc((uint)CustomRPC.SummonNPC)]
+ public static void RpcSummonNPC(PlayerControl source, PlayerControl target)
+ {
+ AddSentNPC(source.PlayerId);
+ SummonNPC(source, target);
+ }
+
+ ///
+ /// Spawns and initializes the wraith NPC that will hunt the target.
+ /// Runs locally on each client after the RPC dispatch.
+ ///
+ /// The Wraith Caller (owner) who summoned the NPC.
+ /// The intended target player.
+ public static void SummonNPC(PlayerControl wraith, PlayerControl target)
+ {
+ var npcObj = new GameObject("WraithNPC_Holder");
+ var wraithNpc = npcObj.AddComponent();
+
+ wraithNpc.Initialize(wraith, target);
+ }
+ }
+ }
diff --git a/README.md b/README.md
index bed53c1..97f4eb2 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@
- [π Releases](#-releases)
- [π₯ Installation](#-installation)
- [β¨ Features](#-features)
+- [π Birthday Special](#-birthday-special)
- [π Compatibility](#-compatibility)
- [π€ Contributing](#-contributing)
- [π± Android](#-android)
@@ -46,6 +47,7 @@
| v1.1.0 | 2024.11.26 | [Download](https://github.com/CallOfCreator/NewMod/releases/download/V1.1.0/NewMod.dll) |
| v1.2.0 | v16.1.0 (2025.6.10) | [Download](https://github.com/CallOfCreator/NewMod/releases/download/V1.2.0/NewMod.zip)
| v1.2.1 | v16.1.0 (2025.6.10) | [Download](https://github.com/CallOfCreator/NewMod/releases/download/V1.2.1/NewMod.zip) |
+| v1.2.4 | v16.1.0 (2025.6.10) | [Download](https://github.com/CallOfCreator/NewMod/releases/download/V1.2.4/NewMod.zip) |
---
@@ -55,6 +57,26 @@
---
+
+## π Birthday Special
+
+This year on **August 28th**, NewMod turns **1 year old**!
+A huge thanks to everyone who has supported the mod so far. π
+
+Hereβs what you can expect for the birthday update:
+
+- **Custom Player Tags**: Show off tags under your name like *Player*, *Dev*, etc.
+- **Custom Lobby**: Check it out:
+
+
+
+- **Custom Cursor (Permanent)**: A brand-new cursor style for NewMod, here to stay.
+- **Special Role: The Wraith Caller**: One of our best roles yet! Summon NPCs through walls to take down your opponents.
+
+You can enjoy all these birthday features from **August 28th, 16:00** until **September 4th** even on **Android via Starlight**!
+
+---
+
# β¨ Features
### **1. Crewmate Cam Access (F2)**
@@ -69,11 +91,11 @@
NewMod is compatible with the following mods, enabling an enhanced experience with combined custom roles. Below are the supported versions of each mod:
-| Mod Name | Mod Version | GitHub Link |
-|--------------|-------------|------------------------------------------------------|
-| yanplaRoles | v0.1.6+ | [Download](https://github.com/yanpla/yanplaRoles) |
-| LaunchpadReloaded | v0.3.4+ | [Download](https://github.com/All-Of-Us-Mods/LaunchpadReloaded) |
-| LevelImposter | v0.20.3+ | [Download](https://github.com/DigiWorm0/LevelImposter) |
+| Mod ame | Mod Version | GitHub Link | Status |
+|-------------------|-------------|------------------------------------------------------|-------------|
+| yanplaRoles | v0.1.6+ | [Download](https://github.com/yanpla/yanplaRoles) | β Deprecated |
+| 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 |
---
@@ -101,6 +123,7 @@ For more information about Starlight, please visit: [https://discord.gg/FYYqJU2b
- **yanplaRoles**: [yanplaRoles](https://github.com/yanpla/yanplaRoles) - Portions of code (SavePlayerRole, GetPlayerRolesHistory).
- **EloySus**: [EloySus](https://github.com/EloySus) β for all button sprites used in NewMod
- **Pixabay**: [Pixabay](https://pixabay.com) - For sound effects used in NewMod
+- **angxlwtf**: [angxlwtf](https://github.com/angxlwtf) - Idea for **Wraith Caller** (originally for Hitman LP)
---