diff --git a/CustomRoles.cs b/CustomRoles.cs index 0d3318380..fbaba18bf 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -416,6 +416,7 @@ public enum CustomRoles Enchanter, Goddess, Illusionist, + MoonDancer, Poache, PotionMaster, Reaper, diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index f2601f009..90e58322e 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -605,6 +605,7 @@ internal static class CustomRolesHelper CustomRoles.Enchanter => RoleTypes.Impostor, CustomRoles.Siren => RoleTypes.Phantom, CustomRoles.Wyrd => RoleTypes.Shapeshifter, + CustomRoles.MoonDancer => RoleTypes.Phantom, _ => RoleTypes.GuardianAngel }; @@ -1116,7 +1117,7 @@ internal static class CustomRolesHelper public static bool CheckAddonConflict(CustomRoles role, PlayerControl pc) { - return role.IsAdditionRole() && (!Main.NeverSpawnTogetherCombos.TryGetValue(OptionItem.CurrentPreset, out Dictionary> neverList) || !neverList.TryGetValue(pc.GetCustomRole(), out List bannedAddonList) || !bannedAddonList.Contains(role)) && pc.GetCustomRole() is not CustomRoles.GuardianAngelEHR and not CustomRoles.God && !pc.Is(CustomRoles.Madmate) && !pc.Is(CustomRoles.GM) && role is not CustomRoles.Lovers && !pc.Is(CustomRoles.LazyGuy) && (!pc.HasSubRole() || pc.GetCustomSubRoles().Count < Options.NoLimitAddonsNumMax.GetInt()) && (!Options.AddonCanBeSettings.TryGetValue(role, out (OptionItem Imp, OptionItem Neutral, OptionItem Crew, OptionItem Coven) o) || ((o.Imp.GetBool() || !pc.GetCustomRole().IsImpostor()) && (o.Neutral.GetBool() || !pc.GetCustomRole().IsNeutral()) && (o.Crew.GetBool() || !pc.IsCrewmate()) && (o.Coven.GetBool() || !pc.Is(Team.Coven)))) && (!role.IsImpOnlyAddon() || (pc.IsImpostor() && !pc.Is(CustomRoles.DoubleAgent)) || (pc.Is(CustomRoles.Traitor) && Traitor.CanGetImpostorOnlyAddons.GetBool())) && role switch + return role.IsAdditionRole() && !(role.IsGhostRole() && pc.IsAlive()) && (!Main.NeverSpawnTogetherCombos.TryGetValue(OptionItem.CurrentPreset, out Dictionary> neverList) || !neverList.TryGetValue(pc.GetCustomRole(), out List bannedAddonList) || !bannedAddonList.Contains(role)) && pc.GetCustomRole() is not CustomRoles.GuardianAngelEHR and not CustomRoles.God && !pc.Is(CustomRoles.Madmate) && !pc.Is(CustomRoles.GM) && role is not CustomRoles.Lovers && !pc.Is(CustomRoles.LazyGuy) && (!pc.HasSubRole() || pc.GetCustomSubRoles().Count < Options.NoLimitAddonsNumMax.GetInt()) && (!Options.AddonCanBeSettings.TryGetValue(role, out (OptionItem Imp, OptionItem Neutral, OptionItem Crew, OptionItem Coven) o) || ((o.Imp.GetBool() || !pc.GetCustomRole().IsImpostor()) && (o.Neutral.GetBool() || !pc.GetCustomRole().IsNeutral()) && (o.Crew.GetBool() || !pc.IsCrewmate()) && (o.Coven.GetBool() || !pc.Is(Team.Coven)))) && (!role.IsImpOnlyAddon() || (pc.IsImpostor() && !pc.Is(CustomRoles.DoubleAgent)) || (pc.Is(CustomRoles.Traitor) && Traitor.CanGetImpostorOnlyAddons.GetBool())) && role switch { CustomRoles.Blind when pc.Is(CustomRoles.Sensor) => false, CustomRoles.Composter when float.IsNaN(pc.GetAbilityUseLimit()) => false, @@ -1285,7 +1286,8 @@ internal static class CustomRolesHelper CustomRoles.Timelord or CustomRoles.Enchanter or CustomRoles.Siren or - CustomRoles.Wyrd; + CustomRoles.Wyrd or + CustomRoles.MoonDancer; } public static bool IncompatibleWithVenom(this CustomRoles role) diff --git a/Modules/GameState.cs b/Modules/GameState.cs index eaa65224b..a91cf7431 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -74,6 +74,7 @@ public class PlayerState(byte playerId) Deafened, Patrolled, Misguess, + LossOfBlood, // Natural Disasters Meteor, diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index e912b4a3f..7d46ae868 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -1804,7 +1804,7 @@ "TargetKnowsLawyer": "Target knows their Lawyer", "YourLawyerIsNotify": "Your Lawyer is {0}. They win if you win.", "ShowLongInfo": "Show Long Role Descriptions instead of Short ones", - "InfectiousBiteCooldownIncrese": "Infect Cooldown Increase Per Use", + "InfectiousBiteCooldownIncrese": "Infect Cooldown Increase Per Kill", "InfectiousBiteMax": "Maximum Infections", "InfectiousKnowTargetRole": "Know infected player's role", "InfectiousTargetKnowOtherTarget": "Infected players know each other", @@ -1979,6 +1979,7 @@ "DeathReason.Deafened": "Deafened", "DeathReason.Patrolled": "Patrolled", "DeathReason.Misguess": "Misguess", + "DeathReason.LossOfBlood": "Loss Of Blood", "ND_DRLaughMessage.Meteor": "Meteor.... I hardly knew her!", "ND_DRLaughMessage.Lava": "Lava is not the best place to swim, huh?", "ND_DRLaughMessage.Tornado": "Out for a spin? I hope this tornado didn't make you dizzy!", @@ -9462,6 +9463,7 @@ "Bloodmoon.Suffix": "<{1}>You will die in <{2}>{0}s", "Bloodmoon.Speed": "<#ff1919>Bloodmoon Speed", "Bloodmoon.DieOnMeetingCall": "Targets Die on Meeting Call", + "Bloodmoon.CDCIncreasePerUse": "Cooldown Increase Per Use", "BloodmoonNearYou": "<#ffff00>The {0} is near you! Stay away to avoid death!", "Prefix.Bloodmoon": "Bloodmoon", "GA": "Guardian Angel", @@ -10084,6 +10086,14 @@ "Wyrd.Suffix.SuicideWarningOnAction.Report": "Reporting a dead body", "Wyrd.Suffix.SuicideWarningOnAction.Button": "Calling an emergency meeting", "Wyrd.Suffix.SuicideWarnings": "<#ffff00>⚠ The following actions will cause you to suicide: ⚠\\n{0}", + "MoonDancer": "Moon Dancer", + "MoonDancerInfo": "Give and Steal Add-ons", + "MoonDancerInfoLong": "(Coven):\\nAs the Moon Dancer, you can assign yourself a random add-on. You can also use your kill button on someone,\\n- once to give them an add-on, that is: helpful if they're a coven member, otherwise harmful (assigned after the next meeting)\\n- twice to steal all add-ons from them*\\n\\nWith the Necronomicon, double clicks kill the target, while also stealing all add-ons from them*.\\n\\n*If settings allow", + "MoonDancer.SelfAssignAddonCooldown": "Self-Assign Add-on Cooldown", + "MoonDancer.OthersAssignAddonCooldown": "Cooldown for Assigning Add-ons to Others", + "MoonDancer.StealAddonCooldown": "Add-on Stealing Cooldown", + "MoonDancer.CanStealAddonsWithoutNecronomicon": "Can Steal Add-ons Without Necronomicon", + "MoonDancer.StealAllAddonsOnKill": "Steal All Add-ons On Kill", "DCNotify.Hacking": "You were banned for hacking.\\n\\nPlease stop.", "DCNotify.Banned": "You were banned from this lobby.\\n\\nContact the host if this was a mistake.", "DCNotify.Kicked": "You were kicked from this lobby.\\n\\nYou may still rejoin.", diff --git a/Roles/AddOns/GhostRoles/Bloodmoon.cs b/Roles/AddOns/GhostRoles/Bloodmoon.cs index ad9006abf..9b551911a 100644 --- a/Roles/AddOns/GhostRoles/Bloodmoon.cs +++ b/Roles/AddOns/GhostRoles/Bloodmoon.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using Hazel; namespace EHR.AddOns.GhostRoles; @@ -8,18 +10,22 @@ internal class Bloodmoon : IGhostRole private static OptionItem Duration; private static OptionItem Speed; private static OptionItem DieOnMeetingCall; + private static OptionItem CDIncreasePerUse; private static readonly Dictionary ScheduledDeaths = []; private long LastUpdate; + private byte BloodmoonID; + public Team Team => Team.Impostor | Team.Neutral; - public int Cooldown => Duration.GetInt() + CD.GetInt(); + public int Cooldown => CD.GetInt() + Main.PlayerStates.Values.Count(x => x.GetRealKiller() == BloodmoonID && x.deathReason == PlayerState.DeathReason.LossOfBlood) * CDIncreasePerUse.GetInt(); public void OnAssign(PlayerControl pc) { Main.AllPlayerSpeed[pc.PlayerId] = Speed.GetFloat(); pc.MarkDirtySettings(); LastUpdate = Utils.TimeStamp; + BloodmoonID = pc.PlayerId; } public void OnProtect(PlayerControl pc, PlayerControl target) @@ -46,6 +52,10 @@ internal class Bloodmoon : IGhostRole DieOnMeetingCall = new BooleanOptionItem(649405, "Bloodmoon.DieOnMeetingCall", true, TabGroup.OtherRoles) .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Bloodmoon]); + + CDIncreasePerUse = new IntegerOptionItem(649406, "Bloodmoon.CDCIncreasePerUse", new(0, 60, 1), 15, TabGroup.OtherRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Bloodmoon]) + .SetValueFormat(OptionFormat.Seconds); } public static void Update(PlayerControl pc, Bloodmoon instance) @@ -67,10 +77,10 @@ internal class Bloodmoon : IGhostRole continue; } - if (pc.RpcCheckAndMurder(player, true)) player.Suicide(realKiller: pc); + if (pc.RpcCheckAndMurder(player, true)) player.Suicide(PlayerState.DeathReason.LossOfBlood, pc); } - Utils.GetPlayersInRadius(5f, pc.Pos()).DoIf(x => !x.Is(Team.Impostor), x => x.Notify(string.Format(Translator.GetString("BloodmoonNearYou"), CustomRoles.Bloodmoon.ToColoredString()))); + Utils.GetPlayersInRadius(7f, pc.Pos()).DoIf(x => !x.Is(Team.Impostor), x => x.Notify(string.Format(Translator.GetString("BloodmoonNearYou"), CustomRoles.Bloodmoon.ToColoredString()), sendOption: SendOption.None)); } public static void OnMeetingStart() diff --git a/Roles/Coven/MoonDancer.cs b/Roles/Coven/MoonDancer.cs new file mode 100644 index 000000000..df916555d --- /dev/null +++ b/Roles/Coven/MoonDancer.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; + +namespace EHR.Coven; + +public class MoonDancer : Coven +{ + public static bool On; + + public override bool IsEnable => On; + + private static OptionItem SelfAssignAddonCooldown; + private static OptionItem OthersAssignAddonCooldown; + private static OptionItem StealAddonCooldown; + private static OptionItem KillCooldown; + private static OptionItem CanStealAddonsWithoutNecronomicon; + private static OptionItem StealAllAddonsOnKill; + private static OptionItem CanVentBeforeNecronomicon; + private static OptionItem CanVentAfterNecronomicon; + + protected override NecronomiconReceivePriorities NecronomiconReceivePriority => NecronomiconReceivePriorities.Random; + + private Dictionary> ToAssign = []; + + public override void SetupCustomOption() + { + StartSetup(657300) + .AutoSetupOption(ref SelfAssignAddonCooldown, 15f, new FloatValueRule(0f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref OthersAssignAddonCooldown, 15f, new FloatValueRule(0f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref StealAddonCooldown, 15f, new FloatValueRule(0f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref KillCooldown, 30f, new FloatValueRule(0f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref CanStealAddonsWithoutNecronomicon, true) + .AutoSetupOption(ref StealAllAddonsOnKill, true) + .AutoSetupOption(ref CanVentBeforeNecronomicon, false) + .AutoSetupOption(ref CanVentAfterNecronomicon, true); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + ToAssign = []; + } + + public override bool CanUseImpostorVentButton(PlayerControl pc) + { + return HasNecronomicon ? CanVentAfterNecronomicon.GetBool() : CanVentBeforeNecronomicon.GetBool(); + } + + public override void ApplyGameOptions(IGameOptions opt, byte playerId) + { + AURoleOptions.PhantomCooldown = SelfAssignAddonCooldown.GetFloat(); + AURoleOptions.PhantomDuration = 0.1f; + } + + public override void SetKillCooldown(byte id) + { + Main.AllPlayerKillCooldown[id] = HasNecronomicon ? KillCooldown.GetFloat() : OthersAssignAddonCooldown.GetFloat(); + } + + public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) + { + if (!HasNecronomicon && !CanStealAddonsWithoutNecronomicon.GetBool()) + { + GiveAddon(); + return false; + } + + if (killer.CheckDoubleTrigger(target, GiveAddon)) + { + if (HasNecronomicon && !StealAllAddonsOnKill.GetBool()) return true; + PlayerState state = Main.PlayerStates[target.PlayerId]; + state.SubRoles.FindAll(x => !killer.Is(x) && !x.IsNotAssignableMidGame() && CustomRolesHelper.CheckAddonConflict(x, killer)).ForEach(x => + { + killer.RpcSetCustomRole(x); + state.RemoveSubRole(x); + }); + if (!HasNecronomicon) killer.SetKillCooldown(StealAddonCooldown.GetFloat()); + return HasNecronomicon; + } + + return false; + + void GiveAddon() + { + CustomRoles addon = Options.GroupedAddons[target.Is(Team.Coven) ? AddonTypes.Helpful : AddonTypes.Harmful].Where(x => !target.Is(x) && !x.IsNotAssignableMidGame() && CustomRolesHelper.CheckAddonConflict(x, target)).RandomElement(); + if (addon == default(CustomRoles)) + return; + + if (ToAssign.TryGetValue(target.PlayerId, out var list)) + list.Add(addon); + else + ToAssign[target.PlayerId] = [addon]; + + killer.SetKillCooldown(OthersAssignAddonCooldown.GetFloat()); + } + } + + public override bool OnVanish(PlayerControl pc) + { + CustomRoles addon = Enum.GetValues().Where(x => x.IsAdditionRole() && !x.IsGhostRole() && !pc.Is(x) && !x.IsNotAssignableMidGame() && CustomRolesHelper.CheckAddonConflict(x, pc)).RandomElement(); + if (addon == default(CustomRoles)) return false; + pc.RpcSetCustomRole(addon); + return false; + } + + public override void AfterMeetingTasks() + { + foreach ((byte id, List addons) in ToAssign) + { + var pc = id.GetPlayer(); + if (pc == null || !pc.IsAlive()) continue; + + addons.ForEach(x => pc.RpcSetCustomRole(x)); + } + + ToAssign = []; + } +} \ No newline at end of file diff --git a/Roles/Neutral/Curser.cs b/Roles/Neutral/Curser.cs index 187d850ea..eebb85576 100644 --- a/Roles/Neutral/Curser.cs +++ b/Roles/Neutral/Curser.cs @@ -93,7 +93,7 @@ public class Curser : RoleBase target.RpcRemoveAbilityUse(); break; case 1: - CustomRoles addon = Options.GroupedAddons[AddonTypes.Harmful].Where(x => x.IsAdditionRole() && !target.Is(x) && !x.IsNotAssignableMidGame() && CustomRolesHelper.CheckAddonConflict(x, target)).RandomElement(); + CustomRoles addon = Options.GroupedAddons[AddonTypes.Harmful].Where(x => !target.Is(x) && !x.IsNotAssignableMidGame() && CustomRolesHelper.CheckAddonConflict(x, target)).RandomElement(); if (addon == default(CustomRoles)) goto Pick; target.RpcSetCustomRole(addon); break;