diff --git a/CustomRoles.cs b/CustomRoles.cs index 5e49a0401..af718a709 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -293,6 +293,7 @@ public enum CustomRoles Terrorist, Tiger, Traitor, + Tremor, Vengeance, VengefulRomantic, Virus, diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs new file mode 100644 index 000000000..fddfc18ff --- /dev/null +++ b/Modules/CustomNetObject.cs @@ -0,0 +1,131 @@ +using UnityEngine; +using InnerNet; +using Hazel; +using System.Collections.Generic; + +// Credit: https://github.com/Rabek009/MoreGamemodes/blob/e054eb498094dfca0a365fc6b6fea8d17f9974d7/Modules/CustomObjects/CustomNetObject.cs +namespace EHR +{ + public class CustomNetObject + { + public void RpcChangeSprite(string sprite) + { + PC.RpcSetName(sprite); + } + + public void RpcTeleport(Vector2 position) + { + PC.NetTransform.RpcSnapTo(position); + Position = position; + } + + public void Despawn() + { + PC.Despawn(); + CustomObjects.Remove(this); + } + + public void Hide(PlayerControl player) + { + if (player.AmOwner) + { + PC.Visible = false; + return; + } + MessageWriter writer = MessageWriter.Get(SendOption.None); + writer.StartMessage(6); + writer.Write(AmongUsClient.Instance.GameId); + writer.WritePacked(player.GetClientId()); + writer.StartMessage(5); + writer.WritePacked(PC.NetId); + writer.EndMessage(); + writer.EndMessage(); + AmongUsClient.Instance.SendOrDisconnect(writer); + writer.Recycle(); + } + + public virtual void OnFixedUpdate() + { + + } + + public void CreateNetObject(string sprite, Vector2 position) + { + PC = Object.Instantiate(AmongUsClient.Instance.PlayerPrefab, Vector2.zero, Quaternion.identity); + PC.PlayerId = 255; + PC.isNew = false; + PC.notRealPlayer = true; + AmongUsClient.Instance.NetIdCnt += 1U; + MessageWriter msg = MessageWriter.Get(SendOption.None); + msg.StartMessage(5); + msg.Write(AmongUsClient.Instance.GameId); + AmongUsClient.Instance.WriteSpawnMessage(PC, -2, SpawnFlags.None, msg); + msg.EndMessage(); + msg.StartMessage(6); + msg.Write(AmongUsClient.Instance.GameId); + msg.WritePacked(int.MaxValue); + for (uint i = 1; i <= 3; ++i) + { + msg.StartMessage(4); + msg.WritePacked(2U); + msg.WritePacked(-2); + msg.Write((byte)SpawnFlags.None); + msg.WritePacked(1); + msg.WritePacked(AmongUsClient.Instance.NetIdCnt - i); + msg.StartMessage(1); + msg.EndMessage(); + msg.EndMessage(); + } + msg.EndMessage(); + AmongUsClient.Instance.SendOrDisconnect(msg); + msg.Recycle(); + if (PlayerControl.AllPlayerControls.Contains(PC)) + PlayerControl.AllPlayerControls.Remove(PC); + new LateTask(() => { + PC.RpcSetName(sprite); + PC.NetTransform.RpcSnapTo(position); + }, 0.1f); + Position = position; + PC.cosmetics.currentBodySprite.BodySprite.color = Color.clear; + PC.cosmetics.colorBlindText.color = Color.clear; + Sprite = sprite; + ++MaxId; + Id = MaxId; + CustomObjects.Add(this); + foreach (var pc in PlayerControl.AllPlayerControls) + { + if (pc.AmOwner) continue; + new LateTask(() => { + CustomRpcSender sender = CustomRpcSender.Create("SetFakeData", SendOption.None); + MessageWriter writer = sender.stream; + sender.StartMessage(pc.GetClientId()); + writer.StartMessage(1); + { + writer.WritePacked(PC.NetId); + writer.Write(pc.PlayerId); + } + writer.EndMessage(); + sender.StartRpc(PC.NetId, (byte)RpcCalls.MurderPlayer) + .WriteNetObject(PC) + .Write((int)MurderResultFlags.FailedError) + .EndRpc(); + writer.StartMessage(1); + { + writer.WritePacked(PC.NetId); + writer.Write((byte)255); + } + writer.EndMessage(); + sender.EndMessage(); + sender.SendMessage(); + }, 0.1f); + } + } + + public static List CustomObjects; + public static int MaxId = -1; + public PlayerControl PC; + public string Sprite; + public int Id; + public Vector2 Position; + } +} \ No newline at end of file diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 75a555aff..e3fa770cf 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -348,6 +348,7 @@ internal static class CustomRolesHelper CustomRoles.BloodKnight => RoleTypes.Impostor, CustomRoles.Poisoner => RoleTypes.Impostor, CustomRoles.NSerialKiller => RoleTypes.Impostor, + CustomRoles.Tremor => RoleTypes.Impostor, CustomRoles.Evolver => RoleTypes.Impostor, CustomRoles.Rogue => RoleTypes.Impostor, CustomRoles.Patroller => RoleTypes.Impostor, @@ -461,6 +462,7 @@ internal static class CustomRolesHelper CustomRoles.Rogue or CustomRoles.Parasite or CustomRoles.NSerialKiller or + CustomRoles.Tremor or CustomRoles.Evolver or CustomRoles.Chemist or CustomRoles.QuizMaster or @@ -1156,6 +1158,7 @@ public enum CountTypes HexMaster, Wraith, NSerialKiller, + Tremor, Evolver, Rogue, Patroller, diff --git a/Modules/RPC.cs b/Modules/RPC.cs index 7f1c4f07a..fc61abbcc 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -138,6 +138,7 @@ public enum CustomRPC SyncSimon, SyncRogue, SyncEvolver, + SyncTremor, // Other Game Modes SyncKBPlayer, @@ -230,6 +231,12 @@ internal class RPCHandlerPatch { if (AmongUsClient.Instance.AmHost) return; var rpcType = (CustomRPC)callId; + + // Finish this later + // Main.PlayerStates.Values + // .Where(x => "Sync" + x.Role.GetType().Name == rpcType.ToString()) + // .Do(x => x.Role.GetType().GetMethod("ReceiveRPC")?.Invoke(reader)); + switch (rpcType) { case CustomRPC.AntiBlackout: @@ -417,6 +424,9 @@ internal class RPCHandlerPatch case CustomRPC.SyncEvolver: (Main.PlayerStates[reader.ReadByte()].Role as Evolver)?.ReceiveRPC(reader); break; + case CustomRPC.SyncTremor: + (Main.PlayerStates[reader.ReadByte()].Role as Tremor)?.ReceiveRPC(reader); + break; case CustomRPC.SetBountyTarget: { byte bountyId = reader.ReadByte(); diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 1b9ef6add..4167913c0 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -565,7 +565,7 @@ public static class Utils case CustomGameMode.HideAndSeek: return HnSManager.HasTasks(p); } - if (Shifter.ForceDisableTasks(p.PlayerId)) return false; + if (Shifter.ForceDisableTasks(p.Object.GetClientId())) return false; var role = States.MainRole; switch (role) @@ -579,6 +579,7 @@ public static class Utils case CustomRoles.Eclipse: case CustomRoles.Pyromaniac: case CustomRoles.NSerialKiller: + case CustomRoles.Tremor: case CustomRoles.Evolver: case CustomRoles.Rogue: case CustomRoles.Patroller: diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 8d451d5ab..68ee07ea0 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -766,6 +766,7 @@ class ReportDeadBodyPatch var killerRole = killer?.GetCustomRole(); if (((IsActive(SystemTypes.Comms) && Options.CommsCamouflage.GetBool() && (Main.NormalOptions.MapId != 5 || !Options.CommsCamouflageDisableOnFungle.GetBool())) || Camouflager.IsActive) && Options.DisableReportWhenCC.GetBool()) return false; + if (Main.PlayerStates.Values.Any(x => x.Role is Tremor {IsDoom: true})) return false; if (target == null) { diff --git a/Roles/Crewmate/Snitch.cs b/Roles/Crewmate/Snitch.cs index 1eb558602..0f2783221 100644 --- a/Roles/Crewmate/Snitch.cs +++ b/Roles/Crewmate/Snitch.cs @@ -133,11 +133,12 @@ public class Snitch : RoleBase var exposedSnitch = PlayerIdList.Where(s => !Main.PlayerStates[s].IsDead && IsExposed[s]); var snitch = exposedSnitch as byte[] ?? exposedSnitch.ToArray(); - if (!snitch.Any()) return string.Empty; + if (snitch.Length == 0) return string.Empty; var warning = $"\n{Translator.GetString("Snitch")} "; if (EnableTargetArrow) warning += TargetArrow.GetArrows(seer, snitch); + else warning += "⚠"; return Utils.ColorString(RoleColor, warning); } diff --git a/Roles/Neutral/Shifter.cs b/Roles/Neutral/Shifter.cs index bb7d3cd43..a2e591fa4 100644 --- a/Roles/Neutral/Shifter.cs +++ b/Roles/Neutral/Shifter.cs @@ -11,7 +11,7 @@ namespace EHR.Neutral private const int Id = 644400; public static bool On; - private static List WasShifter = []; + private static List WasShifter = []; private static Dictionary AllPlayerBasis = []; private static OptionItem KillCooldown; @@ -66,6 +66,9 @@ namespace EHR.Neutral { if (!base.OnCheckMurder(killer, target)) return false; + var clientId = killer.GetClientId(); + if (clientId != -1) WasShifter.Add(clientId); + var killerId = killer.PlayerId; killer.PlayerId = target.PlayerId; target.PlayerId = killerId; diff --git a/Roles/Neutral/Tremor.cs b/Roles/Neutral/Tremor.cs new file mode 100644 index 000000000..0a632f42a --- /dev/null +++ b/Roles/Neutral/Tremor.cs @@ -0,0 +1,126 @@ +using AmongUs.GameOptions; +using UnityEngine; + +namespace EHR.Roles.Neutral; + +public class Tremor : RoleBase +{ + private const int Id = 644600; + public static bool On; + + private static OptionItem KillCooldown; + private static OptionItem CanVent; + private static OptionItem HasImpostorVision; + private static OptionItem TimerStart; + private static OptionItem TimerDecrease; + private static OptionItem DoomTime; + + public override bool IsEnable => On; + + int Timer; + int DoomTimer; + public bool IsDoom => Timer <= 0; + + public static void SetupCustomOption() + { + Options.SetupRoleOptions(Id, TabGroup.NeutralRoles, CustomRoles.Tremor); + KillCooldown = FloatOptionItem.Create(Id + 2, "KillCooldown", new(0f, 180f, 0.5f), 22.5f, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Tremor]) + .SetValueFormat(OptionFormat.Seconds); + CanVent = BooleanOptionItem.Create(Id + 3, "CanVent", true, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Tremor]); + HasImpostorVision = BooleanOptionItem.Create(Id + 4, "ImpostorVision", true, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Tremor]); + TimerStart = IntegerOptionItem.Create(Id + 5, "Tremor.TimerStart", new(0, 600, 1), 180, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Tremor]) + .SetValueFormat(OptionFormat.Seconds); + TimerDecrease = IntegerOptionItem.Create(Id + 6, "Tremor.TimerDecrease", new(0, 180, 1), 15, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Tremor]) + .SetValueFormat(OptionFormat.Seconds); + DoomTime = IntegerOptionItem.Create(Id + 7, "Tremor.DoomTime", new(0, 180, 1), 30, TabGroup.NeutralRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Tremor]) + .SetValueFormat(OptionFormat.Seconds); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + Timer = TimerStart.GetInt(); + DoomTimer = 0; + + if (!AmongUsClient.Instance.AmHost) return; + if (!Main.ResetCamPlayerList.Contains(playerId)) + Main.ResetCamPlayerList.Add(playerId); + } + + public override void SetKillCooldown(byte id) => Main.AllPlayerKillCooldown[id] = KillCooldown.GetFloat(); + public override void ApplyGameOptions(IGameOptions opt, byte id) => opt.SetVision(HasImpostorVision.GetBool()); + public override bool CanUseImpostorVentButton(PlayerControl pc) => CanVent.GetBool(); + + long LastUpdate = Utils.TimeStamp; + int Count = 0; + + public override void OnFixedUpdate(PlayerControl pc) + { + if (!GameStates.IsInTask || !pc.IsAlive()) return; + + bool wasDoom = IsDoom; + + if (!IsDoom && LastUpdate != Utils.TimeStamp) + { + Timer--; + Utils.SendRPC(CustomRPC.SyncTremor, pc.PlayerId, Timer); + LastUpdate = Utils.TimeStamp; + } + + if (wasDoom != IsDoom) + Main.AllAlivePlayerControls.Do(x => x.Notify(Translator.GetString("Tremor.DoomNotify"))); + + if (IsDoom) + { + Count++; + if (Count < 5) return; + Count = 0; + + pc.RpcGuardAndKill(); + + var pos = pc.Pos(); + Main.AllAlivePlayerControls + .Where(x => x.PlayerId != pc.PlayerId && Vector2.Distance(pos, x.Pos()) <= 1.5f) + .Do(x => pc.Kill(x)); + + if (LastUpdate == Utils.TimeStamp) return; + LastUpdate = Utils.TimeStamp; + + DoomTimer--; + Utils.SendRPC(CustomRPC.SyncTremor, pc.PlayerId, DoomTimer); + + if (DoomTimer <= 0) pc.Suicide(); + } + } + + public override void OnMurder(PlayerControl killer, PlayerControl target) + { + Timer -= TimerDecrease.GetInt(); + Utils.SendRPC(CustomRPC.SyncTremor, pc.PlayerId, Timer); + } + + public void ReceiveRPC(Hazel.MessageReader reader) + { + int value = reader.ReadPackedInt32(); + if (IsDoom) DoomTimer = value; + else Timer = value; + } + + public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud, bool meeting) + { + if (seer.PlayerId != target.PlayerId || (seer.IsModClient() && !hud) || meeting) return; + var color = IsDoom ? Color.yellow : Color.cyan; + return Utils.ColorString(color, IsDoom ? DoomTimer.ToString() : Timer.ToString()); + } +} \ No newline at end of file diff --git a/main.cs b/main.cs index ec11c1834..629499187 100644 --- a/main.cs +++ b/main.cs @@ -429,6 +429,7 @@ public class Main : BasePlugin { CustomRoles.HexMaster, "#ff00ff" }, { CustomRoles.Wraith, "#4B0082" }, { CustomRoles.NSerialKiller, "#233fcc" }, + { CustomRoles.Tremor, "#e942f5" }, { CustomRoles.Evolver, "#f2c444" }, { CustomRoles.Rogue, "#7a629c" }, { CustomRoles.Patroller, "#c1cc27" }, @@ -706,6 +707,7 @@ public enum CustomWinner Necromancer = CustomRoles.Necromancer, Wraith = CustomRoles.Wraith, SerialKiller = CustomRoles.NSerialKiller, + Tremor = CustomRoles.Tremor, Evolver = CustomRoles.Evolver, Rogue = CustomRoles.Rogue, Patroller = CustomRoles.Patroller,