From 78ec88b68d57ffc12b9840cf6682b0973cc1201a Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 22 Jan 2026 11:10:06 +0100 Subject: [PATCH] Add Quarry --- CustomRoles.cs | 1 + Main.cs | 2 + Modules/CustomRolesHelper.cs | 3 + Modules/NameColorManager.cs | 1 + Modules/Utils.cs | 1 + Patches/PlayerControlPatch.cs | 7 + Resources/Lang/en_US.json | 9 + Roles/Standard/Neutral/Killing/Accumulator.cs | 8 + Roles/Standard/Neutral/Killing/Quarry.cs | 247 ++++++++++++++++++ 9 files changed, 279 insertions(+) create mode 100644 Roles/Standard/Neutral/Killing/Quarry.cs diff --git a/CustomRoles.cs b/CustomRoles.cs index 7b6b327cc..c5c6ecdff 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -359,6 +359,7 @@ public enum CustomRoles Pulse, Pursuer, Pyromaniac, + Quarry, QuizMaster, Reckless, Revolutionist, diff --git a/Main.cs b/Main.cs index a9b041bf0..6441276cd 100644 --- a/Main.cs +++ b/Main.cs @@ -561,6 +561,7 @@ public class Main : BasePlugin { CustomRoles.Thanos, "#F9D401" }, { CustomRoles.Berserker, "#50538F" }, { CustomRoles.SerialKiller, "#233fcc" }, + { CustomRoles.Quarry, "#c1fb2b" }, { CustomRoles.Accumulator, "#2bfbae" }, { CustomRoles.Spider, "#C9E44C" }, { CustomRoles.SoulCollector, "#6021A0" }, @@ -1067,6 +1068,7 @@ public enum CustomWinner Necromancer = CustomRoles.Necromancer, Wraith = CustomRoles.Wraith, SerialKiller = CustomRoles.SerialKiller, + Quarry = CustomRoles.Quarry, Accumulator = CustomRoles.Accumulator, Spider = CustomRoles.Spider, SoulCollector = CustomRoles.SoulCollector, diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index d455ecb57..f580dbdc9 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -517,6 +517,7 @@ internal static class CustomRolesHelper CustomRoles.Duality => RoleTypes.Impostor, CustomRoles.Berserker => RoleTypes.Impostor, CustomRoles.SerialKiller => RoleTypes.Impostor, + CustomRoles.Quarry => RoleTypes.Shapeshifter, CustomRoles.SoulCollector => RoleTypes.Impostor, CustomRoles.Spider => UsePets ? RoleTypes.Impostor : RoleTypes.Shapeshifter, CustomRoles.Explosivist => UsePets ? RoleTypes.Impostor : RoleTypes.Shapeshifter, @@ -655,6 +656,7 @@ internal static class CustomRolesHelper CustomRoles.Parasite or CustomRoles.Berserker or CustomRoles.SerialKiller or + CustomRoles.Quarry or CustomRoles.Accumulator or CustomRoles.Spider or CustomRoles.SoulCollector or @@ -1966,6 +1968,7 @@ public enum CountTypes HexMaster, Wraith, SerialKiller, + Quarry, Accumulator, Spider, SoulCollector, diff --git a/Modules/NameColorManager.cs b/Modules/NameColorManager.cs index 2306f4653..3043f26f6 100644 --- a/Modules/NameColorManager.cs +++ b/Modules/NameColorManager.cs @@ -209,6 +209,7 @@ public static class NameColorManager CustomRoles.Crewpostor when seer.Is(CustomRoleTypes.Impostor) && Options.AlliesKnowCrewpostor.GetBool() => Main.RoleColors[CustomRoles.Madmate], CustomRoles.Hypocrite when seer.Is(CustomRoleTypes.Impostor) && Hypocrite.AlliesKnowHypocrite.GetBool() => Main.RoleColors[CustomRoles.Madmate], CustomRoles.President when ((President)targetRoleClass).IsRevealed => Main.RoleColors[CustomRoles.President], + CustomRoles.Quarry when ((Quarry)targetRoleClass).TargetId == seer.PlayerId => "000000", _ => color }; diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 3efda520e..346177e59 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -825,6 +825,7 @@ public static class Utils case CustomRoles.Eclipse: case CustomRoles.Pyromaniac: case CustomRoles.SerialKiller: + case CustomRoles.Quarry: case CustomRoles.Spider: case CustomRoles.SoulCollector: case CustomRoles.Berserker: diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 11fb1de91..53e8158fe 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -264,6 +264,7 @@ internal static class CheckMurderPatch if (ToiletMaster.OnAnyoneCheckMurderStart(killer, target)) return false; if (Dad.OnAnyoneCheckMurderStart(target)) return false; + if (Quarry.OnAnyoneCheckMurderStart(killer, target)) return false; Simon.RemoveTarget(killer, Simon.Instruction.Kill); @@ -1069,6 +1070,12 @@ internal static class ReportDeadBodyPatch return false; } + if (!Quarry.OnAnyoneCheckReportDeadBody(__instance)) + { + Notify("QuarryTarget"); + return false; + } + if (!Librarian.OnAnyoneReport(__instance)) return false; // Player dies, no notify needed if (!Trapster.OnAnyoneCheckReportDeadBody(__instance, target)) return false; // Player dies, no notify needed diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 8a36ae297..cb946f9a5 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -9250,6 +9250,15 @@ "Telekinetic.Mode.Shield": "Shield", "Telekinetic.Mode.Speed": "Speed", "Telekinetic.Mode.Doors": "Doors", + "Quarry": "Quarry", + "QuarryInfo": "Hide From Players", + "QuarryInfoLong": "(Neutrals):\\nAs the Quarry, you may shapeshift to select your target and swap places with them, they then have to kill you in a specified time or they die. If they find and kill you, they live. You can also kill normally using the kill button.\\nKill everyone to win!", + "Quarry.TargetHasArrowToQuarry": "Target has an arrow to the <#c1fb2b>Quarry", + "Quarry.CanVentDuringSeekTime": "Can Vent During Seek Time", + "Quarry.TargetDiesOnMeetingCall": "Target Dies on Meeting Call", + "Quarry.SeekTime": "Seek Time", + "Quarry.TargetSeekBeginNotify": "<#ff0000>You were swapped with the {0}! They have a black name!", + "Quarry.TimeLeftSuffix": "<#ffff00>You have <#ffffff>{0}s to kill the {1} <#ffa500>or you die!", "Nonplus": "Nonplus", "NonplusInfo": "Blind Others", "NonplusInfoLong": "(Neutrals):\\nAs the Nonplus, you can blind others by venting or petting. Blinded players can't see anything for a set amount of time.\\nKill everyone to win.", diff --git a/Roles/Standard/Neutral/Killing/Accumulator.cs b/Roles/Standard/Neutral/Killing/Accumulator.cs index d7829886b..ea2d02762 100644 --- a/Roles/Standard/Neutral/Killing/Accumulator.cs +++ b/Roles/Standard/Neutral/Killing/Accumulator.cs @@ -96,6 +96,14 @@ public class Accumulator : RoleBase Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); } + public override void AfterMeetingTasks() + { + if (!Killing) return; + PlayerControl pc = AccumulatorId.GetPlayer(); + if (pc == null || !pc.IsAlive()) return; + pc.RpcSetRoleDesync(RoleTypes.Impostor, pc.OwnerId, setRoleMap: true); + } + public void ReceiveRPC(MessageReader reader) { KCD = reader.ReadSingle(); diff --git a/Roles/Standard/Neutral/Killing/Quarry.cs b/Roles/Standard/Neutral/Killing/Quarry.cs new file mode 100644 index 000000000..9f35c9879 --- /dev/null +++ b/Roles/Standard/Neutral/Killing/Quarry.cs @@ -0,0 +1,247 @@ +using System.Collections.Generic; +using System.Diagnostics; +using AmongUs.GameOptions; +using EHR.Modules; +using EHR.Modules.Extensions; +using Hazel; + +namespace EHR.Roles; + +public class Quarry : RoleBase +{ + public static bool On; + private static List Instances = []; + + private static OptionItem TargetHasArrowToQuarry; + private static OptionItem CanVentDuringSeekTime; + private static OptionItem TargetDiesOnMeetingCall; + private static OptionItem SeekTime; + private static OptionItem ShapeshiftCooldown; + private static OptionItem KillCooldown; + private static OptionItem CanVent; + private static OptionItem ImpostorVision; + private static OptionItem AbilityUseLimit; + private static OptionItem AbilityUseGainWithEachKill; + private static OptionItem CooldownsResetEachOther; + + public override bool IsEnable => On; + + private byte QuarryId; + public byte TargetId; + private Stopwatch SeekTimer; + private long LastUpdate; + + public override void SetupCustomOption() + { + StartSetup(657800) + .AutoSetupOption(ref TargetHasArrowToQuarry, true) + .AutoSetupOption(ref CanVentDuringSeekTime, false) + .AutoSetupOption(ref TargetDiesOnMeetingCall, false) + .AutoSetupOption(ref SeekTime, 15, new IntegerValueRule(0, 120, 1), OptionFormat.Seconds) + .AutoSetupOption(ref ShapeshiftCooldown, 15f, new FloatValueRule(0f, 180f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref KillCooldown, 22.5f, new FloatValueRule(0f, 180f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref CanVent, true) + .AutoSetupOption(ref ImpostorVision, true) + .AutoSetupOption(ref AbilityUseLimit, 2f, new FloatValueRule(0, 20, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityUseGainWithEachKill, 1f, new FloatValueRule(0f, 5f, 0.1f), OptionFormat.Times) + .AutoSetupOption(ref CooldownsResetEachOther, true); + } + + public override void Init() + { + On = false; + Instances = []; + } + + public override void Add(byte playerId) + { + On = true; + QuarryId = playerId; + TargetId = byte.MaxValue; + SeekTimer = new(); + LastUpdate = 0; + Instances.Add(this); + playerId.SetAbilityUseLimit(AbilityUseLimit.GetFloat()); + } + + public override void Remove(byte playerId) + { + Instances.Remove(this); + } + + public override void SetKillCooldown(byte id) + { + Main.AllPlayerKillCooldown[id] = KillCooldown.GetFloat(); + } + + public override void ApplyGameOptions(IGameOptions opt, byte id) + { + opt.SetVision(ImpostorVision.GetBool()); + AURoleOptions.ShapeshifterCooldown = ShapeshiftCooldown.GetFloat(); + AURoleOptions.ShapeshifterDuration = 1f; + } + + public override bool CanUseImpostorVentButton(PlayerControl pc) + { + return CanVent.GetBool() && (TargetId == byte.MaxValue || CanVentDuringSeekTime.GetBool()); + } + + public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) + { + return target.PlayerId != TargetId; + } + + public override void OnMurder(PlayerControl killer, PlayerControl target) + { + if (CooldownsResetEachOther.GetBool()) + killer.RpcResetAbilityCooldown(); + } + + public override bool OnShapeshift(PlayerControl shapeshifter, PlayerControl target, bool shapeshifting) + { + if (!shapeshifting) return true; + + if (shapeshifter.GetAbilityUseLimit() < 1) return false; + shapeshifter.RpcRemoveAbilityUse(); + + Vector2 pos = shapeshifter.Pos(); + shapeshifter.TP(target); + target.TP(pos); + + TargetId = target.PlayerId; + SeekTimer = Stopwatch.StartNew(); + Utils.SendRPC(CustomRPC.SyncRoleData, QuarryId, 1, TargetId); + Main.Instance.StartCoroutine(ContinuouslyResetAbilityCooldown()); + if (TargetHasArrowToQuarry.GetBool()) TargetArrow.Add(target.PlayerId, shapeshifter.PlayerId); + + var sender = CustomRpcSender.Create("QuarryTarget", SendOption.Reliable); + var hasValue = false; + hasValue |= sender.Notify(target, string.Format(Translator.GetString("Quarry.TargetSeekBeginNotify"), CustomRoles.Quarry.ToColoredString())); + hasValue |= sender.RpcSetRole(target, RoleTypes.Impostor, target.OwnerId); + hasValue |= sender.RpcSetRole(shapeshifter, RoleTypes.Crewmate, target.OwnerId); + hasValue |= sender.NotifyRolesSpecific(target, shapeshifter, out sender, out bool cleared); + if (cleared) hasValue = false; + sender.SendMessage(dispose: !hasValue); + + if (CooldownsResetEachOther.GetBool()) + shapeshifter.SetKillCooldown(); + + return false; + + System.Collections.IEnumerator ContinuouslyResetAbilityCooldown() + { + while (TargetId != byte.MaxValue) + { + int interval = ShapeshiftCooldown.GetInt() - 2; + Stopwatch stopwatch = Stopwatch.StartNew(); + while (stopwatch.Elapsed.Seconds < interval) yield return null; + if (TargetId == byte.MaxValue) yield break; + shapeshifter.RpcResetAbilityCooldown(); + } + } + } + + public override void OnFixedUpdate(PlayerControl pc) + { + if (!Main.IntroDestroyed || !GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || !pc.IsAlive() || TargetId == byte.MaxValue) return; + + long now = Utils.TimeStamp; + if (now == LastUpdate) return; + LastUpdate = now; + + PlayerControl target = TargetId.GetPlayer(); + + if (target == null || !target.IsAlive()) + { + TargetId = byte.MaxValue; + SeekTimer.Reset(); + Utils.SendRPC(CustomRPC.SyncRoleData, QuarryId, 2); + return; + } + + if (SeekTimer.GetRemainingTime(SeekTime.GetInt()) <= 0) + { + target.Suicide(); + TargetId = byte.MaxValue; + SeekTimer.Reset(); + Utils.SendRPC(CustomRPC.SyncRoleData, QuarryId, 2); + } + else + { + Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); + Utils.NotifyRoles(SpecifySeer: target, SpecifyTarget: target); + } + } + + public static bool OnAnyoneCheckMurderStart(PlayerControl killer, PlayerControl target) + { + foreach (Quarry instance in Instances) + { + if (instance.TargetId == killer.PlayerId && instance.QuarryId == target.PlayerId) + { + killer.Kill(target); + killer.RpcSetRoleDesync(killer.GetRoleTypes(), killer.OwnerId); + instance.TargetId = byte.MaxValue; + instance.SeekTimer.Reset(); + Utils.SendRPC(CustomRPC.SyncRoleData, instance.QuarryId, 2); + return true; + } + } + + return false; + } + + public static bool OnAnyoneCheckReportDeadBody(PlayerControl reporter) + { + return !Instances.Exists(x => x.TargetId == reporter.PlayerId); + } + + public override bool CanUseVent(PlayerControl pc, int ventId) + { + return pc.PlayerId != TargetId; + } + + public override void OnReportDeadBody() + { + if (TargetId == byte.MaxValue) return; + + TargetId = byte.MaxValue; + SeekTimer.Reset(); + Utils.SendRPC(CustomRPC.SyncRoleData, QuarryId, 2); + + PlayerControl target = TargetId.GetPlayer(); + if (target == null || !target.IsAlive()) return; + + if (TargetDiesOnMeetingCall.GetBool()) + target.Suicide(); + else + { + PlayerControl pc = QuarryId.GetPlayer(); + if (pc == null || !pc.IsAlive()) return; + + pc.RpcIncreaseAbilityUseLimitBy(1); + } + } + + public void ReceiveRPC(MessageReader reader) + { + switch (reader.ReadPackedInt32()) + { + case 1: + TargetId = reader.ReadByte(); + SeekTimer = Stopwatch.StartNew(); + break; + case 2: + TargetId = byte.MaxValue; + SeekTimer.Reset(); + break; + } + } + + public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) + { + if ((seer.PlayerId != QuarryId && seer.PlayerId != TargetId) || seer.PlayerId != target.PlayerId || meeting || hud || TargetId == byte.MaxValue) return string.Empty; + string timer = string.Format(Translator.GetString("Quarry.TimeLeftSuffix"), SeekTimer.GetRemainingTime(SeekTime.GetInt()) - 1, CustomRoles.Quarry.ToColoredString()); + return seer.PlayerId == TargetId ? $"{TargetArrow.GetAllArrows(TargetId)}\n{timer}" : timer; + } +} \ No newline at end of file