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: +

+ Birthday Custom Lobby Preview +

+- **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) ---