diff --git a/.gitignore b/.gitignore index 74c2aaa..7c0fbec 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ obj/ References/ NewMod/Components/Minigames NewMod/Components/Hidden.cs +NewMod/Private /packages/ riderModule.iml .idea diff --git a/NewMod/Buttons/Necromancer/ReviveButton.cs b/NewMod/Buttons/Necromancer/ReviveButton.cs index e9b717b..d35f4da 100644 --- a/NewMod/Buttons/Necromancer/ReviveButton.cs +++ b/NewMod/Buttons/Necromancer/ReviveButton.cs @@ -6,6 +6,10 @@ using UnityEngine; using NewMod.Utilities; using MiraAPI.Keybinds; +using AmongUs.GameOptions; +using Reactor.Utilities; +using MiraAPI.Utilities; +using System.Linq; namespace NewMod.Buttons.Necromancer { @@ -54,15 +58,29 @@ public class ReviveButton : CustomActionButton /// protected override void OnClick() { + var local = PlayerControl.LocalPlayer; + + var body = Helpers.GetNearestDeadBodies( + local.GetTruePosition(), + ShipStatus.Instance.MaxLightRadius, + Helpers.CreateFilter(Constants.NotShipMask)) + .Where(b => b != null && !PranksterUtilities.IsPranksterBody(b)) + .OrderBy(b => Vector2.Distance(local.GetTruePosition(), b.TruePosition)) + .FirstOrDefault(); + + if (body == null) return; + SoundManager.Instance.PlaySound(NewModAsset.ReviveSound?.LoadAsset(), false, 2f); - var closestBody = Utils.GetClosestBody(); - if (closestBody != null) - { - Utils.RpcRevive(closestBody); - } + Utils.HandleRevive( + local, + body.ParentId, + RoleTypes.Crewmate, + body.transform.position.x, + body.transform.position.y + ); + NecromancerRole.RevivedPlayers[body.ParentId] = local.PlayerId; } - /// /// Determines whether this button is enabled for the role, returning true if the role is . /// @@ -79,32 +97,26 @@ public override bool Enabled(RoleBehaviour role) /// True if all requirements to use this button are met; otherwise false. public override bool CanUse() { - bool isTimerDone = Timer <= 0; - bool hasUsesLeft = UsesLeft > 0; - var closestBody = Utils.GetClosestBody(); - bool isNearDeadBody = closestBody != null; - bool isFakeBody = isNearDeadBody && PranksterUtilities.IsPranksterBody(closestBody); + var bodiesInRange = Helpers.GetNearestDeadBodies( + PlayerControl.LocalPlayer.transform.position, + ShipStatus.Instance.MaxLightRadius, + Helpers.CreateFilter(Constants.NotShipMask)); - if (closestBody == null) + bool canUse = bodiesInRange.Any(body => { - return false; - } + if (PranksterUtilities.IsPranksterBody(body)) return false; - bool wasNotKilledByNecromancer = true; - var deadBody = closestBody.GetComponent(); - if (deadBody != null) - { - var killedPlayer = GameData.Instance.GetPlayerById(deadBody.ParentId)?.Object; - if (killedPlayer != null) - { - var killer = Utils.GetKiller(killedPlayer); - if (killer != null && killer.Data.Role is NecromancerRole) - { - wasNotKilledByNecromancer = false; - } - } - } - return isTimerDone && hasUsesLeft && isNearDeadBody && wasNotKilledByNecromancer && !isFakeBody; + var killedPlayer = GameData.Instance.GetPlayerById(body.ParentId)?.Object; + if (killedPlayer == null) return false; + + var killer = Utils.GetKiller(killedPlayer); + if (killer.PlayerId == PlayerControl.LocalPlayer.PlayerId) + return false; + + return true; + }); + + return canUse; } } } diff --git a/NewMod/Buttons/RevivedKillButton.cs b/NewMod/Buttons/RevivedKillButton.cs new file mode 100644 index 0000000..b32a61d --- /dev/null +++ b/NewMod/Buttons/RevivedKillButton.cs @@ -0,0 +1,63 @@ +using MiraAPI.Hud; +using MiraAPI.Keybinds; +using MiraAPI.Utilities.Assets; +using NewMod.Roles.ImpostorRoles; +using Reactor.Utilities; +using AmongUs.GameOptions; +using System.Linq; +using UnityEngine; +using MiraAPI.Networking; +using MiraAPI.Utilities; + +namespace NewMod.Buttons +{ + public class RevivedKillButton : CustomActionButton + { + public override string Name => "KILL"; + public override float Cooldown => GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown); + public override int MaxUses => 0; + public override float EffectDuration => 0f; + public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility; + public override ButtonLocation Location => ButtonLocation.BottomRight; + public override LoadableAsset Sprite => NewModAsset.VanillaKillButton; + public override bool Enabled(RoleBehaviour role) + { + return NecromancerRole.RevivedPlayers.ContainsKey(PlayerControl.LocalPlayer.PlayerId); + } + public override PlayerControl GetTarget() + { + return PlayerControl.LocalPlayer.GetClosestPlayer(true, Distance); + } + public override bool IsTargetValid(PlayerControl target) + { + return target.PlayerId != PlayerControl.LocalPlayer.PlayerId; + } + public override void SetOutline(bool active) + { + Target.cosmetics.SetOutline(active, new Il2CppSystem.Nullable(Palette.ImpostorRed)); + } + + public override bool CanUse() + { + if (!NecromancerRole.RevivedPlayers.ContainsKey(PlayerControl.LocalPlayer.PlayerId)) return false; + return true; + } + + protected override void OnClick() + { + var local = PlayerControl.LocalPlayer; + + local.RpcCustomMurder( + Target, + didSucceed: true, + resetKillTimer: true, + createDeadBody: true, + teleportMurderer: true, + showKillAnim: true, + playKillSound: true + ); + + NecromancerRole.RevivedPlayers.Remove(local.PlayerId); + } + } +} \ No newline at end of file diff --git a/NewMod/Buttons/WraithCaller/CallWraith.cs b/NewMod/Buttons/WraithCaller/CallWraith.cs index 7a1031f..a3d8e75 100644 --- a/NewMod/Buttons/WraithCaller/CallWraith.cs +++ b/NewMod/Buttons/WraithCaller/CallWraith.cs @@ -85,7 +85,7 @@ protected override void OnClick() player => { menu.Close(); - WraithCallerUtilities.RpcSummonNPC(PlayerControl.LocalPlayer, player); + WraithCallerUtilities.RpcRequestSummonNPC(PlayerControl.LocalPlayer, player); SetTimerPaused(false); }); diff --git a/NewMod/Components/FearPulseArea.cs b/NewMod/Components/FearPulseArea.cs index 989b31f..bdcd8a5 100644 --- a/NewMod/Components/FearPulseArea.cs +++ b/NewMod/Components/FearPulseArea.cs @@ -13,16 +13,25 @@ namespace NewMod.Components public class FearPulseArea(IntPtr ptr) : MonoBehaviour(ptr) { public byte ownerId; - float _radius, _duration, _speedMul, _t; + + float _radius; + float _duration; + float _speedMul; + float _t; + readonly Dictionary _origSpeed = new(); readonly HashSet _insideNow = new(); + public static readonly HashSet AffectedPlayers = new(); public static readonly HashSet _speedNotifShown = new(); public static readonly HashSet _visionNotifShown = new(); + public AudioClip _enterClip; public AudioClip _heartbeatClip; public bool _pulsingHb; + bool _restored; + public void Init(byte ownerId, float radius, float duration, float speedMul) { this.ownerId = ownerId; @@ -35,6 +44,8 @@ public void Init(byte ownerId, float radius, float duration, float speedMul) public void Update() { + if (_restored) return; + _t += Time.deltaTime; if (_t > _duration) { @@ -89,12 +100,13 @@ public void Update() { _visionNotifShown.Add(p.PlayerId); var notif = Helpers.CreateAndShowNotification( - "You have entered the Fear Pulse Area. Your vision is reduced!", - new Color(1f, 0.8f, 0.2f), - spr: NewModAsset.VisionDebuff.LoadAsset() - ); + "You have entered the Fear Pulse Area. Your vision is reduced!", + new Color(1f, 0.8f, 0.2f), + spr: NewModAsset.VisionDebuff.LoadAsset() + ); notif.Text.SetOutlineThickness(0.36f); } + Coroutines.Start(Utils.CoShakeCamera(Camera.main.GetComponent(), 0.5f)); } @@ -112,40 +124,57 @@ public void Update() var toRestore = _origSpeed.Keys.Where(id => !_insideNow.Contains(id)).ToList(); foreach (var id in toRestore) { - var p = Utils.PlayerById(id); - if (p) p.MyPhysics.Speed = _origSpeed[id]; - _origSpeed.Remove(id); + RestorePlayer(id); + } + } + } + + public void RestorePlayer(byte playerId) + { + if (_origSpeed.TryGetValue(playerId, out var originalSpeed)) + { + var p = Utils.PlayerById(playerId); + + if (!p.Data.IsDead || !p.Data.Disconnected) + { + p.MyPhysics.Speed = originalSpeed; if (p.AmOwner) { - AffectedPlayers.Remove(p.PlayerId); p.lightSource.lightChild.SetActive(true); - _speedNotifShown.Remove(p.PlayerId); - _visionNotifShown.Remove(p.PlayerId); - Helpers.CreateAndShowNotification("Your vision is restored.", new Color(0.8f, 1f, 0.8f)); + + Helpers.CreateAndShowNotification( + "Your speed and vision are restored.", + new Color(0.8f, 1f, 0.8f) + ); } } + + _origSpeed.Remove(playerId); } + + AffectedPlayers.Remove(playerId); + _speedNotifShown.Remove(playerId); + _visionNotifShown.Remove(playerId); } + public void RestoreAll() { - foreach (var kv in _origSpeed) + if (_restored) return; + _restored = true; + + var ids = _origSpeed.Keys.ToList(); + foreach (var id in ids) { - var p = Utils.PlayerById(kv.Key); - if (p) p.MyPhysics.Speed = kv.Value; + RestorePlayer(id); } - _origSpeed.Clear(); - AffectedPlayers.Clear(); - - var lp = PlayerControl.LocalPlayer; - - if (AffectedPlayers.Contains(lp.PlayerId)) - AffectedPlayers.Remove(lp.PlayerId); - lp.lightSource.lightChild.SetActive(true); + _insideNow.Clear(); + } - _speedNotifShown.Remove(lp.PlayerId); - _visionNotifShown.Remove(lp.PlayerId); + public void OnDestroy() + { + RestoreAll(); } } -} +} \ No newline at end of file diff --git a/NewMod/Components/WraithCallerNpc.cs b/NewMod/Components/WraithCallerNpc.cs index 599081d..9df95e7 100644 --- a/NewMod/Components/WraithCallerNpc.cs +++ b/NewMod/Components/WraithCallerNpc.cs @@ -19,52 +19,45 @@ public class WraithCallerNpc(IntPtr ptr) : MonoBehaviour(ptr) public PlayerControl Target { get; set; } public PlayerControl npc; public LightSource ownerLight; - public bool isActive = false; + public bool isActive; [HideFromIl2Cpp] - // Inspired by: https://github.com/NuclearPowered/Reactor/blob/e27a79249ea706318f3c06f3dc56a5c42d65b1cf/Reactor.Debugger/Window/Tabs/GameTab.cs#L70 - public void Initialize(PlayerControl wraith, PlayerControl target) + public void Initialize(PlayerControl wraith, PlayerControl target, PlayerControl spawned) { Owner = wraith; Target = target; + npc = spawned; - 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); + isActive = true; - npc.isDummy = false; - npc.notRealPlayer = true; KillAnimation.SetMovement(npc, true); - npc.NetTransform.RpcSnapTo(Owner.transform.position); - npc.MyPhysics.Speed = OptionGroupSingleton.Instance.NPCSpeed; - - 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; + npc.MyPhysics.Speed = OptionGroupSingleton.Instance.NPCSpeed; - var noShadow = npc.gameObject.AddComponent(); - if (noShadow != null) - { - noShadow.rend = npc.cosmetics.currentBodySprite.BodySprite; - noShadow.hitOverride = npc.Collider; - } + if (!npc.TryGetComponent(out _)) + npc.gameObject.AddComponent(); - if (!npc.TryGetComponent(out var mc)) + if (AmongUsClient.Instance.AmHost) { - mc = npc.gameObject.AddComponent(); + NewMod.Instance.Log.LogMessage($"Host is setting cosmetics for NPC (ID: {npc.PlayerId}"); + npc.NetTransform.RpcSnapTo(Owner.transform.position); + + var color = (byte)(npc.PlayerId % Palette.PlayerColors.Length); + npc.RpcSetName("Wraith NPC"); + npc.RpcSetColor(color); + + var noShadow = npc.gameObject.AddComponent(); + if (noShadow != null) + { + noShadow.rend = npc.cosmetics.currentBodySprite.BodySprite; + noShadow.hitOverride = npc.Collider; + } } - + + Coroutines.Start(WalkToTarget()); + if (OptionGroupSingleton.Instance.ShouldSwitchCamToNPC) { Camera.main.GetComponent().SetTarget(npc); @@ -73,52 +66,46 @@ public void Initialize(PlayerControl wraith, PlayerControl target) ownerLight.transform.localPosition = npc.Collider.offset; } - npc.cosmetics.enabled = false; + npc.cosmetics.enabled = true; npc.enabled = false; - isActive = true; - - Coroutines.Start(WalkToTarget()); - if (Target.AmOwner) - { SoundManager.Instance.PlaySound(NewModAsset.HeartbeatSound.LoadAsset(), false, 1f); - } } + [HideFromIl2Cpp] - public System.Collections.IEnumerator WalkToTarget() + private System.Collections.IEnumerator WalkToTarget() { - if (Target.Data.IsDead || Target.Data.Disconnected) - { - Dispose(); - } + //yield return null; + + if (!AmongUsClient.Instance.AmHost) yield break; + while (isActive && !MeetingHud.Instance) { + if (!Target || !Target.Data || Target.Data.IsDead) + break; + 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) + if (Vector2.Distance(npcPos, targetPos) <= 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; + WraithCallerUtilities.AddKillNPC(Owner.PlayerId); + break; } yield return new WaitForFixedUpdate(); } + npc.MyPhysics.SetNormalizedVelocity(Vector2.zero); Dispose(); } @@ -127,25 +114,26 @@ public System.Collections.IEnumerator WalkToTarget() public void Dispose() { if (!isActive) return; - isActive = false; - if (npc != null) + if (OptionGroupSingleton.Instance.ShouldSwitchCamToNPC) + { + Camera.main.GetComponent().SetTarget(Owner); + ownerLight.transform.SetParent(Owner.transform, false); + ownerLight.transform.localPosition = Owner.Collider.offset; + } + + if (AmongUsClient.Instance.AmHost) { - if (OptionGroupSingleton.Instance.ShouldSwitchCamToNPC) - { - 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); GameData.Instance.RemovePlayer(info.PlayerId); PlayerControl.AllPlayerControls.Remove(npc); + npc.Despawn(); Destroy(npc.gameObject); npc = null; } + Destroy(gameObject); } } diff --git a/NewMod/CustomRPC.cs b/NewMod/CustomRPC.cs index c340bf9..c196db1 100644 --- a/NewMod/CustomRPC.cs +++ b/NewMod/CustomRPC.cs @@ -2,7 +2,7 @@ namespace NewMod; public enum CustomRPC { - Revive, + HandleRevive, Drain, FakeBody, AssignMission, diff --git a/NewMod/NewMod.cs b/NewMod/NewMod.cs index 4822c30..b43aa33 100644 --- a/NewMod/NewMod.cs +++ b/NewMod/NewMod.cs @@ -47,7 +47,7 @@ public override void Load() { Instance = this; AddComponent(); - ReactorCredits.Register("NewMod", "v1.2.9 Hotfix 1", true, ReactorCredits.AlwaysShow); + ReactorCredits.Register("NewMod", "v1.2.9 Hotfix 2", true, ReactorCredits.AlwaysShow); Harmony.PatchAll(); NewModEventHandler.RegisterEventsLogs(); @@ -170,7 +170,7 @@ public static class SetTaskTextPatch { public static void Postfix(TaskPanelBehaviour __instance, [HarmonyArgument(0)] string str) { - if (PlayerControl.LocalPlayer.Data.IsDead) + if (__instance.taskText != null && PlayerControl.LocalPlayer.Data.IsDead) { __instance.taskText.text += "\n" + (OptionGroupSingleton.Instance.AllowCams ? "Press F2 For Open Cams" : "You cannot open cams because the host has disabled this setting"); } diff --git a/NewMod/NewMod.csproj b/NewMod/NewMod.csproj index ecceb52..b21bb28 100644 --- a/NewMod/NewMod.csproj +++ b/NewMod/NewMod.csproj @@ -12,7 +12,7 @@ - + diff --git a/NewMod/NewModAsset.cs b/NewMod/NewModAsset.cs index 0e92036..2105dcb 100644 --- a/NewMod/NewModAsset.cs +++ b/NewMod/NewModAsset.cs @@ -42,6 +42,7 @@ public static class NewModAsset public static LoadableResourceAsset Shield { get; } = new("NewMod.Resources.Shield.png"); public static LoadableResourceAsset Slash { get; } = new("NewMod.Resources.Slash.png"); public static LoadableResourceAsset DeployZone { get; } = new("NewMod.Resources.deployzone.png"); + public static LoadableResourceAsset VanillaKillButton { get; } = new("NewMod.Resources.killbutton.png"); // SFX public static LoadableAudioResourceAsset ReviveSound { get; } = new("NewMod.Resources.Sounds.revive.wav"); diff --git a/NewMod/NewModDateTime.cs b/NewMod/NewModDateTime.cs index 049d833..6adbaf0 100644 --- a/NewMod/NewModDateTime.cs +++ b/NewMod/NewModDateTime.cs @@ -40,7 +40,6 @@ public static bool IsNewModBirthdayWeek return now >= start && now < end; } } - public static bool IsWraithCallerUnlocked => DateTime.Now >= BirthdayStartThisYear; private const int HalloweenMonth = 10; private const int HalloweenDay = 31; public static readonly TimeSpan HalloweenWindow = TimeSpan.FromDays(7); diff --git a/NewMod/NewModEventHandler.cs b/NewMod/NewModEventHandler.cs index 93f1b4d..4ea18f6 100644 --- a/NewMod/NewModEventHandler.cs +++ b/NewMod/NewModEventHandler.cs @@ -4,6 +4,8 @@ using System.Reflection; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; +using NewMod.Roles.ImpostorRoles; +using NewMod.Utilities; namespace NewMod { @@ -56,13 +58,15 @@ public static void RegisterEventsLogs() NewMod.Instance.Log.LogInfo(sb.ToString()); } - // General events [RegisterEvent] public static void OnRoundStart(RoundStartEvent evt) { if (!evt.TriggeredByIntro) return; HudManager.Instance.Chat.enabled = false; + + Utils.ResetKillTracking(); + NecromancerRole.RevivedPlayers.Clear(); } } } diff --git a/NewMod/Patches/EndGamePatch.cs b/NewMod/Patches/EndGamePatch.cs index c42a5f4..35e4e2a 100644 --- a/NewMod/Patches/EndGamePatch.cs +++ b/NewMod/Patches/EndGamePatch.cs @@ -25,6 +25,15 @@ namespace NewMod.Patches { public static class EndGamePatch { + public static bool EndGameTriggered = false; + + [RegisterEvent] + public static void OnGameStart(RoundStartEvent evt) + { + EndGameTriggered = false; + EndGameResult.CachedWinners.Clear(); + } + [RegisterEvent] public static void OnGameEnd(GameEndEvent evt) { @@ -68,6 +77,7 @@ public static void OnGameEnd(GameEndEvent evt) { poolablePlayer.SetFlipX(i % 2 == 0); } + poolablePlayer.UpdateFromPlayerOutfit( playerData.Outfit, PlayerMaterial.MaskType.None, @@ -151,6 +161,7 @@ public static void OnGameEnd(GameEndEvent evt) endGameManager.WinText.transform.position.y - 0.5f, endGameManager.WinText.transform.position.z); customWinTextObject.transform.localScale = new Vector3(0.7f, 0.7f, 1f); + var customWinTextComponent = customWinTextObject.GetComponent(); customWinTextComponent.text = customWinText; customWinTextComponent.color = customWinColor; @@ -173,6 +184,7 @@ public static string GetRoleName(CachedPlayerData playerData, out Color roleColo { return $"{newmodRole.RoleName}\n{Utils.GetFactionDisplay((INewModRole)customRole)}"; } + return customRole.RoleName; } else @@ -214,6 +226,20 @@ private static Color GetRoleColor(RoleTypes roleType) return Color.white; } } + + public static bool EndCustomGame(GameOverReason reason, Action winners = null) + { + if (EndGameTriggered) return true; + if (EndGameResult.CachedWinners.Count > 0) return true; + + EndGameTriggered = true; + + EndGameResult.CachedWinners.Clear(); + winners?.Invoke(); + + GameManager.Instance.RpcEndGame(reason, false); + return true; + } } [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] @@ -222,7 +248,10 @@ public static class CheckGameEndPatch public static bool Prefix(ShipStatus __instance) { if (DestroyableSingleton.InstanceExists) return true; + if (!AmongUsClient.Instance.AmHost) return true; if (Time.timeSinceLevelLoad < 2f) return true; + if (EndGamePatch.EndGameTriggered) return false; + if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.WraithCallerWin)) return false; if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.ShadeWin)) return false; if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.PulseBladeWin)) return false; @@ -231,18 +260,22 @@ public static bool Prefix(ShipStatus __instance) if (CheckEndGameForRole(__instance, (GameOverReason)NewModEndReasons.SpecialAgentWin)) return false; if (CheckEndGameForRole(__instance, (GameOverReason)NewModEndReasons.PranksterWin, 3)) return false; if (CheckEndGameForRole(__instance, (GameOverReason)NewModEndReasons.EnergyThiefWin)) return false; + if (CheckEndGameForRole(__instance, (GameOverReason)NewModEndReasons.InjectorWin)) return false; + return true; } + public static bool CheckForEndGameFaction(ShipStatus __instance, GameOverReason winReason, int maxCount = 1) where TFaction : INewModRole { var players = PlayerControl.AllPlayerControls.ToArray() - .Where(p => p.Data.Role is TFaction) - .Take(maxCount) - .ToList(); + .Where(p => p.Data.Role is TFaction) + .Take(maxCount) + .ToList(); foreach (var player in players) { bool shouldEndGame = false; + Action extraWinners = null; if (typeof(TFaction) == typeof(PulseBlade)) { @@ -260,51 +293,58 @@ public static bool CheckForEndGameFaction(ShipStatus __instance, GameO shouldEndGame = true; } } + if (typeof(TFaction) == typeof(Tyrant)) { if (Tyrant.ApexThroneReady && Tyrant.ApexThroneOutcomeSet) { shouldEndGame = true; - var tyrantRole = player.Data.Role as Tyrant; - byte champId = tyrantRole.GetChampion(); - var champion = Utils.PlayerById(champId); + extraWinners = () => + { + var tyrantRole = player.Data.Role as Tyrant; + byte champId = tyrantRole.GetChampion(); + var champion = Utils.PlayerById(champId); - bool championWin = Tyrant.Outcome == Tyrant.ThroneOutcome.ChampionSideWin; + bool championWin = Tyrant.Outcome == Tyrant.ThroneOutcome.ChampionSideWin; - if (champion && championWin) - { - EndGameResult.CachedWinners.Add(new(champion.Data)); - } + if (champion && championWin) + { + EndGameResult.CachedWinners.Add(new(champion.Data)); + } + }; } } + if (typeof(TFaction) == typeof(WraithCaller)) { int required = (int)OptionGroupSingleton.Instance.RequiredNPCsToSend; int current = WraithCallerUtilities.GetKillsNPC(player.PlayerId); shouldEndGame = current >= required; } + if (typeof(TFaction) == typeof(Shade)) { Shade.ShadeKills.TryGetValue(player.PlayerId, out var count); int required = (int)OptionGroupSingleton.Instance.RequiredKills; shouldEndGame = count >= required; } + if (shouldEndGame) { - GameManager.Instance.RpcEndGame(winReason, false); - CustomStatsManager.IncrementRoleWin((INewModRole)player.Data.Role); - return true; + return EndGamePatch.EndCustomGame(winReason, extraWinners); } } + return false; } + public static bool CheckEndGameForRole(ShipStatus __instance, GameOverReason winReason, int maxCount = 1) where T : RoleBehaviour { var rolePlayers = PlayerControl.AllPlayerControls.ToArray() - .Where(p => p.Data.Role is T) - .Take(maxCount) - .ToList(); + .Where(p => p.Data.Role is T) + .Take(maxCount) + .ToList(); foreach (var player in rolePlayers) { @@ -316,19 +356,21 @@ public static bool CheckEndGameForRole(ShipStatus __instance, GameOverReason bool isSabotageActive = Utils.IsSabotage(); shouldEndGame = tasksCompleted && isSabotageActive; } + if (typeof(T) == typeof(EnergyThief)) { int drainCount = Utils.GetDrainCount(player.PlayerId); int requiredDrainCount = (int)OptionGroupSingleton.Instance.RequiredDrainCount; - shouldEndGame = drainCount >= requiredDrainCount; } + if (typeof(T) == typeof(Prankster)) { int WinReportCount = 2; int currentReportCount = PranksterUtilities.GetReportCount(player.PlayerId); shouldEndGame = currentReportCount >= WinReportCount; } + if (typeof(T) == typeof(SpecialAgent)) { int missionSuccessCount = Utils.GetMissionSuccessCount(player.PlayerId); @@ -336,20 +378,21 @@ public static bool CheckEndGameForRole(ShipStatus __instance, GameOverReason int netScore = missionSuccessCount - missionFailureCount; shouldEndGame = netScore >= OptionGroupSingleton.Instance.RequiredMissionsToWin; } + if (typeof(T) == typeof(InjectorRole)) { int injectedCount = Utils.GetInjectedCount(); int required = (int)OptionGroupSingleton.Instance.RequiredInjectCount; shouldEndGame = injectedCount >= required; } + if (shouldEndGame) { - GameManager.Instance.RpcEndGame(winReason, false); - CustomStatsManager.IncrementRoleWin((ICustomRole)player.Data.Role); - return true; + return EndGamePatch.EndCustomGame(winReason); } } + return false; } } -} +} \ No newline at end of file diff --git a/NewMod/Patches/MainMenuPatch.cs b/NewMod/Patches/MainMenuPatch.cs index 0e62cb4..acd3793 100644 --- a/NewMod/Patches/MainMenuPatch.cs +++ b/NewMod/Patches/MainMenuPatch.cs @@ -22,7 +22,6 @@ 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] @@ -40,11 +39,6 @@ public static void StartPostfix(MainMenuManager __instance) RightPanel = __instance.transform.Find("MainUI/AspectScaler/RightPanel"); - if (NewModDateTime.IsWraithCallerUnlocked && !_wraithRegistered) - { - _wraithRegistered = true; - } - if (NewModDateTime.IsNewModBirthdayWeek) { Coroutines.Start(ApplyBirthdayUI(__instance)); diff --git a/NewMod/Patches/RolePatch.cs b/NewMod/Patches/RolePatch.cs index 94c010f..3730d0e 100644 --- a/NewMod/Patches/RolePatch.cs +++ b/NewMod/Patches/RolePatch.cs @@ -4,7 +4,6 @@ using System.Linq; using AmongUs.GameOptions; using HarmonyLib; -using Hazel; using MiraAPI.GameOptions; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -31,7 +30,7 @@ public static void Postfix(RoleManager __instance) private static IEnumerator CoAdjustNeutrals() { yield return null; - yield return new WaitForSeconds(0.05f); + yield return new WaitForSeconds(0.06f); var opts = OptionGroupSingleton.Instance; int target = Mathf.RoundToInt(opts.TotalNeutrals); @@ -220,7 +219,7 @@ private static IEnumerator CoAdjustNeutrals() Logger.Instance.LogMessage("-------------- NEUTRAL ADJUST: END --------------"); } - public struct Candidate + public class Candidate { public ICustomRole Role; public int Left; diff --git a/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs b/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs index 08f5635..f8d71d2 100644 --- a/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs +++ b/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs @@ -15,17 +15,17 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)] EndGam Utils.ResetMissionFailureCount(); Utils.ResetInjections(); Utils.ResetStrikeCount(); + Utils.ResetKillTracking(); PranksterUtilities.ResetReportCount(); VisionaryUtilities.DeleteAllScreenshots(); WraithCallerUtilities.ClearAll(); Shade.ShadeKills.Clear(); - Revenant.HasUsedFeignDeath = false; - Revenant.FeignDeathStates.Remove(PlayerControl.LocalPlayer.PlayerId); - Revenant.StalkingStates[PlayerControl.LocalPlayer.PlayerId] = false; + Revenant.ResetAllStates(); + NecromancerRole.RevivedPlayers.Clear(); NewMod.Instance.Log.LogInfo("Reset Drain Count Successfully"); NewMod.Instance.Log.LogInfo("Reset Clone Report Count Successfully"); NewMod.Instance.Log.LogInfo("Reset Mission Success Count Successfully"); NewMod.Instance.Log.LogInfo("Reset Mission Failure Count Successfully"); NewMod.Instance.Log.LogInfo("Deleted all Visionary's screenshots Successfully"); } -} \ No newline at end of file +} diff --git a/NewMod/Patches/StatsPopupPatch.cs b/NewMod/Patches/StatsPopupPatch.cs index eaee3e1..8fead5e 100644 --- a/NewMod/Patches/StatsPopupPatch.cs +++ b/NewMod/Patches/StatsPopupPatch.cs @@ -1,4 +1,4 @@ -using AmongUs.GameOptions; +/*using AmongUs.GameOptions; using UnityEngine; using HarmonyLib; using MiraAPI.Roles; @@ -173,4 +173,4 @@ public static bool Prefix(StatsPopup __instance) return false; } } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/NewMod/Resources/killbutton.png b/NewMod/Resources/killbutton.png new file mode 100644 index 0000000..d137058 Binary files /dev/null and b/NewMod/Resources/killbutton.png differ diff --git a/NewMod/Roles/CrewmateRoles/Specialist.cs b/NewMod/Roles/CrewmateRoles/Specialist.cs index fac940d..c43cb92 100644 --- a/NewMod/Roles/CrewmateRoles/Specialist.cs +++ b/NewMod/Roles/CrewmateRoles/Specialist.cs @@ -37,17 +37,17 @@ public class Specialist : CrewmateRole, ICustomRole [RegisterEvent] public static void OnTaskComplete(CompleteTaskEvent evt) { - PlayerControl player = evt.Player; - if (player.Data.Role is not Specialist) return; + PlayerControl specialist = evt.Player; + if (specialist.Data.Role is not Specialist) return; List abilityAction = new List { () => { - var target = Utils.GetRandomPlayer(p => !p.Data.IsDead && !p.Data.Disconnected && p != player); + var target = Utils.GetRandomPlayer(p => !p.Data.IsDead && !p.Data.Disconnected && p != specialist); if (target != null) { - Utils.RpcRandomDrainActions(player, target); + Utils.RpcRandomDrainActions(specialist, target); Helpers.CreateAndShowNotification($"Energy Drain activated on {target.Data.PlayerName}!",Color.green); } }, @@ -57,13 +57,13 @@ public static void OnTaskComplete(CompleteTaskEvent evt) var player = Utils.PlayerById(closestBody.ParentId); if (closestBody != null) { - Utils.RpcRevive(closestBody); + Utils.HandleRevive(specialist, closestBody.ParentId, AmongUs.GameOptions.RoleTypes.Crewmate, closestBody.transform.position.x, closestBody.transform.position.y); Helpers.CreateAndShowNotification($"Player {player.Data.PlayerName} has been revived.", Color.green); } }, () => { - PranksterUtilities.CreatePranksterDeadBody(player, player.PlayerId); + PranksterUtilities.CreatePranksterDeadBody(specialist, specialist.PlayerId); Helpers.CreateAndShowNotification("Fake Body created!", Color.green); }, () => diff --git a/NewMod/Roles/ImpostorRoles/Necromancer.cs b/NewMod/Roles/ImpostorRoles/Necromancer.cs index 6ea738d..921fe04 100644 --- a/NewMod/Roles/ImpostorRoles/Necromancer.cs +++ b/NewMod/Roles/ImpostorRoles/Necromancer.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using MiraAPI.GameOptions; using MiraAPI.Roles; using MiraAPI.Utilities; @@ -9,6 +10,7 @@ namespace NewMod.Roles.ImpostorRoles; public class NecromancerRole : ImpostorRole, ICustomRole { + public static Dictionary RevivedPlayers = new(); public string RoleName => "Necromancer"; public string RoleDescription => "You can revive dead players who weren't killed by you"; public string RoleLongDescription => "As the Necromancer, you possess a unique and powerful ability: the power to bring one dead player back to life. However,\nyou can only revive someone who wasn't killed by you"; @@ -24,6 +26,22 @@ public class NecromancerRole : ImpostorRole, ICustomRole public TeamIntroConfiguration TeamConfiguration => new() { IntroTeamDescription = RoleDescription, - IntroTeamColor = RoleColor + IntroTeamColor = RoleColor }; + public override bool DidWin(GameOverReason reason) + { + if (reason == (GameOverReason)NewModEndReasons.TyrantWin || + reason == (GameOverReason)NewModEndReasons.ShadeWin || + reason == (GameOverReason)NewModEndReasons.WraithCallerWin || + reason == (GameOverReason)NewModEndReasons.SpecialAgentWin || + reason == (GameOverReason)NewModEndReasons.PranksterWin || + reason == (GameOverReason)NewModEndReasons.EnergyThiefWin || + reason == (GameOverReason)NewModEndReasons.InjectorWin || + reason == (GameOverReason)NewModEndReasons.DoubleAgentWin) + { + return false; + } + + return base.DidWin(reason); + } } diff --git a/NewMod/Roles/ImpostorRoles/PulseBlade.cs b/NewMod/Roles/ImpostorRoles/PulseBlade.cs index a7c0f07..f4d3628 100644 --- a/NewMod/Roles/ImpostorRoles/PulseBlade.cs +++ b/NewMod/Roles/ImpostorRoles/PulseBlade.cs @@ -61,9 +61,24 @@ public StringBuilder SetTabText() return tabText; } - public override bool DidWin(GameOverReason gameOverReason) + public override bool DidWin(GameOverReason reason) { - return gameOverReason == (GameOverReason)NewModEndReasons.PulseBladeWin; + if (reason == (GameOverReason)NewModEndReasons.PulseBladeWin) + return true; + + if (reason == (GameOverReason)NewModEndReasons.TyrantWin || + reason == (GameOverReason)NewModEndReasons.ShadeWin || + reason == (GameOverReason)NewModEndReasons.WraithCallerWin || + reason == (GameOverReason)NewModEndReasons.SpecialAgentWin || + reason == (GameOverReason)NewModEndReasons.PranksterWin || + reason == (GameOverReason)NewModEndReasons.EnergyThiefWin || + reason == (GameOverReason)NewModEndReasons.InjectorWin || + reason == (GameOverReason)NewModEndReasons.DoubleAgentWin) + { + return false; + } + + return base.DidWin(reason); } } } \ No newline at end of file diff --git a/NewMod/Roles/ImpostorRoles/Revenant.cs b/NewMod/Roles/ImpostorRoles/Revenant.cs index 1d067f1..eba7774 100644 --- a/NewMod/Roles/ImpostorRoles/Revenant.cs +++ b/NewMod/Roles/ImpostorRoles/Revenant.cs @@ -41,17 +41,32 @@ public class FeignDeathInfo public bool Reported; } + public static void ResetAllStates() + { + FeignDeathStates.Clear(); + StalkingStates.Clear(); + HasUsedFeignDeath = false; + } + [RegisterEvent] public static void OnPlayerExit(PlayerLeaveEvent evt) { - if (FeignDeathStates.ContainsKey(evt.ClientData.Character.PlayerId)) - { - FeignDeathStates.Remove(evt.ClientData.Character.PlayerId); - } - if (StalkingStates.ContainsKey(evt.ClientData.Character.PlayerId)) + ResetAllStates(); + } + public override bool DidWin(GameOverReason reason) + { + if (reason == (GameOverReason)NewModEndReasons.TyrantWin || + reason == (GameOverReason)NewModEndReasons.ShadeWin || + reason == (GameOverReason)NewModEndReasons.WraithCallerWin || + reason == (GameOverReason)NewModEndReasons.SpecialAgentWin || + reason == (GameOverReason)NewModEndReasons.PranksterWin || + reason == (GameOverReason)NewModEndReasons.EnergyThiefWin || + reason == (GameOverReason)NewModEndReasons.InjectorWin || + reason == (GameOverReason)NewModEndReasons.DoubleAgentWin) { - StalkingStates.Remove(evt.ClientData.Character.PlayerId); + return false; } - HasUsedFeignDeath = false; + + return base.DidWin(reason); } } diff --git a/NewMod/Roles/NeutralRoles/Tyrant.cs b/NewMod/Roles/NeutralRoles/Tyrant.cs index 82eea6b..00d6dcc 100644 --- a/NewMod/Roles/NeutralRoles/Tyrant.cs +++ b/NewMod/Roles/NeutralRoles/Tyrant.cs @@ -94,6 +94,23 @@ void AppendAbilityLine(int index, string text) return tabText; } + public override bool DidWin(GameOverReason reason) + { + if (reason == (GameOverReason)NewModEndReasons.TyrantWin) return true; + + if (reason == (GameOverReason)NewModEndReasons.ShadeWin || + reason == (GameOverReason)NewModEndReasons.WraithCallerWin || + reason == (GameOverReason)NewModEndReasons.SpecialAgentWin || + reason == (GameOverReason)NewModEndReasons.PranksterWin || + reason == (GameOverReason)NewModEndReasons.EnergyThiefWin || + reason == (GameOverReason)NewModEndReasons.InjectorWin || + reason == (GameOverReason)NewModEndReasons.DoubleAgentWin) + { + return false; + } + + return base.DidWin(reason); + } public int _kills; public static byte _championId; public static bool ApexThroneReady; @@ -111,7 +128,7 @@ public static void OnAfterMurderEvent(AfterMurderEvent evt) { if (!Utils.IsRoleActive("Tyrant")) return; - var tyrant = evt.Source.Data.Role as Tyrant; + if (evt.Source.Data.Role is not Tyrant tyrant) return; tyrant._kills++; diff --git a/NewMod/Utilities/CoroutinesHelper.cs b/NewMod/Utilities/CoroutinesHelper.cs index 23f1be4..b7433c3 100644 --- a/NewMod/Utilities/CoroutinesHelper.cs +++ b/NewMod/Utilities/CoroutinesHelper.cs @@ -9,6 +9,7 @@ using NewMod.Roles.NeutralRoles; using Reactor.Utilities.Extensions; using NewMod.Components.ScreenEffects; +using AmongUs.GameOptions; namespace NewMod.Utilities { @@ -296,7 +297,7 @@ public static IEnumerator CoReviveAndKill(PlayerControl target) { revivedParentId = deadBody.ParentId; - Utils.RpcRevive(deadBody); + Utils.HandleRevive(target, deadBody.ParentId, RoleTypes.Crewmate, deadBody.transform.position.x, deadBody.transform.position.y); yield return new WaitForSeconds(0.5f); diff --git a/NewMod/Utilities/Utils.cs b/NewMod/Utilities/Utils.cs index 8aead77..3cec4ab 100644 --- a/NewMod/Utilities/Utils.cs +++ b/NewMod/Utilities/Utils.cs @@ -102,7 +102,7 @@ public static PlayerControl PlayerById(byte id) /// The player who was killed. public static void RecordOnKill(PlayerControl killer, PlayerControl victim) { - if (PlayerKiller.ContainsKey(killer)) + if (PlayerKiller.ContainsKey(victim)) { PlayerKiller[victim] = killer; } @@ -111,6 +111,10 @@ public static void RecordOnKill(PlayerControl killer, PlayerControl victim) PlayerKiller.Add(victim, killer); } } + public static void ResetKillTracking() + { + PlayerKiller.Clear(); + } /// /// Retrieves the killer of the specified victim. @@ -153,57 +157,6 @@ public static DeadBody GetClosestBody() return closestBody; } - // Inspired By : https://github.com/eDonnes124/Town-Of-Us-R/blob/master/source/Patches/CrewmateRoles/AltruistMod/Coroutine.cs#L57 - public static void Revive(DeadBody body) - { - if (body == null) return; - - var parentId = body.ParentId; - var player = PlayerById(parentId); - - if (player != null) - { - foreach (var deadBody in GameObject.FindObjectsOfType()) - { - if (deadBody.ParentId == body.ParentId) - Object.Destroy(deadBody.gameObject); - } - player.Revive(); - - if (player.Data.Role is NoisemakerRole role) - { - Object.Destroy(role.deathArrowPrefab.gameObject); - } - player.RpcSetRole(RoleTypes.Impostor, true); - } - } - - // Inspired By : https://github.com/eDonnes124/Town-Of-Us-R/blob/master/source/Patches/CrewmateRoles/AltruistMod/Coroutine.cs#L57 - public static void ReviveV2(DeadBody body) - { - if (body == null) return; - - var parentId = body.ParentId; - var player = PlayerById(parentId); - - if (player != null) - { - foreach (var deadBody in GameObject.FindObjectsOfType()) - { - if (deadBody.ParentId == body.ParentId) - Object.Destroy(deadBody.gameObject); - } - player.Revive(); - - if (player.Data.Role is NoisemakerRole role) - { - Object.Destroy(role.deathArrowPrefab.gameObject); - } - player.RpcSetRole((RoleTypes)RoleId.Get(), true); - NewMod.Instance.Log.LogError($"---------------^^^^^^SETTING ROLE TO REVENANT-------------^^^^ NEW ROLE: {player.Data.Role.NiceName}"); - } - } - // Thanks to: https://github.com/Rabek009/MoreGamemodes/blob/master/Modules/Utils.cs#L66 /// /// Checks if a particular system type is active on the current map. @@ -423,38 +376,71 @@ public static void ResetInjections() { InjectedPlayerIds.Clear(); } - /// - /// Sends an RPC to revive a player from a dead body. - /// - /// The DeadBody instance to revive from. - public static void RpcRevive(DeadBody body) - { - Revive(body); - var writer = AmongUsClient.Instance.StartRpcImmediately( - PlayerControl.LocalPlayer.NetId, - (byte)CustomRPC.Revive, - SendOption.Reliable - ); - writer.Write(PlayerControl.LocalPlayer.PlayerId); - writer.Write(body.ParentId); - AmongUsClient.Instance.FinishRpcImmediately(writer); - } + // Inspired By: https://github.com/AU-Avengers/TOU-Mira/blob/dev/TownOfUs/Modules/ReviveUtilities.cs#L40 - /// - /// Sends an RPC to revive a player from a dead body and set their role to Revenant. - /// - /// The DeadBody instance to revive from. - public static void RpcReviveV2(DeadBody body) + [MethodRpc((uint)CustomRPC.HandleRevive)] + public static IEnumerator HandleRevive(PlayerControl source, byte revivedId, RoleTypes roleToSet, float reviveX, float reviveY) { - ReviveV2(body); - var writer = AmongUsClient.Instance.StartRpcImmediately( - PlayerControl.LocalPlayer.NetId, - (byte)CustomRPC.Revive, - SendOption.Reliable - ); - writer.Write(PlayerControl.LocalPlayer.PlayerId); - writer.Write(body.ParentId); - AmongUsClient.Instance.FinishRpcImmediately(writer); + var revived = PlayerById(revivedId); + + if (revived.Data.Disconnected) + yield break; + + yield return new WaitForSeconds(0.15f); + + if (revived.Data.Disconnected || !revived.Data.IsDead) + yield break; + + var revivePos = new Vector2(reviveX, reviveY); + var inMeetingOrExile = MeetingHud.Instance || ExileController.Instance; + + if (revived.Data.Role is NoisemakerRole noisemaker && noisemaker.deathArrowPrefab != null) + { + Object.Destroy(noisemaker.deathArrowPrefab.gameObject); + } + + revived.Revive(); + revived.RemainingEmergencies = 0; + + if (AmongUsClient.Instance.AmHost) + { + revived.RpcSetRole(roleToSet, true); + } + + if (!inMeetingOrExile) + { + revived.transform.position = revivePos; + revived.MyPhysics.body.position = revivePos; + Physics2D.SyncTransforms(); + + if (revived.AmOwner) + { + revived.NetTransform.RpcSnapTo(revivePos); + } + } + + foreach (var deadBody in Object.FindObjectsOfType()) + { + if (deadBody.ParentId == revived.PlayerId) + { + Object.Destroy(deadBody.gameObject); + } + } + + float elapsed = 0f; + while (elapsed < 1f) + { + foreach (var deadBody in Object.FindObjectsOfType()) + { + if (deadBody.ParentId == revived.PlayerId) + { + Object.Destroy(deadBody.gameObject); + } + } + + elapsed += 0.05f; + yield return new WaitForSeconds(0.05f); + } } // Thanks to: https://github.com/yanpla/yanplaRoles/blob/master/Utils.cs#L55 @@ -878,8 +864,8 @@ public static IEnumerator StartFeignDeath(PlayerControl player) yield break; } } - RpcReviveV2(body); - player.transform.position = body.transform.position; + HandleRevive(player, player.PlayerId, (RoleTypes)RoleId.Get(), body.transform.position.x, body.transform.position.y); + yield return new WaitForSeconds(0.2f); player.RpcShapeshift(GetRandomPlayer(p => !p.Data.IsDead && !p.Data.Disconnected), false); Coroutines.Start(CoroutinesHelper.CoNotify("You have been revived in a new body!")); Revenant.HasUsedFeignDeath = true; diff --git a/NewMod/Utilities/WraithCallerUtilities.cs b/NewMod/Utilities/WraithCallerUtilities.cs index 8323c7c..3f8123e 100644 --- a/NewMod/Utilities/WraithCallerUtilities.cs +++ b/NewMod/Utilities/WraithCallerUtilities.cs @@ -1,88 +1,106 @@ - using System.Collections.Generic; - using NewMod.Components; - using Reactor.Networking.Attributes; - using UnityEngine; +using System.Collections.Generic; +using System.Linq; +using NewMod.Components; +using Reactor.Networking.Attributes; +using UnityEngine; - namespace NewMod.Utilities +namespace NewMod.Utilities +{ + public static class WraithCallerUtilities { - 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) { - /// - /// A dictionary holding the number of NPCs sent by each Wraith Caller. - /// - private static readonly Dictionary Sent = []; + return Sent.TryGetValue(ownerId, out var v) ? v : 0; + } - /// - /// A dictionary holding the number of kills achieved by NPCs summoned by each Wraith Caller. - /// - private static readonly Dictionary Kills = []; + /// + /// 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; + } - /// - /// 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; - } + /// + /// Increments the number of NPCs sent by the owner. + /// + public static void AddSentNPC(byte ownerId, int amount = 1) + { + Sent[ownerId] = GetSentNPC(ownerId) + amount; + } - /// - /// 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 kills credited to the owner’s wraiths. + /// + public static void AddKillNPC(byte ownerId, int amount = 1) + { + Kills[ownerId] = GetKillsNPC(ownerId) + amount; + } - /// - /// Increments the number of NPCs sent by the owner. - /// - public static void AddSentNPC(byte ownerId, int amount = 1) - { - Sent[ownerId] = GetSentNPC(ownerId) + amount; - } + /// + /// Clears all counters for both NPCs sent and kills achieved. + /// + public static void ClearAll() + { + Sent.Clear(); + Kills.Clear(); + } + [MethodRpc((uint)CustomRPC.RequestSummon)] + public static void RpcRequestSummonNPC(PlayerControl source, PlayerControl target) + { + if (!AmongUsClient.Instance.AmHost) return; - /// - /// 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; - } + var npcId = HostNPC(source); + RpcSummonNPC(source, target, npcId); + } + public static byte HostNPC(PlayerControl source) + { + var prefab = AmongUsClient.Instance.PlayerPrefab; + var npc = Object.Instantiate(prefab); + npc.PlayerId = (byte)GameData.Instance.GetAvailableId(); - /// - /// Clears all counters for both NPCs sent and kills achieved. - /// - public static void ClearAll() - { - Sent.Clear(); - Kills.Clear(); - } + npc.isDummy = false; + npc.notRealPlayer = true; - /// - /// RPC for . Runs on all clients to keep state in sync. - /// - /// The Wraith Caller owner who summoned the NPC. - /// The intended target player. + var host = AmongUsClient.Instance.GetHost(); + var npcInfo = GameData.Instance.AddPlayer(npc, host); - [MethodRpc((uint)CustomRPC.SummonNPC)] - public static void RpcSummonNPC(PlayerControl source, PlayerControl target) - { - AddSentNPC(source.PlayerId); - SummonNPC(source, target); - } + AmongUsClient.Instance.Spawn(npcInfo); + AmongUsClient.Instance.Spawn(npc); - /// - /// 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(); + npc.NetTransform.RpcSnapTo(source.transform.position); + + return npc.PlayerId; + } + [MethodRpc((uint)CustomRPC.SummonNPC)] + public static void RpcSummonNPC(PlayerControl source, PlayerControl target, byte npcId) + { + AddSentNPC(source.PlayerId); - wraithNpc.Initialize(wraith, target); + InitializeNPC(source, target, Utils.PlayerById(npcId)); + return; + + } + public static void InitializeNPC(PlayerControl owner, PlayerControl target, PlayerControl npc) + { + if (!npc.gameObject.TryGetComponent(out _)) + { + var comp = npc.gameObject.AddComponent(); + comp.Initialize(owner, target, npc); } } } +}