diff --git a/CustomRoles.cs b/CustomRoles.cs index 0d3318380..019722aa7 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -378,6 +378,7 @@ public enum CustomRoles Slenderman, SoulCollector, SoulHunter, + Spider, Spirit, Spiritcaller, Sprayer, @@ -416,6 +417,7 @@ public enum CustomRoles Enchanter, Goddess, Illusionist, + MoonDancer, Poache, PotionMaster, Reaper, diff --git a/Main.cs b/Main.cs index b2008f788..80689c42d 100644 --- a/Main.cs +++ b/Main.cs @@ -557,6 +557,7 @@ public class Main : BasePlugin { CustomRoles.Thanos, "#F9D401" }, { CustomRoles.Berserker, "#50538F" }, { CustomRoles.SerialKiller, "#233fcc" }, + { CustomRoles.Spider, "#f54251" }, { CustomRoles.SoulCollector, "#6021A0" }, { CustomRoles.Sharpshooter, "#5901D4" }, { CustomRoles.Explosivist, "#ff5900" }, @@ -1062,6 +1063,7 @@ public enum CustomWinner Necromancer = CustomRoles.Necromancer, Wraith = CustomRoles.Wraith, SerialKiller = CustomRoles.SerialKiller, + Spider = CustomRoles.Spider, SoulCollector = CustomRoles.SoulCollector, Berserker = CustomRoles.Berserker, Sharpshooter = CustomRoles.Sharpshooter, diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index f2601f009..4214ec929 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -518,6 +518,7 @@ internal static class CustomRolesHelper CustomRoles.Berserker => RoleTypes.Impostor, CustomRoles.SerialKiller => RoleTypes.Impostor, CustomRoles.SoulCollector => RoleTypes.Impostor, + CustomRoles.Spider => UsePets ? RoleTypes.Impostor : RoleTypes.Shapeshifter, CustomRoles.Explosivist => UsePets ? RoleTypes.Impostor : RoleTypes.Shapeshifter, CustomRoles.Sharpshooter => UsePets ? RoleTypes.Impostor : RoleTypes.Shapeshifter, CustomRoles.Thanos => UsePets ? RoleTypes.Impostor : RoleTypes.Shapeshifter, @@ -605,6 +606,7 @@ internal static class CustomRolesHelper CustomRoles.Enchanter => RoleTypes.Impostor, CustomRoles.Siren => RoleTypes.Phantom, CustomRoles.Wyrd => RoleTypes.Shapeshifter, + CustomRoles.MoonDancer => RoleTypes.Phantom, _ => RoleTypes.GuardianAngel }; @@ -652,6 +654,7 @@ internal static class CustomRolesHelper CustomRoles.Parasite or CustomRoles.Berserker or CustomRoles.SerialKiller or + CustomRoles.Spider or CustomRoles.SoulCollector or CustomRoles.Sharpshooter or CustomRoles.Explosivist or @@ -1093,6 +1096,7 @@ internal static class CustomRolesHelper or CustomRoles.RouleteGrandeur or CustomRoles.Enderman or CustomRoles.Explosivist + or CustomRoles.Spider or CustomRoles.Hookshot or CustomRoles.Mycologist or CustomRoles.Magician @@ -1116,7 +1120,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 +1289,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) @@ -1965,6 +1970,7 @@ public enum CountTypes HexMaster, Wraith, SerialKiller, + Spider, SoulCollector, Berserker, Sharpshooter, diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 23ed97ed1..fa4863146 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -891,10 +891,13 @@ internal static class ExtendedPlayerControl PlayerState state = Main.PlayerStates[pc.PlayerId]; - if (realKiller != null && state.Role is SchrodingersCat cat) + switch (state.Role) { - cat.OnCheckMurderAsTarget(realKiller, pc); - return; + case SchrodingersCat cat when realKiller != null: + cat.OnCheckMurderAsTarget(realKiller, pc); + return; + case Veteran when Veteran.VeteranInProtect.ContainsKey(pc.PlayerId): + return; } state.deathReason = deathReason; diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index 7557fec52..700f41657 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -478,6 +478,7 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS Curser.OnAnyoneApplyGameOptions(opt, player.PlayerId); Auditor.OnAnyoneApplyGameOptions(opt, player.PlayerId); Clerk.OnAnyoneApplyGameOptions(opt, player.PlayerId); + Spider.OnAnyoneApplyGameOptions(opt, player.PlayerId); if (Sprayer.LowerVisionList.Contains(player.PlayerId)) { diff --git a/Modules/GameState.cs b/Modules/GameState.cs index eaa65224b..1b63786e6 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, @@ -409,9 +410,9 @@ public class PlayerState(byte playerId) return IsDead && RealKiller.TimeStamp != DateTime.MinValue ? RealKiller.ID : byte.MaxValue; } - public int GetKillCount(bool excludeSelfKill = false) + public int GetKillCount() { - return Main.PlayerStates.Values.Count(state => !(excludeSelfKill && state.PlayerId == PlayerId) && state.GetRealKiller() == PlayerId); + return Main.PlayerStates.Values.Count(state => state.PlayerId != PlayerId && state.GetRealKiller() == PlayerId); } } diff --git a/Modules/LobbySharingAPI.cs b/Modules/LobbySharingAPI.cs index 22b23e568..2439d5b9b 100644 --- a/Modules/LobbySharingAPI.cs +++ b/Modules/LobbySharingAPI.cs @@ -151,6 +151,8 @@ internal static class ExitGamePatch { public static void Prefix() { + Logger.Msg("Exiting game", "ExitGamePatch.Prefix"); + Main.RealOptionsData?.Restore(GameOptionsManager.Instance.CurrentGameOptions); if (SetUpRoleTextPatch.IsInIntro) diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index d697f4579..588106d03 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -349,9 +349,6 @@ public static class Options public static OptionItem ArsonistMinPlayersToIgnite; public static OptionItem ArsonistMaxPlayersToIgnite; public static OptionItem LegacyNemesis; - public static OptionItem VeteranSkillCooldown; - public static OptionItem VeteranSkillDuration; - public static OptionItem VeteranSkillMaxOfUsage; public static OptionItem BodyguardProtectRadius; public static OptionItem BodyguardKillsKiller; public static OptionItem WitnessCD; @@ -442,6 +439,8 @@ public static class Options public static OptionItem AutoPlayAgain; public static OptionItem AutoPlayAgainCountdown; public static OptionItem AutoStartTimer; + + public static OptionItem EnterKeyToStartGame; public static OptionItem SaboteurCD; public static OptionItem SaboteurCDAfterMeetings; @@ -1696,6 +1695,8 @@ public static class Options AutoDraftStartCommandCooldown = new IntegerOptionItem(19427, "AutoDraftStartCommandCooldown", new(10, 600, 5), 150, TabGroup.SystemSettings) .SetParent(AutoDraftStartCommandAfterJoin) .SetValueFormat(OptionFormat.Seconds); + + EnterKeyToStartGame = new BooleanOptionItem(19432, "EnterKeyToStartGame", false, TabGroup.SystemSettings); LowLoadMode = new BooleanOptionItem(19317, "LowLoadMode", true, TabGroup.SystemSettings) .SetHeader(true) @@ -3472,7 +3473,6 @@ public static class Options // ReSharper disable NotAccessedField.Global // Ability Use Gain With Each Task Completed - public static OptionItem VeteranAbilityUseGainWithEachTaskCompleted; public static OptionItem GrenadierAbilityUseGainWithEachTaskCompleted; public static OptionItem LighterAbilityUseGainWithEachTaskCompleted; public static OptionItem SecurityGuardAbilityUseGainWithEachTaskCompleted; @@ -3483,7 +3483,6 @@ public static class Options public static OptionItem LighterAbilityChargesWhenFinishedTasks; public static OptionItem SecurityGuardAbilityChargesWhenFinishedTasks; public static OptionItem PacifistAbilityChargesWhenFinishedTasks; - public static OptionItem VeteranAbilityChargesWhenFinishedTasks; // ReSharper restore NotAccessedField.Global } diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 91bf04309..c4cd7611b 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -845,6 +845,7 @@ public static class Utils case CustomRoles.Eclipse: case CustomRoles.Pyromaniac: case CustomRoles.SerialKiller: + case CustomRoles.Spider: case CustomRoles.SoulCollector: case CustomRoles.Berserker: case CustomRoles.Sharpshooter: @@ -3417,7 +3418,7 @@ public static class Utils CustomRoles.Grenadier => Options.GrenadierSkillCooldown.GetInt() + (includeDuration ? Options.GrenadierSkillDuration.GetInt() : 0), CustomRoles.Lighter => Options.LighterSkillCooldown.GetInt() + (includeDuration ? Options.LighterSkillDuration.GetInt() : 0), CustomRoles.SecurityGuard => Options.SecurityGuardSkillCooldown.GetInt() + (includeDuration ? Options.SecurityGuardSkillDuration.GetInt() : 0), - CustomRoles.Veteran => Options.VeteranSkillCooldown.GetInt() + (includeDuration ? Options.VeteranSkillDuration.GetInt() : 0), + CustomRoles.Veteran => Veteran.VeteranSkillCooldown.GetInt() + (includeDuration ? Veteran.VeteranSkillDuration.GetInt() : 0), CustomRoles.Rhapsode => Rhapsode.AbilityCooldown.GetInt() + (includeDuration ? Rhapsode.AbilityDuration.GetInt() : 0), CustomRoles.Whisperer => Whisperer.Cooldown.GetInt() + (includeDuration ? Whisperer.Duration.GetInt() : 0), CustomRoles.Astral => Astral.AbilityCooldown.GetInt() + (includeDuration ? Astral.AbilityDuration.GetInt() : 0), @@ -3453,6 +3454,7 @@ public static class Utils CustomRoles.Abyssbringer => Abyssbringer.BlackHolePlaceCooldown.GetInt(), CustomRoles.Venerer => Venerer.AbilityCooldown.GetInt(), CustomRoles.Wiper => Wiper.AbilityCooldown.GetInt(), + CustomRoles.Spider => Spider.AbilityCooldown.GetInt(), CustomRoles.Warlock => Warlock.IsCursed ? -1 : Warlock.ShapeshiftCooldown.GetInt(), CustomRoles.Stealth => Stealth.AbilityCooldown.GetInt() + (includeDuration ? Stealth.OptionDarkenDuration.GetInt() : 0), CustomRoles.Stasis => Stasis.AbilityCooldown.GetInt() + (includeDuration ? Stasis.AbilityDuration.GetInt() : 0), @@ -3495,79 +3497,95 @@ public static class Utils public static void AfterMeetingTasks() { LateTask.New(() => GameEndChecker.ShouldNotCheck = false, 0.1f, "Enable GameEndChecker"); - - if (Lovers.PrivateChat.GetBool() && Main.LoversPlayers.TrueForAll(x => x.IsAlive())) - Main.LoversPlayers.ForEach(x => x.SetChatVisible(true)); - AFKDetector.NumAFK = 0; - AFKDetector.PlayerData.Clear(); + try + { + if (Lovers.PrivateChat.GetBool() && Main.LoversPlayers.TrueForAll(x => x.IsAlive())) + Main.LoversPlayers.ForEach(x => x.SetChatVisible(true)); + } + catch (System.Exception e) { ThrowException(e); } - Camouflage.CheckCamouflage(); + try + { + AFKDetector.NumAFK = 0; + AFKDetector.PlayerData.Clear(); - CopyCat.ResetRoles(); - Imitator.SetRoles(); + Camouflage.CheckCamouflage(); + + CopyCat.ResetRoles(); + Imitator.SetRoles(); + } + catch (System.Exception e) { ThrowException(e); } foreach (PlayerControl pc in Main.AllPlayerControls) { - if (pc.IsAlive()) + try { - if (pc.Is(CustomRoles.Bloodlust)) + if (pc.IsAlive()) { - pc.RpcSetRoleDesync(RoleTypes.Impostor, pc.OwnerId); - LateTask.New(() => pc.SetKillCooldown(), 0.2f, log: false); - } - - if (pc.Is(CustomRoles.Truant)) - { - float beforeSpeed = Main.AllPlayerSpeed[pc.PlayerId]; - Main.AllPlayerSpeed[pc.PlayerId] = Main.MinSpeed; - pc.MarkDirtySettings(); - - LateTask.New(() => + try { - Main.AllPlayerSpeed[pc.PlayerId] = beforeSpeed; - pc.MarkDirtySettings(); - }, Options.TruantWaitingTime.GetFloat(), $"Truant Waiting: {pc.GetNameWithRole()}"); - } + if (pc.Is(CustomRoles.Bloodlust)) + { + pc.RpcSetRoleDesync(RoleTypes.Impostor, pc.OwnerId); + LateTask.New(() => pc.SetKillCooldown(), 0.2f, log: false); + } - if (Options.UsePets.GetBool()) - { - pc.AddAbilityCD(false); + if (pc.Is(CustomRoles.Truant)) + { + float beforeSpeed = Main.AllPlayerSpeed[pc.PlayerId]; + Main.AllPlayerSpeed[pc.PlayerId] = Main.MinSpeed; + pc.MarkDirtySettings(); - LateTask.New(() => + LateTask.New(() => + { + Main.AllPlayerSpeed[pc.PlayerId] = beforeSpeed; + pc.MarkDirtySettings(); + }, Options.TruantWaitingTime.GetFloat(), $"Truant Waiting: {pc.GetNameWithRole()}"); + } + } + catch (Exception e) { ThrowException(e); } + + if (Options.UsePets.GetBool()) { - if (GameStates.IsEnded) return; - string petId = PetsHelper.GetPetId(); - PetsHelper.SetPet(pc, petId); - pc.Data.DefaultOutfit.PetSequenceId += 10; - pc.RpcSetPet(petId); - }, 3f, "No Pet Reassign"); + pc.AddAbilityCD(false); + + LateTask.New(() => + { + if (GameStates.IsEnded) return; + string petId = PetsHelper.GetPetId(); + PetsHelper.SetPet(pc, petId); + pc.Data.DefaultOutfit.PetSequenceId += 10; + pc.RpcSetPet(petId); + }, 3f, "No Pet Reassign"); + } + + AFKDetector.RecordPosition(pc); + + if (Camouflage.IsCamouflage) + Camouflage.RpcSetSkin(pc); + } + else + { + TaskState taskState = pc.GetTaskState(); + + if (pc.IsCrewmate() && !taskState.IsTaskFinished && taskState.HasTasks) + pc.Notify(GetString("DoYourTasksPlease"), 8f); + + GhostRolesManager.NotifyAboutGhostRole(pc); } - AFKDetector.RecordPosition(pc); + Main.PlayerStates[pc.PlayerId].Role.AfterMeetingTasks(); - if (Camouflage.IsCamouflage) - Camouflage.RpcSetSkin(pc); - } - else - { - TaskState taskState = pc.GetTaskState(); - - if (pc.IsCrewmate() && !taskState.IsTaskFinished && taskState.HasTasks) - pc.Notify(GetString("DoYourTasksPlease"), 8f); - - GhostRolesManager.NotifyAboutGhostRole(pc); - } - - Main.PlayerStates[pc.PlayerId].Role.AfterMeetingTasks(); - - if (pc.Is(CustomRoles.Phantasm) || pc.Is(CustomRoles.Haunter)) - pc.RpcResetAbilityCooldown(); + if (pc.Is(CustomRoles.Phantasm) || pc.Is(CustomRoles.Haunter)) + pc.RpcResetAbilityCooldown(); - if (pc.Is(CustomRoles.TaskMaster)) - TaskMaster.AfterMeetingTasks(pc); + if (pc.Is(CustomRoles.TaskMaster)) + TaskMaster.AfterMeetingTasks(pc); - Main.CheckShapeshift[pc.PlayerId] = false; + Main.CheckShapeshift[pc.PlayerId] = false; + } + catch (System.Exception e) { ThrowException(e); } } LateTask.New(() => Main.ProcessShapeshifts = true, 1f, log: false); @@ -3575,22 +3593,26 @@ public static class Utils if (Options.DiseasedCDReset.GetBool()) { Main.KilledDiseased.SetAllValues(0); - Main.KilledDiseased.Keys.ToValidPlayers().Do(x => x?.ResetKillCooldown()); + Main.KilledDiseased.Keys.ToValidPlayers().Do(x => x.ResetKillCooldown()); Main.KilledDiseased.Clear(); } if (Options.AntidoteCDReset.GetBool()) { Main.KilledAntidote.SetAllValues(0); - Main.KilledAntidote.Keys.ToValidPlayers().Do(x => x?.ResetKillCooldown()); + Main.KilledAntidote.Keys.ToValidPlayers().Do(x => x.ResetKillCooldown()); Main.KilledAntidote.Clear(); } - Damocles.AfterMeetingTasks(); - Stressed.AfterMeetingTasks(); - Circumvent.AfterMeetingTasks(); - Deadlined.AfterMeetingTasks(); - Tired.Reset(); + try + { + Damocles.AfterMeetingTasks(); + Stressed.AfterMeetingTasks(); + Circumvent.AfterMeetingTasks(); + Deadlined.AfterMeetingTasks(); + Tired.Reset(); + } + catch (System.Exception e) { ThrowException(e); } if (Options.AirshipVariableElectrical.GetBool()) AirshipElectricalDoors.Initialize(); diff --git a/Patches/AirShipElectricalDoors.cs b/Patches/AirShipElectricalDoors.cs index ed0b95e24..114cf9d94 100644 --- a/Patches/AirShipElectricalDoors.cs +++ b/Patches/AirShipElectricalDoors.cs @@ -5,28 +5,26 @@ namespace EHR; public static class AirshipElectricalDoors { - private static ElectricalDoors Instance - => ShipStatus.Instance.Systems[SystemTypes.Decontamination].CastFast(); + private static ElectricalDoors Instance => ShipStatus.Instance.Systems[SystemTypes.Decontamination].CastFast(); public static void Initialize() { if (Main.NormalOptions.MapId != 4) return; - Instance.Initialize(); } - public static byte[] GetClosedDoors() + public static IEnumerable GetClosedDoors() { - List DoorsArray = []; - if (Instance.Doors == null || Instance.Doors.Length == 0) return [.. DoorsArray]; + List doorsArray = []; + if (Instance.Doors == null || Instance.Doors.Length == 0) return doorsArray; for (byte i = 0; i < Instance.Doors.Count; i++) { StaticDoor door = Instance.Doors[i]; - if (door != null && !door.IsOpen) DoorsArray.Add(i); + if (door != null && !door.IsOpen) doorsArray.Add(i); } - return DoorsArray?.ToArray(); + return doorsArray; } // 0: BottomRightHort // 1: BottomHort @@ -48,22 +46,6 @@ internal static class ElectricalDoorsInitializePatch public static void Postfix( /*ElectricalDoors __instance*/) { if (!GameStates.IsInGame) return; - - var closedoors = string.Empty; - var isFirst = true; - byte[] array = AirshipElectricalDoors.GetClosedDoors(); - - foreach (byte num in array) - { - if (isFirst) - { - isFirst = false; - closedoors += num.ToString(); - } - else - closedoors += $", {num}"; - } - - Logger.Info($"Closed Doors: {closedoors}", "ElectricalDoors Initialize"); + Logger.Info($"Closed Doors: {string.Join(", ", AirshipElectricalDoors.GetClosedDoors())}", "ElectricalDoors Initialize"); } } \ No newline at end of file diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 528694a91..c1b48324a 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -148,7 +148,7 @@ internal static class ChatCommands new(["gamestate", "gstate", "gs", "kcount", "kc", "кубийц", "гс", "статигры", "对局状态", "estadojogo", "status"], "", GetString("CommandDescription.KCount"), Command.UsageLevels.Everyone, Command.UsageTimes.InGame, KCountCommand, true, false), new(["addmod", "добмодера", "指定协管", "moderador-add"], "{id}", GetString("CommandDescription.AddMod"), Command.UsageLevels.Host, Command.UsageTimes.Always, AddModCommand, true, false, [GetString("CommandArgs.AddMod.Id")]), new(["deletemod", "убрмодера", "удмодера", "убратьмодера", "удалитьмодера", "移除协管", "moderador-remover"], "{id}", GetString("CommandDescription.DeleteMod"), Command.UsageLevels.Host, Command.UsageTimes.Always, DeleteModCommand, true, false, [GetString("CommandArgs.DeleteMod.Id")]), - new(["combo", "комбо", "设置不会同时出现的职业", "combinação", "combinar"], "{mode} {role} {addon} [all]", GetString("CommandDescription.Combo"), Command.UsageLevels.Host, Command.UsageTimes.Always, ComboCommand, true, false, [GetString("CommandArgs.Combo.Mode"), GetString("CommandArgs.Combo.Role"), GetString("CommandArgs.Combo.Addon"), GetString("CommandArgs.Combo.All")]), + new(["combo", "комбо", "设置不会同时出现的职业", "combinação", "combinar"], "{mode} {role} {addon} [all]", GetString("CommandDescription.Combo"), Command.UsageLevels.Everyone, Command.UsageTimes.Always, ComboCommand, true, false, [GetString("CommandArgs.Combo.Mode"), GetString("CommandArgs.Combo.Role"), GetString("CommandArgs.Combo.Addon"), GetString("CommandArgs.Combo.All")]), new(["eff", "effect", "эффект", "效果", "efeito"], "{effect}", GetString("CommandDescription.Effect"), Command.UsageLevels.Host, Command.UsageTimes.InGame, EffectCommand, true, false, [GetString("CommandArgs.Effect.Effect")]), new(["afkexempt", "освафк", "афкосв", "挂机检测器不会检测", "afk-isentar"], "{id}", GetString("CommandDescription.AFKExempt"), Command.UsageLevels.HostOrAdmin, Command.UsageTimes.Always, AFKExemptCommand, true, false, [GetString("CommandArgs.AFKExempt.Id")]), new(["m", "myrole", "м", "мояроль", "我的职业", "minhafunção"], "", GetString("CommandDescription.MyRole"), Command.UsageLevels.Everyone, Command.UsageTimes.InGame, MyRoleCommand, true, false), @@ -156,7 +156,7 @@ internal static class ChatCommands new(["tpin", "тпин", "传送进"], "", GetString("CommandDescription.TPIn"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, TPInCommand, true, false), new(["t", "template", "т", "темплейт", "模板"], "{tag}", GetString("CommandDescription.Template"), Command.UsageLevels.Everyone, Command.UsageTimes.Always, TemplateCommand, true, false, [GetString("CommandArgs.Template.Tag")]), new(["mw", "messagewait", "мв", "медленныйрежим", "消息冷却", "espera-mensagens"], "{duration}", GetString("CommandDescription.MessageWait"), Command.UsageLevels.Host, Command.UsageTimes.Always, MessageWaitCommand, true, false, [GetString("CommandArgs.MessageWait.Duration")]), - new(["death", "d", "д", "смерть", "死亡原因", "abate"], "", GetString("CommandDescription.Death"), Command.UsageLevels.Everyone, Command.UsageTimes.AfterDeath, DeathCommand, true, false), + new(["death", "d", "д", "смерть", "死亡原因", "abate"], "[id]", GetString("CommandDescription.Death"), Command.UsageLevels.Everyone, Command.UsageTimes.AfterDeath, DeathCommand, true, false, [GetString("CommandArgs.Death.Id")]), new(["say", "s", "сказать", "с", "说", "falar", "dizer"], "{message}", GetString("CommandDescription.Say"), Command.UsageLevels.HostOrModerator, Command.UsageTimes.Always, SayCommand, true, false, [GetString("CommandArgs.Say.Message")]), new(["vote", "голос", "投票给", "votar"], "{id}", GetString("CommandDescription.Vote"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, VoteCommand, true, true, [GetString("CommandArgs.Vote.Id")]), new(["ask", "спр", "спросить", "数学家提问", "perguntar"], "{number1} {number2}", GetString("CommandDescription.Ask"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, AskCommand, true, true, [GetString("CommandArgs.Ask.Number1"), GetString("CommandArgs.Ask.Number2")]), @@ -212,7 +212,7 @@ internal static class ChatCommands new(["deletetag", "удалитьтег", "убратьтег", "удтег", "убртег", "删除标签"], "{id}", GetString("CommandDescription.DeleteTag"), Command.UsageLevels.Host, Command.UsageTimes.InLobby, DeleteTagCommand, true, false, [GetString("CommandArgs.DeleteTag.Id")]), new(["daybreak", "db", "дейбрейк", "破晓"], "", GetString("CommandDescription.DayBreak"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, DayBreakCommand, true, true), new(["fix", "blackscreenfix", "fixblackscreen", "фикс", "исправить", "修复"], "{id}", GetString("CommandDescription.Fix"), Command.UsageLevels.HostOrModerator, Command.UsageTimes.InGame, FixCommand, true, false, [GetString("CommandArgs.Fix.Id")]), - new(["xor", "异或命令"], "{role} {role}", GetString("CommandDescription.XOR"), Command.UsageLevels.Host, Command.UsageTimes.InLobby, XORCommand, true, false, [GetString("CommandArgs.XOR.Role"), GetString("CommandArgs.XOR.Role")]), + new(["xor", "异或命令"], "{role} {role}", GetString("CommandDescription.XOR"), Command.UsageLevels.Everyone, Command.UsageTimes.Always, XORCommand, true, false, [GetString("CommandArgs.XOR.Role"), GetString("CommandArgs.XOR.Role")]), new(["ci", "chemistinfo", "химик", "化学家"], "", GetString("CommandDescription.ChemistInfo"), Command.UsageLevels.Everyone, Command.UsageTimes.Always, ChemistInfoCommand, true, false), new(["forge"], "{id} {role}", GetString("CommandDescription.Forge"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, ForgeCommand, true, true, [GetString("CommandArgs.Forge.Id"), GetString("CommandArgs.Forge.Role")]), new(["choose", "pick", "выбрать", "选择", "escolher"], "{role}", GetString("CommandDescription.Choose"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, ChooseCommand, true, true, [GetString("CommandArgs.Choose.Role")]), @@ -852,7 +852,17 @@ internal static class ChatCommands private static void XORCommand(PlayerControl player, string text, string[] args) { - if (args.Length < 3 || !GetRoleByName(args[1], out CustomRoles role1) || !GetRoleByName(args[2], out CustomRoles role2)) return; + if (!AmongUsClient.Instance.AmHost) + { + RequestCommandProcessingFromHost(nameof(XORCommand), text); + return; + } + + if (!player.IsHost() || args.Length < 3 || !GetRoleByName(args[1], out CustomRoles role1) || !GetRoleByName(args[2], out CustomRoles role2)) + { + Utils.SendMessage(string.Join('\n', Main.XORRoles.ConvertAll(x => $"{x.Item1.ToColoredString()} ⊕ {x.Item2.ToColoredString()}")), player.PlayerId, GetString("XORListTitle")); + return; + } if (Main.XORRoles.Remove((role1, role2)) || Main.XORRoles.Remove((role2, role1))) { @@ -2381,11 +2391,14 @@ internal static class ChatCommands if (!GameStates.IsInGame) return; if (Main.DiedThisRound.Contains(player.PlayerId) && Utils.IsRevivingRoleAlive()) return; - PlayerControl killer = player.GetRealKiller(); + PlayerControl target = args.Length < 2 || !byte.TryParse(args[1], out byte targetId) ? player : targetId.GetPlayer(); + if (target == null) return; + + PlayerControl killer = target.GetRealKiller(); if (killer == null) { - Utils.SendMessage("\n", player.PlayerId, string.Format(GetString("DeathCommandFail"), GetString($"DeathReason.{Main.PlayerStates[player.PlayerId].deathReason}")), sendOption: SendOption.None); + Utils.SendMessage("\n", player.PlayerId, string.Format(GetString("DeathCommandFail"), GetString($"DeathReason.{Main.PlayerStates[target.PlayerId].deathReason}")), sendOption: SendOption.None); return; } @@ -2547,7 +2560,7 @@ internal static class ChatCommands return; } - if (args.Length < 4) + if (!player.IsHost() || args.Length < 4) { if (Main.AlwaysSpawnTogetherCombos.Count == 0 && Main.NeverSpawnTogetherCombos.Count == 0) return; @@ -2934,10 +2947,10 @@ internal static class ChatCommands if (args.Length < 2) return; - string name = string.Join(' ', args[1..]); + string name = Regex.Replace(string.Join(' ', args[1..]), "]*>", string.Empty).Trim(); if (name.RemoveHtmlTags().Length is > 15 or < 1) - Utils.SendMessage(GetString("Message.AllowNameLength"), player.PlayerId); + Utils.SendMessage(GetString("Message.AllowNameLength"), player.PlayerId, sendOption: SendOption.None); else { if (player.AmOwner) @@ -2956,12 +2969,6 @@ internal static class ChatCommands return; } - if (name.Length is > 50 or < 1) - { - Utils.SendMessage(GetString("Message.AllowNameLength"), player.PlayerId, sendOption: SendOption.None); - return; - } - Main.AllPlayerNames[player.PlayerId] = name; player.RpcSetName(name); } diff --git a/Patches/ControlPatch.cs b/Patches/ControlPatch.cs index 89086b496..843c0e260 100644 --- a/Patches/ControlPatch.cs +++ b/Patches/ControlPatch.cs @@ -145,7 +145,7 @@ internal static class ControllerManagerUpdatePatch Logger.SendInGame(GetString("CancelStartCountDown")); } - if (Input.GetKeyDown(KeyCode.Return) && GameStates.IsLobby && GameStartManager.InstanceExists && GameStartManager.Instance.startState == GameStartManager.StartingStates.NotStarting && !HudManager.Instance.Chat.IsOpenOrOpening && !OnGameJoinedPatch.JoiningGame && GameSettingMenu.Instance == null) + if (Input.GetKeyDown(KeyCode.Return) && GameStates.IsLobby && GameStartManager.InstanceExists && Options.EnterKeyToStartGame.GetBool() && GameStartManager.Instance.startState == GameStartManager.StartingStates.NotStarting && !HudManager.Instance.Chat.IsOpenOrOpening && !OnGameJoinedPatch.JoiningGame && GameSettingMenu.Instance == null) { Logger.Info("ENTER pressed: Starting game by host", "KeyCommand"); GameStartManager.Instance.BeginGame(); diff --git a/Patches/MapPickerMenuPatch.cs b/Patches/MapPickerMenuPatch.cs index 06f833f04..2f2c5110d 100644 --- a/Patches/MapPickerMenuPatch.cs +++ b/Patches/MapPickerMenuPatch.cs @@ -22,7 +22,7 @@ internal static class CreateOptionsPickerPatch [HarmonyPrefix] public static void Prefix_SelectMap([HarmonyArgument(0)] ref int mapId) { - if (mapId > 5) mapId = 0; + if (mapId is > 5 or 3) mapId = 0; } [HarmonyPatch(typeof(GameOptionsMapPicker), nameof(GameOptionsMapPicker.SetupMapButtons))] diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 43b8336cd..e94f156f5 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -295,6 +295,9 @@ internal static class CheckMurderPatch if (Pursuer.OnClientMurder(killer)) return false; + if (Main.PlayerStates[target.PlayerId].Role is Veteran veteran && Veteran.VeteranAlertActivatesOnNonKillingInteractions.GetBool() && !veteran.OnCheckMurderAsTarget(killer, target)) + return false; + Seamstress.OnAnyoneCheckMurder(killer, target); if (killer.PlayerId != target.PlayerId) @@ -878,18 +881,15 @@ internal static class ShapeshiftPatch PlagueBearer.CheckAndSpreadInfection(target, shapeshifter); } - var isSSneeded = true; + if (Main.PlayerStates[target.PlayerId].Role is Veteran veteran && Veteran.VeteranAlertActivatesOnNonKillingInteractions.GetBool()) + veteran.OnCheckMurderAsTarget(shapeshifter, target); - if (!Pelican.IsEaten(shapeshifter.PlayerId) && !GameStates.IsVoting) + var isSSneeded = shapeshifter.IsAlive(); + + if (!Pelican.IsEaten(shapeshifter.PlayerId) && !GameStates.IsVoting && isSSneeded) isSSneeded = Main.PlayerStates[shapeshifter.PlayerId].Role.OnShapeshift(shapeshifter, target, shapeshifting); - if (shapeshifter.Is(CustomRoles.Hangman) && shapeshifter.GetAbilityUseLimit() < 1 && shapeshifting) - { - shapeshifter.SetKillCooldown(Hangman.ShapeshiftDuration.GetFloat() + 1f); - isSSneeded = false; - } - - bool forceCancel = role.ForceCancelShapeshift(); + bool forceCancel = role.ForceCancelShapeshift() || !shapeshifter.IsAlive(); if (Changeling.ChangedRole.TryGetValue(shapeshifter.PlayerId, out bool changed) && changed && shapeshifter.GetRoleTypes() != RoleTypes.Shapeshifter) { diff --git a/Resources/Config/EACList.txt b/Resources/Config/EACList.txt index 301e6e0b0..5780d15c4 100644 --- a/Resources/Config/EACList.txt +++ b/Resources/Config/EACList.txt @@ -1,4 +1,5 @@ ## Hackers and cheaters +chancygrid#7957,1466867f1 flowerfit#4735,422cfefe8 wideink#3618,677db6a5c, Fastpathos // Hacking streamer's lobby wiggreeny#9194,91f48d4e0 @@ -92,6 +93,7 @@ woveelixir#4088,42044d211,DMD 3CD //hacker and probably BoomplayYT's alt tenpalace#1924,6a259fcd9,Goaty cat#2761,f70d4ddb1,ULC pinbrittle#1538,4955c179f,Moralmaser // Cheating and disrupting lobbies +dollfly#4791,5ba449454,Mari // Spamming RPCs to crash lobbies ## Griefers, toxic, harrasing, morons setmolar#4374,藏族顶针 diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 51ef0d760..c569f7bbd 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -1093,6 +1093,7 @@ "AutoMPollCommandCooldown": "Delay Before Auto Executing /mpoll (After Joining The Lobby)", "AutoDraftStartCommandAfterJoin": "Auto Execute /draftstart After Joining The Lobby", "AutoDraftStartCommandCooldown": "Delay Before Auto Executing /draftstart (After Joining The Lobby)", + "EnterKeyToStartGame": "Begin the starting countdown by pressing the Enter key", "RemovePetsAtDeadPlayers": "Remove pets from dead players", "SeeEjectedRolesInMeeting": "See ejected roles in meetings", "EnableKillerLeftCommand": "Enable use of /gs (formerly /kcount) command", @@ -1767,6 +1768,7 @@ "JudgeCanTrialMadmate": "Can trial Madmates", "JudgeCanTrialConverted": "Can trial <#ffab1b>Converted players", "VeteranSkillMaxOfUsage": "(Initial) Max number of Alerts", + "VeteranAlertActivatesOnNonKillingInteractions": "Kill Killer on Non-Killing Interactions (e.g., douse, shield, etc.)", "SwooperCooldown": "Swoop Cooldown", "SwooperDuration": "Swoop Duration", "SwooperVentNormallyOnCooldown": "Swooper vents normally when swooping is on cooldown", @@ -1803,10 +1805,6 @@ "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", - "InfectiousBiteMax": "Maximum Infections", - "InfectiousKnowTargetRole": "Know infected player's role", - "InfectiousTargetKnowOtherTarget": "Infected players know each other", "VirusKillCooldown": "Kill Cooldown", "VirusCanVent": "Can Vent", "VirusInfectMax": "Maximum Number Of Spreads", @@ -1978,6 +1976,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!", @@ -2433,7 +2432,7 @@ "CommandDescription.TPIn": "Teleport into the lobby", "CommandDescription.Template": "Send a message template (from template.txt)", "CommandDescription.MessageWait": "Set the buffer time for system messages", - "CommandDescription.Death": "View who killed you and their role", + "CommandDescription.Death": "View who killed you (or another player) and their role", "CommandDescription.Say": "Send a message as the host/moderator", "CommandDescription.Vote": "Vote for a player", "CommandDescription.Ask": "Ask a question as the Mathematician", @@ -2530,6 +2529,7 @@ "CommandArgs.AFKExempt.Id": "The player's ID to exempt from AFK detection", "CommandArgs.Template.Tag": "The tag of the template to send", "CommandArgs.MessageWait.Duration": "The duration in seconds", + "CommandArgs.Death.Id": "The player's ID to view the killer of (leave blank for yourself)", "CommandArgs.Say.Message": "The message to send", "CommandArgs.Vote.Id": "The player's ID to vote for", "CommandArgs.Ask.Number1": "The first number in the question", @@ -2676,6 +2676,7 @@ "XORSameRole": "<#ff0000>X XOR combinations must have 2 different roles.", "XORAdditionRole": "<#ff0000>X XOR combinations don't support add-ons. Use <#ffff00>/combo for that.", "XORAdded": "<#00ffa5>✓ XOR combination added successfully.", + "XORListTitle": "<#00a5ff>➡ XOR Combinations ⬅", "ForgeSuccess": "<#00ffa5>✓ Successfully forged {1} as {2}.\\nYou have {0} ability uses left.", "ThankYouForUsingEHR": "Thank you for using EHR!", "GMRoles": "【 ★ Roles ★ 】", @@ -9461,6 +9462,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", @@ -9658,6 +9660,12 @@ "DazzlerResetDazzledVisionOnDeath": "Reset vision of dazzled players on death/eject", "DazzleCooldown": "Dazzle Cooldown", "DazzleButtonText": "Dazzle", + "Spider": "Spider", + "SpiderInfo": "Trap players in your webs then eat them", + "SpiderInfoLong": "(Neutrals):\\nAs the Spider, vanish/pet/shapeshift to place a web. If someone walks into it, they'll be frozen and have lower vision, and you'll get a notification about someone being trapped in your web. If someone is in your web for too long, the web breaks and they get freed. You can only kill the players that are trapped in a web!\\nKill everyone to win!", + "Spider.WebTrapRange": "Web Trap Range", + "Spider.LowerVisionBy": "Lower Vision By", + "Spider.TrappedDuration": "Trapped Duration (after which the web breaks)", "Gardener": "Gardener", "GardenerInfo": "Plant Plants to Block Kills", "GardenerInfoLong": "(Crewmates):\\nAs the Gardener, pet to place a plant. After the next meeting, it will be grown. No one can directly kill near grown plants. However, once a plant has blocked a kill attempt, it despawns.", @@ -10083,6 +10091,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 vanish to 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/Crewmate/Veteran.cs b/Roles/Crewmate/Veteran.cs index 78169579d..82933e5b9 100644 --- a/Roles/Crewmate/Veteran.cs +++ b/Roles/Crewmate/Veteran.cs @@ -11,6 +11,13 @@ internal class Veteran : RoleBase public static bool On; public override bool IsEnable => On; + + public static OptionItem VeteranSkillCooldown; + public static OptionItem VeteranSkillDuration; + public static OptionItem VeteranSkillMaxOfUsage; + public static OptionItem VeteranAbilityUseGainWithEachTaskCompleted; + public static OptionItem VeteranAbilityChargesWhenFinishedTasks; + public static OptionItem VeteranAlertActivatesOnNonKillingInteractions; public override void SetupCustomOption() { @@ -36,6 +43,9 @@ internal class Veteran : RoleBase VeteranAbilityChargesWhenFinishedTasks = new FloatOptionItem(id + 6, "AbilityChargesWhenFinishedTasks", new(0f, 5f, 0.05f), 0.2f, TabGroup.CrewmateRoles) .SetParent(CustomRoleSpawnChances[CustomRoles.Veteran]) .SetValueFormat(OptionFormat.Times); + + VeteranAlertActivatesOnNonKillingInteractions = new BooleanOptionItem(id + 7, "VeteranAlertActivatesOnNonKillingInteractions", false, TabGroup.CrewmateRoles) + .SetParent(CustomRoleSpawnChances[CustomRoles.Veteran]); } public override void Add(byte playerId) @@ -106,9 +116,7 @@ internal class Veteran : RoleBase { if (!killer.IsAlive()) return false; - if (VeteranInProtect.ContainsKey(target.PlayerId) - && killer.PlayerId != target.PlayerId - && VeteranInProtect[target.PlayerId] + VeteranSkillDuration.GetInt() >= Utils.TimeStamp) + if (VeteranInProtect.ContainsKey(target.PlayerId) && killer.PlayerId != target.PlayerId) { if (!killer.Is(CustomRoles.Pestilence)) { diff --git a/Roles/Impostor/ClockBlocker.cs b/Roles/Impostor/ClockBlocker.cs index 503a57fde..def1f5fb7 100644 --- a/Roles/Impostor/ClockBlocker.cs +++ b/Roles/Impostor/ClockBlocker.cs @@ -52,7 +52,7 @@ public class ClockBlocker : RoleBase private int GetAddedTime() { - return !Main.PlayerStates.TryGetValue(ClockBlockerId, out PlayerState state) || (state.IsDead && !CountAddedTimeAfterDeath.GetBool()) ? 0 : EmergencyCooldownIncreasePerKill.GetInt() * state.GetKillCount(true); + return !Main.PlayerStates.TryGetValue(ClockBlockerId, out PlayerState state) || (state.IsDead && !CountAddedTimeAfterDeath.GetBool()) ? 0 : EmergencyCooldownIncreasePerKill.GetInt() * state.GetKillCount(); } public static int GetTotalTime(int originalTime) diff --git a/Roles/Impostor/Hangman.cs b/Roles/Impostor/Hangman.cs index 6f06428df..bf5ecc0d4 100644 --- a/Roles/Impostor/Hangman.cs +++ b/Roles/Impostor/Hangman.cs @@ -96,6 +96,17 @@ public class Hangman : RoleBase return true; } + public override bool OnShapeshift(PlayerControl shapeshifter, PlayerControl target, bool shapeshifting) + { + if (shapeshifter.GetAbilityUseLimit() < 1 && shapeshifting) + { + shapeshifter.SetKillCooldown(ShapeshiftDuration.GetFloat() + 1f); + return false; + } + + return true; + } + public override void SetButtonTexts(HudManager hud, byte id) { if (id.IsPlayerShifted()) hud.KillButton?.OverrideText(Translator.GetString("HangmanKillButtonTextDuringSS")); diff --git a/Roles/Impostor/Mercenary.cs b/Roles/Impostor/Mercenary.cs index aedd9d834..df5ee856a 100644 --- a/Roles/Impostor/Mercenary.cs +++ b/Roles/Impostor/Mercenary.cs @@ -63,7 +63,7 @@ public class Mercenary : RoleBase private static bool HasKilled(PlayerControl pc) { - return pc != null && pc.Is(CustomRoles.Mercenary) && pc.IsAlive() && (Main.PlayerStates[pc.PlayerId].GetKillCount(true) > 0 || !WaitFor1Kill.GetBool()); + return pc != null && pc.Is(CustomRoles.Mercenary) && pc.IsAlive() && (Main.PlayerStates[pc.PlayerId].GetKillCount() > 0 || !WaitFor1Kill.GetBool()); } public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) diff --git a/Roles/Impostor/TimeThief.cs b/Roles/Impostor/TimeThief.cs index 49a0f837e..602434d53 100644 --- a/Roles/Impostor/TimeThief.cs +++ b/Roles/Impostor/TimeThief.cs @@ -57,7 +57,7 @@ public class TimeThief : RoleBase private static int StolenTime(byte id) { return PlayerIdList.Contains(id) && (Utils.GetPlayerById(id).IsAlive() || !ReturnStolenTimeUponDeath.GetBool()) - ? DecreaseMeetingTime.GetInt() * Main.PlayerStates[id].GetKillCount(true) + ? DecreaseMeetingTime.GetInt() * Main.PlayerStates[id].GetKillCount() : 0; } 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; diff --git a/Roles/Neutral/Spider.cs b/Roles/Neutral/Spider.cs new file mode 100644 index 000000000..4546732dd --- /dev/null +++ b/Roles/Neutral/Spider.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; +using EHR.Modules; +using Hazel; + +namespace EHR.Neutral; + +public class Spider : RoleBase +{ + public static bool On; + private static List Instances = []; + + public static OptionItem AbilityCooldown; + private static OptionItem WebTrapRange; + private static OptionItem LowerVisionBy; + private static OptionItem TrappedDuration; + private static OptionItem KillCooldown; + private static OptionItem CanVent; + private static OptionItem ImpostorVision; + + public override bool IsEnable => On; + + private Dictionary> Webs = []; + private long LastNotifyTS; + private bool NameDirty; + private byte SpiderId; + + public override void SetupCustomOption() + { + StartSetup(12800) + .AutoSetupOption(ref AbilityCooldown, 30f, new FloatValueRule(0f, 180f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref WebTrapRange, 2f, new FloatValueRule(0.05f, 10f, 0.05f), OptionFormat.Multiplier) + .AutoSetupOption(ref LowerVisionBy, 0.5f, new FloatValueRule(0f, 3f, 0.05f), OptionFormat.Multiplier) + .AutoSetupOption(ref TrappedDuration, 20f, new FloatValueRule(0f, 180f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref KillCooldown, 10f, new FloatValueRule(0f, 180f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref CanVent, true) + .AutoSetupOption(ref ImpostorVision, true); + } + + public override void Init() + { + On = false; + Instances = []; + } + + public override void Add(byte playerId) + { + On = true; + Webs = []; + LastNotifyTS = 0; + NameDirty = false; + SpiderId = playerId; + Instances.Add(this); + } + + 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()); + + if (Options.UsePhantomBasis.GetBool() && Options.UsePhantomBasisForNKs.GetBool()) + { + AURoleOptions.PhantomCooldown = AbilityCooldown.GetFloat(); + AURoleOptions.PhantomDuration = 1f; + } + else if (!Options.UsePets.GetBool()) + { + AURoleOptions.ShapeshifterCooldown = AbilityCooldown.GetFloat(); + AURoleOptions.ShapeshifterDuration = 1f; + } + } + + public override bool CanUseImpostorVentButton(PlayerControl pc) + { + return CanVent.GetBool(); + } + + public override bool OnShapeshift(PlayerControl shapeshifter, PlayerControl target, bool shapeshifting) + { + if (!shapeshifting) return true; + UseAbility(shapeshifter); + return false; + } + + public override bool OnVanish(PlayerControl pc) + { + UseAbility(pc); + return false; + } + + public override void OnPet(PlayerControl pc) + { + UseAbility(pc); + } + + void UseAbility(PlayerControl player) + { + Vector2 pos = player.Pos(); + if (Webs.Keys.Any(x => Vector2.Distance(x, pos) <= WebTrapRange.GetFloat() * 2f)) return; + Webs[pos] = []; + LocateArrow.Add(player.PlayerId, pos); + player.Notify(Translator.GetString("MarkDone")); + } + + public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) + { + return Webs.Any(x => x.Value.ContainsKey(target.PlayerId)); + } + + public override void OnMurder(PlayerControl killer, PlayerControl target) + { + Main.PlayerStates[target.PlayerId].deathReason = PlayerState.DeathReason.Eaten; + Webs.DoIf(x => x.Value.Remove(target.PlayerId), x => Utils.SendRPC(CustomRPC.SyncRoleData, SpiderId, 3, x.Key, target.PlayerId)); + } + + public override void OnCheckPlayerPosition(PlayerControl pc) + { + if (pc.PlayerId == SpiderId) return; + + Vector2 pos = pc.Pos(); + + if (Webs.FindFirst(x => Vector2.Distance(x.Key, pos) <= WebTrapRange.GetFloat() && x.Value.TryAdd(pc.PlayerId, Utils.TimeStamp + TrappedDuration.GetInt()), out KeyValuePair> kvp)) + { + pc.MarkDirtySettings(); + Utils.SendRPC(CustomRPC.SyncRoleData, SpiderId, 1, kvp.Key, pc.PlayerId, kvp.Value.Last().Value); + } + } + + public override void OnFixedUpdate(PlayerControl pc) + { + long now = Utils.TimeStamp; + List toRemove = []; + + foreach ((Vector2 pos, Dictionary trapped) in Webs) + { + if (trapped.Count > 0) + NameDirty = true; + + if (trapped.Values.Min() <= now) + { + toRemove.Add(pos); + LateTask.New(() => + { + trapped.Keys.ToValidPlayers().Do(x => + { + Main.AllPlayerSpeed[x.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); + x.MarkDirtySettings(); + }); + }, 0.2f, log: false); + } + } + + toRemove.ForEach(x => + { + Webs.Remove(x); + LocateArrow.Remove(pc.PlayerId, x); + Utils.SendRPC(CustomRPC.SyncRoleData, SpiderId, 2, x); + }); + + if (NameDirty && now != LastNotifyTS) + { + NameDirty = false; + LastNotifyTS = now; + Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); + } + } + + public static void OnAnyoneApplyGameOptions(IGameOptions opt, byte id) + { + foreach (Spider instance in Instances) + { + if (instance.Webs.Any(x => x.Value.ContainsKey(id))) + { + float vision = Main.DefaultCrewmateVision - LowerVisionBy.GetFloat(); + opt.SetFloat(FloatOptionNames.CrewLightMod, vision); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, vision); + Main.AllPlayerSpeed[id] = Main.MinSpeed; + } + } + } + + public void ReceiveRPC(MessageReader reader) + { + switch (reader.ReadPackedInt32()) + { + case 1: + Vector2 pos = reader.ReadVector2(); + byte trappedId = reader.ReadByte(); + long releaseTS = long.Parse(reader.ReadString()); + Webs.TryAdd(pos, []); + Webs[pos][trappedId] = releaseTS; + break; + case 2: + Webs.Remove(reader.ReadVector2()); + break; + case 3: + Vector2 webPos = reader.ReadVector2(); + byte deadId = reader.ReadByte(); + Webs.GetValueOrDefault(webPos, []).Remove(deadId); + break; + } + } + + public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) + { + if (seer.PlayerId != SpiderId || seer.PlayerId != target.PlayerId || (seer.IsModdedClient() && !hud) || meeting) return string.Empty; + return string.Join('\n', Webs.Where(x => x.Value.Count > 0).Select(x => $"{LocateArrow.GetArrow(seer, x.Key)} {x.Value.Count} ({Math.Max(0, x.Value.Values.Min() - Utils.TimeStamp)}s)")); + } +} \ No newline at end of file