From fc57ca87a019cc2a45b6406a86ce7438ebc188b4 Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Mon, 15 Sep 2025 15:11:00 +0200 Subject: [PATCH] Add Compelled, Inquisitor, Postponer, Tree --- CustomRoles.cs | 4 ++ Main.cs | 5 ++ Modules/CustomNetObject.cs | 9 +++ Modules/CustomRolesHelper.cs | 10 +++- Modules/FixedUpdateCaller.cs | 12 ++++ Modules/OptionHolder.cs | 11 ++++ Modules/Utils.cs | 8 +++ Patches/ChatCommandPatch.cs | 13 ++++- Patches/IntroPatch.cs | 2 + Patches/MeetingHudPatch.cs | 3 + Patches/OutroPatch.cs | 1 + Patches/PlayerControlPatch.cs | 8 +++ Patches/PlayerJoinAndLeftPatch.cs | 1 + Resources/Lang/en_US.json | 18 ++++++ Roles/AddOns/Compelled.cs | 11 ++++ Roles/Crewmate/Inquisitor.cs | 68 ++++++++++++++++++++++ Roles/Crewmate/Tree.cs | 95 +++++++++++++++++++++++++++++++ Roles/Impostor/AntiAdminer.cs | 2 +- Roles/Impostor/Postponer.cs | 63 ++++++++++++++++++++ 19 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 Roles/AddOns/Compelled.cs create mode 100644 Roles/Crewmate/Inquisitor.cs create mode 100644 Roles/Crewmate/Tree.cs create mode 100644 Roles/Impostor/Postponer.cs diff --git a/CustomRoles.cs b/CustomRoles.cs index 1f22880c5..e3d38adbc 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -86,6 +86,7 @@ public enum CustomRoles Overheat, Parasite, Penguin, + Postponer, Psychopath, Puppeteer, QuickShooter, @@ -201,6 +202,7 @@ public enum CustomRoles Insight, ParityCop, // Inspector Inquirer, + Inquisitor, Jailor, Journalist, Judge, @@ -266,6 +268,7 @@ public enum CustomRoles Transmitter, Transporter, Tracefinder, + Tree, Tunneler, Ventguard, Veteran, @@ -489,6 +492,7 @@ public enum CustomRoles Cleansed, Clumsy, Commited, + Compelled, Contagious, Damocles, Deadlined, diff --git a/Main.cs b/Main.cs index 461818bdf..d4e7c9405 100644 --- a/Main.cs +++ b/Main.cs @@ -154,6 +154,8 @@ public class Main : BasePlugin public static Dictionary NumEmergencyMeetingsUsed = []; public static int MadmateNum; public static uint LobbyBehaviourNetId; + + public static float GameTimer; public static bool ShowResult = true; @@ -415,6 +417,8 @@ public class Main : BasePlugin { CustomRoles.Gardener, "#00ff00" }, { CustomRoles.Farmer, "#FFDE59" }, { CustomRoles.Transmitter, "#c9a11e" }, + { CustomRoles.Tree, "#00ff00" }, + { CustomRoles.Inquisitor, "#7726B6" }, { CustomRoles.Imitator, "#c99e28" }, { CustomRoles.PortalMaker, "#700078" }, { CustomRoles.Ankylosaurus, "#7FE44C" }, @@ -642,6 +646,7 @@ public class Main : BasePlugin { CustomRoles.Listener, "#060270" }, { CustomRoles.Unbound, "#DFC57B" }, { CustomRoles.AntiTP, "#fcba03" }, + { CustomRoles.Compelled, "#D2E44C" }, { CustomRoles.Commited, "#f5c542" }, { CustomRoles.BananaMan, "#ffe135" }, { CustomRoles.Blind, "#666666" }, diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs index d80523140..cee4d87f6 100644 --- a/Modules/CustomNetObject.cs +++ b/Modules/CustomNetObject.cs @@ -12,6 +12,7 @@ using InnerNet; using TMPro; using UnityEngine; using GameStates = EHR.GameStates; +using Tree = EHR.Crewmate.Tree; // Credit: https://github.com/Rabek009/MoreGamemodes/blob/e054eb498094dfca0a365fc6b6fea8d17f9974d7/Modules/AllObjects // Huge thanks to Rabek009 for this code! @@ -681,6 +682,14 @@ namespace EHR } } + internal sealed class FallenTree : CustomNetObject + { + public FallenTree(Vector2 position) + { + CreateNetObject(Tree.FallenSprite, position); + } + } + internal sealed class ShapeshiftMenuElement : CustomNetObject { private readonly byte VisibleTo; diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 7ddbd1dd6..3449f987d 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -40,6 +40,7 @@ internal static class CustomRolesHelper CustomRoles.Gardener, CustomRoles.Farmer, CustomRoles.Explosivist, + CustomRoles.Tree, // Add-ons CustomRoles.Energetic, @@ -151,6 +152,7 @@ internal static class CustomRolesHelper CustomRoles.Forger => CustomRoles.Impostor, CustomRoles.ClockBlocker => CustomRoles.Impostor, CustomRoles.Psychopath => CustomRoles.Impostor, + CustomRoles.Postponer => CustomRoles.Impostor, CustomRoles.Venerer => CustomRoles.Shapeshifter, CustomRoles.Kidnapper => CustomRoles.Shapeshifter, CustomRoles.Stasis => UsePets ? CustomRoles.Impostor : CustomRoles.Shapeshifter, @@ -233,6 +235,8 @@ internal static class CustomRolesHelper CustomRoles.Bane => CustomRoles.Crewmate, CustomRoles.Farmer => CustomRoles.Crewmate, CustomRoles.Transmitter => CustomRoles.Crewmate, + CustomRoles.Tree => CustomRoles.Crewmate, + CustomRoles.Inquisitor => CustomRoles.Crewmate, CustomRoles.Gardener => CustomRoles.Crewmate, CustomRoles.Imitator => CustomRoles.Crewmate, CustomRoles.PortalMaker => CustomRoles.Crewmate, @@ -710,6 +714,7 @@ internal static class CustomRolesHelper CustomRoles.Wiper or CustomRoles.Psychopath or CustomRoles.Kidnapper or + CustomRoles.Postponer or CustomRoles.Venerer or CustomRoles.ClockBlocker or CustomRoles.Forger or @@ -1628,6 +1633,7 @@ internal static class CustomRolesHelper CustomRoles.Miner => RoleOptionType.Impostor_Concealing, CustomRoles.Morphling => RoleOptionType.Impostor_Concealing, CustomRoles.Penguin => RoleOptionType.Impostor_Concealing, + CustomRoles.Postponer => RoleOptionType.Impostor_Concealing, CustomRoles.Puppeteer => RoleOptionType.Impostor_Concealing, CustomRoles.RiftMaker => RoleOptionType.Impostor_Concealing, CustomRoles.Scavenger => RoleOptionType.Impostor_Concealing, @@ -1712,9 +1718,10 @@ internal static class CustomRolesHelper CustomRoles.MeetingManager => RoleOptionType.Crewmate_Investigate, CustomRoles.Ignitor => RoleOptionType.Crewmate_Investigate, CustomRoles.Imitator => RoleOptionType.Crewmate_Investigate, + CustomRoles.Inquirer => RoleOptionType.Crewmate_Investigate, + CustomRoles.Inquisitor => RoleOptionType.Crewmate_Investigate, CustomRoles.Insight => RoleOptionType.Crewmate_Investigate, CustomRoles.ParityCop => RoleOptionType.Crewmate_Investigate, - CustomRoles.Inquirer => RoleOptionType.Crewmate_Investigate, CustomRoles.Leery => RoleOptionType.Crewmate_Investigate, CustomRoles.Lighter => RoleOptionType.Crewmate_Investigate, CustomRoles.Lookout => RoleOptionType.Crewmate_Investigate, @@ -1809,6 +1816,7 @@ internal static class CustomRolesHelper CustomRoles.Randomizer => RoleOptionType.Crewmate_Chaos, CustomRoles.ToiletMaster => RoleOptionType.Crewmate_Chaos, CustomRoles.Tornado => RoleOptionType.Crewmate_Chaos, + CustomRoles.Tree => RoleOptionType.Crewmate_Chaos, _ => role.IsImpostor() ? RoleOptionType.Impostor_Miscellaneous : RoleOptionType.Neutral_Benign }; } diff --git a/Modules/FixedUpdateCaller.cs b/Modules/FixedUpdateCaller.cs index 4bf8c6e4a..5104b439c 100644 --- a/Modules/FixedUpdateCaller.cs +++ b/Modules/FixedUpdateCaller.cs @@ -201,6 +201,18 @@ public static class FixedUpdateCaller } } catch (Exception e) { Utils.ThrowException(e); } + + try + { + Main.GameTimer += Time.fixedDeltaTime; + + if (Options.EnableGameTimeLimit.GetBool() && Main.GameTimer > Options.GameTimeLimit.GetInt()) + { + Main.GameTimer = 0f; + CustomWinnerHolder.ResetAndSetWinner(CustomWinner.None); + } + } + catch (Exception e) { Utils.ThrowException(e); } } } catch (Exception e) { Utils.ThrowException(e); } diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index dc8f17863..88a75679b 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -736,6 +736,8 @@ public static class Options public static OptionItem ShowTaskCountWhenAlive; public static OptionItem ShowTaskCountWhenDead; public static OptionItem IntegrateNaturalDisasters; + public static OptionItem EnableGameTimeLimit; + public static OptionItem GameTimeLimit; public static OptionItem ShowDifferentEjectionMessageForSomeRoles; public static OptionItem ShowAntiBlackoutWarning; public static OptionItem AllowConsole; @@ -2942,6 +2944,15 @@ public static class Options .SetColor(new Color32(193, 255, 209, byte.MaxValue)) .RegisterUpdateValueEvent((_, _, _) => GameOptionsMenuPatch.ReloadUI()); + EnableGameTimeLimit = new BooleanOptionItem(24455, "EnableGameTimeLimit", false, TabGroup.GameSettings) + .SetColor(new Color32(193, 255, 209, byte.MaxValue)) + .SetHeader(true); + + GameTimeLimit = new FloatOptionItem(24456, "GameTimeLimit", new(20f, 3600f, 20f), 900f, TabGroup.GameSettings) + .SetColor(new Color32(193, 255, 209, byte.MaxValue)) + .SetParent(EnableGameTimeLimit) + .SetValueFormat(OptionFormat.Seconds); + new TextOptionItem(100029, "MenuTitle.Ghost", TabGroup.GameSettings) .SetGameMode(CustomGameMode.Standard) diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 9a15455eb..93e10c51b 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -30,6 +30,7 @@ using InnerNet; using Newtonsoft.Json; using UnityEngine; using static EHR.Translator; +using Tree = EHR.Crewmate.Tree; namespace EHR; /* @@ -2436,6 +2437,12 @@ public static class Utils sender.RpcSetName(seer, Car.Name); return true; } + + if (Main.PlayerStates.TryGetValue(seer.PlayerId, out var seerState) && seerState.Role is Tree { TreeSpriteActive: true }) + { + sender.RpcSetName(seer, Tree.Sprite); + return true; + } if (forMeeting && Magistrate.CallCourtNextMeeting) { @@ -3230,6 +3237,7 @@ public static class Utils CustomRoles.Gardener => Gardener.AbilityCooldown.GetInt(), CustomRoles.Mayor when Mayor.MayorHasPortableButton.GetBool() => (int)Math.Round(Options.AdjustedDefaultKillCooldown), CustomRoles.Paranoia => (int)Math.Round(Options.AdjustedDefaultKillCooldown), + CustomRoles.Tree => 5 + (includeDuration ? Tree.FallDelay.GetInt() + Tree.FallStunDuration.GetInt() : 0), 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), diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 069b23b9f..0a5065345 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -219,7 +219,8 @@ internal static class ChatCommands new(["vs", "votestart", "голосованиестарт", "投票开始"], "", GetString("CommandDescription.VoteStart"), Command.UsageLevels.Everyone, Command.UsageTimes.InLobby, VoteStartCommand, true, false), new(["imitate", "имитировать", "模仿"], "{id}", GetString("CommandDescription.Imitate"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, ImitateCommand, true, true, [GetString("CommandArgs.Imitate.Id")]), new(["retribute", "воздать", "报复"], "{id}", GetString("CommandDescription.Retribute"), Command.UsageLevels.Everyone, Command.UsageTimes.InMeeting, RetributeCommand, true, true, [GetString("CommandArgs.Retribute.Id")]), - + new(["revive", "воскрешение", "воскрешать", "复活", "reviver"], "{id}", GetString("CommandDescription.Revive"), Command.UsageLevels.Host, Command.UsageTimes.InGame, ReviveCommand, true, false, [GetString("CommandArgs.Revive.Id")]), + new(["confirmauth"], "{uuid}", GetString("CommandDescription.ConfirmAuth"), Command.UsageLevels.Everyone, Command.UsageTimes.Always, ConfirmAuthCommand, true, false, [GetString("CommandArgs.ConfirmAuth.UUID")]), // Commands with action handled elsewhere @@ -482,6 +483,16 @@ internal static class ChatCommands // --------------------------------------------------------------------------------------------------------------------------------------------- + private static void ReviveCommand(PlayerControl player, string text, string[] args) + { + if ((!Options.NoGameEnd.GetBool() && !player.FriendCode.GetDevUser().up) || args.Length < 2 || !byte.TryParse(args[1], out byte targetId)) return; + + PlayerControl target = Utils.GetPlayerById(targetId); + if (target == null) return; + + target.RpcRevive(); + } + private static void ConfirmAuthCommand(PlayerControl player, string text, string[] args) { if (GameStates.CurrentServerType != GameStates.ServerType.Vanilla) diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index 74d5c8dd8..da444fca6 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -1242,6 +1242,8 @@ internal static class IntroCutsceneDestroyPatch LateTask.New(() => { + Main.GameTimer = 0f; + if (SubmergedCompatibility.IsSubmerged()) { foreach (PlayerControl pc in Main.AllAlivePlayerControls) diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index a4d49ff38..4a8bc4887 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -158,6 +158,9 @@ internal static class CheckForEndVotingPatch voteLog.Info($"{voter.GetNameWithRole().RemoveHtmlTags()} Skip for not voting"); break; } + + if (voter.Is(CustomRoles.Compelled)) + TryAddAfterMeetingDeathPlayers(PlayerState.DeathReason.Suicide, ps.TargetPlayerId); } } diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index 2dc1d1c0a..a0ac6a013 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -118,6 +118,7 @@ internal static class EndGamePatch ChatCommands.Spectators.Clear(); Utils.NumSnapToCallsThisRound = 0; + Main.GameTimer = 0f; switch (Options.CurrentGameMode) { diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index de5af2cfb..fd9a99d99 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -22,6 +22,7 @@ using TMPro; using UnityEngine; using static EHR.Translator; using static EHR.Utils; +using Tree = EHR.Crewmate.Tree; namespace EHR; @@ -1784,6 +1785,13 @@ internal static class FixedUpdatePatch return; } + if (Main.PlayerStates.TryGetValue(target.PlayerId, out var targetState) && targetState.Role is Tree { TreeSpriteActive: true }) + { + target.cosmetics.nameText.text = Tree.Sprite; + roleText.enabled = false; + return; + } + Mark.Clear(); Suffix.Clear(); diff --git a/Patches/PlayerJoinAndLeftPatch.cs b/Patches/PlayerJoinAndLeftPatch.cs index ee3c73f75..208b7ef5b 100644 --- a/Patches/PlayerJoinAndLeftPatch.cs +++ b/Patches/PlayerJoinAndLeftPatch.cs @@ -41,6 +41,7 @@ internal static class OnGameJoinedPatch GameStates.InGame = false; ErrorText.Instance?.Clear(); ChatCommands.VotedToStart = []; + Main.GameTimer = 0f; Utils.DirtyName = []; diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 05784cfc6..9183f07f3 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -2018,6 +2018,8 @@ "RomanticArrows": "Romantic has arrows pointing to their Partner", "RomanticPartnerHasArrows": "Partner has arrows pointing to the Romantic", "SpiritualistTargetMessage": "The Spiritualist has an arrow pointing toward you", + "EnableGameTimeLimit": "Enable Game Time Limit (Expired = No One Wins)", + "GameTimeLimit": "Game Time Limit", "Gaslighter": "Gaslighter", "GaslighterInfo": "Make others doubt their own memory", "GaslighterInfoLong": "(Neutrals):\\nAs the Gaslighter, you do different actions through your kill button every round.\\n\\nRound 1: Normal kills\\nRound 2: Knight players\\nRound 3: Curse players (like Witch)\\nRound 4: Shield players\\n\\nThen the cycle repeats, or you stay on normally killing until the end of the game depending on the settings.\\nSettings determine your win condition.", @@ -2487,6 +2489,7 @@ "CommandDescription.Imitate": "Imitate a dead player's role as the Imitator", "CommandDescription.Retribute": "Guess who killed the player you camped as the Retributionist", "CommandDescription.ConfirmAuth": "Command required to register a new EHR account", + "CommandDescription.Revive": "Revives a dead player. Requires special access or No Game End enabled.", "CommandArgs.ChangeSetting.Name": "The setting's name to change (leave blank to view all settings)", "CommandArgs.ChangeSetting.UnknownValue": "(leave blank to view what to put here)", "CommandArgs.Rename.Name": "Your new name", @@ -2579,6 +2582,7 @@ "CommandArgs.Imitate.Id": "The dead player's ID to imitate the role of", "CommandArgs.Retribute.Id": "The potential killer's ID to guess", "CommandArgs.ConfirmAuth.UUID": "The unique token provided by the server", + "CommandArgs.Revive.Id": "The dead player's ID to revive", "Commands.ChangeSettingHelp": "Arguments:\\n- map (theskeld/mirahq/polus/dlekseht/airship/thefungle/custom)\\n- impostors (number)\\n- players (number)\\n- recommended (on/off)\\n- confirmejects (on/off)\\n- emergencymeetings (number)\\n- anonymousvotes (on/off)\\n- emergencycooldown (number)\\n- discussiontime (number)\\n- votingtime (number)\\n- playerspeed (float)\\n- crewmatevision (float)\\n- impostorvision (float)\\n- killcooldown (float)\\n- killdistance (short/medium/long/custom)\\n- taskbarupdates (always/meetings/never)\\n- visualtasks (on/off)\\n- commontasks (number)\\n- longtasks (number)\\n- shorttasks (number)\\n- scientistcount (number)\\n- scientistchance (number)\\n- vitalsdisplaycooldown (float)\\n- batteryduration (float)\\n- engineercount (number)\\n- engineerchance (number)\\n- ventusecooldown (float)\\n- maxtimeinvents (float)\\n- guardianangelcount (number)\\n- guardianangelchance (number)\\n- protectcooldown (float)\\n- protectduration (float)\\n- protectvisibletoimpostors (on/off)\\n- shapeshiftercount (number)\\n- shapeshifterchance (number)\\n- shapeshiftduration (float)\\n- shapeshiftcooldown (float)\\n- leaveshapeshiftevidence (on/off)\\n- phantomcount (number)\\n- phantomchance (number)\\n- inviscooldown (float)\\n- invisduration (float)\\n- noisemakercount (number)\\n- noisemakerchance (number)\\n- noisemakerimpostoralert (on/off)\\n- noisemakeralertduration (number)\\n- trackercount (number)\\n- trackerchance (number)\\n- trackduration (float)\\n- trackcooldown (float)\\n- trackdelay (float)\\n- ghostdotasks (on/off)\\n\\nNotes:\\n1. Upper and lowercase matters\\n2. Where it says custom, you obviously put a number argument after it\\n3. A float is a single precision floating number, like 1.7\\n4. \"ghostdotasks\" causes issues if you set it\\n5. Example uses:\\n - \"/cs map polus\"\\n - \"/cs killdistance custom 4\"\\n - \"/cs trackdelay 1,7\"\\n - \"/cs confirmejects on\"", "AddTagSuccess": "<#00ffa5>✓ Suceessfully created the tag {0} for {1}. If you want to change it, just use this command again and it will override the current tag. To delete it, simply type <#00a5ff>/deletetag {2}!", "DeleteTagSuccess": "<#00ffa5>✓ Successfully deleted {0}'s tag.", @@ -8981,6 +8985,20 @@ "VenomInfo": "Kill to Dissolve Bodies", "VenomInfoLong": "(Impostors):\\nGrants Viper ability for impostor roles.", "Prefix.Venom": "Venom", + "Inquisitor": "Inquisitor", + "InquisitorInfo": "Check Who Knows Whose Role", + "InquisitorInfoLong": "(Crewmates):\\nAs the Inquisitor, you may vote someone in a meeting to see whose role they know.", + "Inquisitor.KnowExactRolesAfterTasksFinished": "Know Exact Roles After Finishing All Tasks", + "Inquisitor.ExcludeDeadPlayers": "Exclude Dead Players' Roles Seen", + "InquisitorVoteResult": "{0} knows the role of:\\n{1}", + "InquisitorNoInfo": "No one, other than themselves", + "Compelled": "Compelled", + "CompelledInfo": "Must Vote Or Suicide", + "CompelledInfoLong": "(Add-Ons):\\nYou must vote in every meeting, or you suicide.", + "Prefix.Compelled": "Compelled", + "Postponer": "Postponer", + "PostponerInfo": "Postpone Dead Body Spawning", + "PostponerInfoLong": "(Impostors):\\nAs the Postponer, when you kill, no dead body spawns instantly. Instead, it will spawn after a delay (in the same place as the murder).", "Clairvoyant": "Clairvoyant", "ClairvoyantInfo": "Know Remaining Killer Counts", "ClairvoyantInfoLong": "(Crewmates):\\nAs the Clairvoyant, you can see the remaining impostor and neutral killer counts by entering a vent or petting.", diff --git a/Roles/AddOns/Compelled.cs b/Roles/AddOns/Compelled.cs new file mode 100644 index 000000000..d6ea1089b --- /dev/null +++ b/Roles/AddOns/Compelled.cs @@ -0,0 +1,11 @@ +namespace EHR.AddOns; + +public class Compelled : IAddon +{ + public AddonTypes Type => AddonTypes.Harmful; + + public void SetupCustomOption() + { + Options.SetupAdtRoleOptions(654980, CustomRoles.Compelled, canSetNum: true, teamSpawnOptions: true); + } +} \ No newline at end of file diff --git a/Roles/Crewmate/Inquisitor.cs b/Roles/Crewmate/Inquisitor.cs new file mode 100644 index 000000000..b37ce6523 --- /dev/null +++ b/Roles/Crewmate/Inquisitor.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using EHR.Neutral; + +namespace EHR.Crewmate; + +public class Inquisitor : RoleBase +{ + public static bool On; + + public override bool IsEnable => On; + + private static OptionItem KnowExactRolesAfterTasksFinished; + private static OptionItem ExcludeDeadPlayers; + private static OptionItem AbilityUseLimit; + private static OptionItem AbilityUseGainWithEachTaskCompleted; + private static OptionItem AbilityChargesWhenFinishedTasks; + + public override void SetupCustomOption() + { + StartSetup(654900) + .AutoSetupOption(ref KnowExactRolesAfterTasksFinished, true) + .AutoSetupOption(ref ExcludeDeadPlayers, false) + .AutoSetupOption(ref AbilityUseLimit, 1f, new FloatValueRule(0, 20, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityUseGainWithEachTaskCompleted, 0.5f, new FloatValueRule(0f, 5f, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityChargesWhenFinishedTasks, 0.2f, new FloatValueRule(0f, 5f, 0.05f), OptionFormat.Times); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + playerId.SetAbilityUseLimit(AbilityUseLimit.GetFloat()); + } + + public override bool OnVote(PlayerControl voter, PlayerControl target) + { + if (Starspawn.IsDayBreak) return false; + if (voter == null || target == null || voter.PlayerId == target.PlayerId || Main.DontCancelVoteList.Contains(voter.PlayerId)) return false; + + PlayerControl[] players = ExcludeDeadPlayers.GetBool() ? Main.AllAlivePlayerControls : Main.AllPlayerControls; + List<(byte Id, CustomRoles Role)> knownRoles = Main.PlayerStates.Values.SelectMany(s => from pc in players where s.Role.KnowRole(target, pc) select (pc.PlayerId, pc.GetCustomRole())).DistinctBy(x => x.PlayerId).ToList(); + + string result; + + if (knownRoles.Count == 0) + result = Translator.GetString("InquisitorNoInfo"); + else if (KnowExactRolesAfterTasksFinished.GetBool() && voter.GetTaskState().IsTaskFinished) + result = string.Join('\n', knownRoles.Select(x => $"{x.Id.ColoredPlayerName()}: {x.Role.ToColoredString()}")); + else + result = string.Join(", ", knownRoles.Select(x => x.Id.ColoredPlayerName())); + + Utils.SendMessage("\n", voter.PlayerId, string.Format(Translator.GetString("InquisitorVoteResult"), target.PlayerId.ColoredPlayerName(), result)); + + voter.RpcRemoveAbilityUse(); + Main.DontCancelVoteList.Add(voter.PlayerId); + return true; + } + + public override void OnMeetingShapeshift(PlayerControl shapeshifter, PlayerControl target) + { + OnVote(shapeshifter, target); + } +} \ No newline at end of file diff --git a/Roles/Crewmate/Tree.cs b/Roles/Crewmate/Tree.cs new file mode 100644 index 000000000..281fe69aa --- /dev/null +++ b/Roles/Crewmate/Tree.cs @@ -0,0 +1,95 @@ +using AmongUs.GameOptions; + +namespace EHR.Crewmate; + +public class Tree : RoleBase +{ + public static bool On; + + public const string Sprite = "██<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>███
█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>██
<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
███<#711e1e>█<#711e1e>████
███<#711e1e>█<#711e1e>████
███<#711e1e>█<#711e1e>████
"; + public const string FallenSprite = "████████
████████
████████
████<#6ef514>█<#6ef514>███
████<#6ef514>█<#6ef514>█<#6ef514>██
████<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
<#6f1515>█<#6f1515>█<#6f1515>█<#6f1515>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
<#6f1515>█<#6f1515>█<#6f1515>█<#6f1515>█<#6ef514>█<#6ef514>█<#6ef514>█<#6ef514>█
"; + + public override bool IsEnable => On; + + private static OptionItem TreeSpriteVisible; + public static OptionItem FallDelay; + private static OptionItem AbilityUseLimit; + private static OptionItem AbilityUseGainWithEachTaskCompleted; + private static OptionItem AbilityChargesWhenFinishedTasks; + private static OptionItem FallRadius; + private static OptionItem FallKillChance; + public static OptionItem FallStunDuration; + + public bool TreeSpriteActive; + + public override void SetupCustomOption() + { + StartSetup(655100) + .AutoSetupOption(ref TreeSpriteVisible, true) + .AutoSetupOption(ref FallDelay, 5, new IntegerValueRule(1, 60, 1), OptionFormat.Seconds) + .AutoSetupOption(ref AbilityUseLimit, 1f, new FloatValueRule(0, 20, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityUseGainWithEachTaskCompleted, 0.5f, new FloatValueRule(0f, 5f, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityChargesWhenFinishedTasks, 0.2f, new FloatValueRule(0f, 5f, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref FallRadius, 3f, new FloatValueRule(0.25f, 10f, 0.25f), OptionFormat.Multiplier) + .AutoSetupOption(ref FallKillChance, 10, new IntegerValueRule(0, 100, 5), OptionFormat.Percent) + .AutoSetupOption(ref FallStunDuration, 5, new IntegerValueRule(1, 60, 1), OptionFormat.Seconds); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + playerId.SetAbilityUseLimit(AbilityUseLimit.GetFloat()); + TreeSpriteActive = false; + } + + public override void OnPet(PlayerControl pc) + { + if (pc.GetAbilityUseLimit() < 1) return; + pc.RpcRemoveAbilityUse(); + + Main.AllPlayerSpeed[pc.PlayerId] = Main.MinSpeed; + pc.MarkDirtySettings(); + + bool treeSpriteVisible = TreeSpriteVisible.GetBool(); + if (treeSpriteVisible) TreeSpriteActive = true; + else pc.RpcMakeInvisible(); + + LateTask.New(() => + { + if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || pc == null || !pc.IsAlive()) return; + + Main.AllPlayerSpeed[pc.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); + pc.MarkDirtySettings(); + + if (treeSpriteVisible) TreeSpriteActive = false; + else pc.RpcMakeVisible(); + + int chance = FallKillChance.GetInt(); + + Utils.GetPlayersInRadius(FallRadius.GetFloat(), pc.Pos()).Without(pc).Do(x => + { + if (IRandom.Instance.Next(100) < chance) x.Suicide(PlayerState.DeathReason.Fall, pc); + else + { + Main.AllPlayerSpeed[x.PlayerId] = Main.MinSpeed; + x.MarkDirtySettings(); + + LateTask.New(() => + { + if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || pc == null || !pc.IsAlive()) return; + Main.AllPlayerSpeed[x.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); + x.MarkDirtySettings(); + }, FallStunDuration.GetFloat(), log: false); + } + }); + + var cno = new FallenTree(pc.Pos()); + LateTask.New(() => cno.Despawn(), FallStunDuration.GetFloat(), log: false); + }, FallDelay.GetFloat(), log: false); + } +} \ No newline at end of file diff --git a/Roles/Impostor/AntiAdminer.cs b/Roles/Impostor/AntiAdminer.cs index c358acce8..4a09dcdcc 100644 --- a/Roles/Impostor/AntiAdminer.cs +++ b/Roles/Impostor/AntiAdminer.cs @@ -76,7 +76,7 @@ internal class AntiAdminer : RoleBase IsMonitor = Main.PlayerStates[playerId].MainRole == CustomRoles.Monitor; ExtraAbilityStartTimeStamp = 0; AntiAdminerId = playerId; - if (IsMonitor && Main.CurrentMap != MapNames.MiraHQ) playerId.SetAbilityUseLimit(Monitor.UseLimitOpt.GetInt()); + if (IsMonitor && Main.CurrentMap != MapNames.MiraHQ) playerId.SetAbilityUseLimit(Monitor.UseLimitOpt.GetFloat()); } public override void Remove(byte playerId) diff --git a/Roles/Impostor/Postponer.cs b/Roles/Impostor/Postponer.cs new file mode 100644 index 000000000..fcc01890e --- /dev/null +++ b/Roles/Impostor/Postponer.cs @@ -0,0 +1,63 @@ +using EHR.Modules; +using UnityEngine; + +namespace EHR.Impostor; + +public class Postponer : RoleBase +{ + public static bool On; + + public override bool IsEnable => On; + + private static OptionItem KillCooldown; + private static OptionItem BodyDelay; + + public override void SetupCustomOption() + { + StartSetup(655000) + .AutoSetupOption(ref KillCooldown, 30f, new FloatValueRule(0.5f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref BodyDelay, 10, new IntegerValueRule(1, 120, 1), OptionFormat.Seconds); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + } + + public override void SetKillCooldown(byte id) + { + Main.AllPlayerKillCooldown[id] = KillCooldown.GetFloat(); + } + + public override bool OnCheckMurder(PlayerControl killer, PlayerControl target) + { + if (!base.OnCheckMurder(killer, target) || !killer.RpcCheckAndMurder(target, check: true)) return false; + + RPC.PlaySoundRPC(killer.PlayerId, Sounds.KillSound); + killer.RpcRemoveAbilityUse(); + target.Data.IsDead = true; + target.SetRealKiller(killer); + Main.PlayerStates[target.PlayerId].deathReason = PlayerState.DeathReason.Kill; + target.RpcExileV2(); + Main.PlayerStates[target.PlayerId].SetDead(); + Utils.AfterPlayerDeathTasks(target); + target.SetRealKiller(killer); + killer.SetKillCooldown(KillCooldown.GetFloat()); + + Vector2 position = target.Pos(); + byte colorId = (byte)target.Data.DefaultOutfit.ColorId; + + LateTask.New(() => + { + if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks) return; + Utils.RpcCreateDeadBody(position, colorId, target); + }, BodyDelay.GetInt(), "Postponer Body Delay"); + + return false; + } +} \ No newline at end of file