diff --git a/CustomRoles.cs b/CustomRoles.cs index c46593994..add07710e 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -180,6 +180,7 @@ public enum CustomRoles Express, Farseer, Divinator, // Fortune Teller + Gardener, Gaulois, Goose, Grappler, diff --git a/Main.cs b/Main.cs index 5f58e3d31..58150e208 100644 --- a/Main.cs +++ b/Main.cs @@ -407,6 +407,7 @@ public class Main : BasePlugin { CustomRoles.Mathematician, "#eb3474" }, { CustomRoles.Helper, "#fcf1bd" }, { CustomRoles.Astral, "#b329d6" }, + { CustomRoles.Gardener, "#00ff00" }, { CustomRoles.Transmitter, "#c9a11e" }, { CustomRoles.Imitator, "#c99e28" }, { CustomRoles.PortalMaker, "#700078" }, diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs index 5ef9e5ebd..4b69252f9 100644 --- a/Modules/CustomNetObject.cs +++ b/Modules/CustomNetObject.cs @@ -639,6 +639,24 @@ namespace EHR CreateNetObject("█<#2b006b>█<#2b006b>█<#2b006b>█<#2b006b>██
█<#2b006b>█<#fa69ff>█<#fa69ff>█<#2b006b>██
█<#2b006b>█<#fa69ff>█<#fa69ff>█<#2b006b>██
█<#2b006b>█<#fa69ff>█<#fa69ff>█<#2b006b>██
█<#2b006b>█<#fa69ff>█<#fa69ff>█<#2b006b>██
█<#2b006b>█<#2b006b>█<#2b006b>█<#2b006b>██
", position); } } + + internal sealed class Plant : CustomNetObject + { + public bool Spawned; + + public Plant(Vector2 position) + { + Position = position; + Spawned = false; + } + + public void SpawnIfNotSpawned() + { + if (Spawned) return; + CreateNetObject("█<#00ff15>██<#00ff15>██<#00ff15>█
█<#00ff15>██<#00ff15>██<#00ff15>█
<#00ff15>█<#00ff15>██<#00ff15>██<#00ff15>█
<#00ff15>███<#00ff15>██<#00ff15>█
<#00ff15>██<#00ff15>█<#00ff15>██<#00ff15>█
<#00ff15>██<#00ff15>███<#00ff15>█
", Position); + Spawned = true; + } + } } [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RawSetName))] diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index ab05a7567..7f49a7b8b 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -37,6 +37,7 @@ internal static class CustomRolesHelper CustomRoles.Amogus, CustomRoles.Wiper, CustomRoles.PortalMaker, + CustomRoles.Gardener, // Add-ons CustomRoles.Energetic, @@ -227,6 +228,7 @@ internal static class CustomRolesHelper CustomRoles.MeetingManager => CustomRoles.Crewmate, CustomRoles.Bane => CustomRoles.Crewmate, CustomRoles.Transmitter => CustomRoles.Crewmate, + CustomRoles.Gardener => CustomRoles.Crewmate, CustomRoles.Imitator => CustomRoles.Crewmate, CustomRoles.PortalMaker => CustomRoles.Crewmate, CustomRoles.Astral => UsePets ? CustomRoles.Crewmate : CustomRoles.Engineer, @@ -1722,6 +1724,7 @@ internal static class CustomRolesHelper CustomRoles.DovesOfNeace => RoleOptionType.Crewmate_Support, CustomRoles.Electric => RoleOptionType.Crewmate_Support, CustomRoles.Escort => RoleOptionType.Crewmate_Support, + CustomRoles.Gardener => RoleOptionType.Crewmate_Support, CustomRoles.Gaulois => RoleOptionType.Crewmate_Support, CustomRoles.Grappler => RoleOptionType.Crewmate_Support, CustomRoles.Grenadier => RoleOptionType.Crewmate_Support, diff --git a/Modules/Statistics.cs b/Modules/Statistics.cs index 634e8060e..5ab6a678b 100644 --- a/Modules/Statistics.cs +++ b/Modules/Statistics.cs @@ -36,7 +36,7 @@ public static class Statistics CustomGameMode gm = Options.CurrentGameMode; - if (gm != CustomGameMode.Standard && GameStates.CurrentServerType is not GameStates.ServerType.Modded and not GameStates.ServerType.Niko) + if (GameStates.CurrentServerType is not GameStates.ServerType.Modded and not GameStates.ServerType.Niko) { try { diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 43e9e37f9..b365bfb2b 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -3183,6 +3183,7 @@ public static class Utils CustomRoles.Mole => Mole.CD.GetInt(), CustomRoles.Monitor => Monitor.VentCooldown.GetInt(), CustomRoles.Tether => Tether.VentCooldown.GetInt(), + CustomRoles.Gardener => Gardener.AbilityCooldown.GetInt(), CustomRoles.Mayor when Mayor.MayorHasPortableButton.GetBool() => (int)Math.Round(Options.AdjustedDefaultKillCooldown), CustomRoles.Paranoia => (int)Math.Round(Options.AdjustedDefaultKillCooldown), CustomRoles.Grenadier => Options.GrenadierSkillCooldown.GetInt() + (includeDuration ? Options.GrenadierSkillDuration.GetInt() : 0), diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index ab992af8d..3c8a563d9 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -474,6 +474,12 @@ internal static class CheckMurderPatch return false; } + if (!Gardener.OnAnyoneCheckMurder(killer, target)) + { + Notify("GardenerPlantNearby"); + return false; + } + if (!Socialite.OnAnyoneCheckMurder(killer, target)) { Notify("SocialiteTarget"); diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 8744505d2..5290c865e 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -1845,6 +1845,7 @@ "CovenKillEachOther": "As a Coven member, you cannot (and shouldn't) kill other Coven members", "CovenKillEntranced": "Coven members cannot kill Entranced players", "EntrancedKillCoven": "As an Entranced player, you cannot kill Coven members", + "GardenerPlantNearby": "You cannot kill near plants", "SomeSortOfProtection": "This player is shielded due to a role-specific action", "HexButtonText": "Hex", "EscapeeAbilityButtonText": "TP", @@ -9228,6 +9229,12 @@ "DazzlerResetDazzledVisionOnDeath": "Reset vision of dazzled players on death/eject", "DazzleCooldown": "Dazzle Cooldown", "DazzleButtonText": "Dazzle", + "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.", + "Gardener.PlantRange": "Plants' Kill Blocking Range", + "Gardener.PlantAlreadyExistsNearby": "<#ff0000>Can't place a new plant here, one is already nearby!", + "Gardener.PlantCreated": "<#00ff00>Plant created!\\nIt will be visible after the next meeting.", "Pawn": "Pawn", "PawnInfo": "Complete Tasks Then Pick Your Role", "PawnInfoLong": "(Neutrals):\\nAs the Pawn, you must complete all of your tasks, then use /choose {role} to pick your role.\\n\\n(Replace {role} with the name of the role that you want to become.) (You can only choose roles that are enabled.)\\nYou lose if you have this role when the game ends.", diff --git a/Roles/Crewmate/Gardener.cs b/Roles/Crewmate/Gardener.cs new file mode 100644 index 000000000..bcd02bcba --- /dev/null +++ b/Roles/Crewmate/Gardener.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; + +namespace EHR.Crewmate; + +public class Gardener : RoleBase +{ + public static bool On; + + public override bool IsEnable => On; + + public static OptionItem AbilityCooldown; + private static OptionItem PlantRange; + private static OptionItem AbilityUseLimit; + private static OptionItem AbilityUseGainWithEachTaskCompleted; + private static OptionItem AbilityChargesWhenFinishedTasks; + + private static List Plants = []; + + public override void SetupCustomOption() + { + StartSetup(653400) + .AutoSetupOption(ref AbilityCooldown, 30, new IntegerValueRule(0, 120, 1), OptionFormat.Seconds) + .AutoSetupOption(ref PlantRange, 2.5f, new FloatValueRule(0.1f, 10f, 0.1f), OptionFormat.Multiplier) + .AutoSetupOption(ref AbilityUseLimit, 1f, new FloatValueRule(0, 20, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityUseGainWithEachTaskCompleted, 0.3f, new FloatValueRule(0f, 5f, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityChargesWhenFinishedTasks, 0.05f, new FloatValueRule(0f, 5f, 0.05f), OptionFormat.Times); + } + + public override void Init() + { + On = false; + Plants = []; + } + + public override void Add(byte playerId) + { + On = true; + playerId.SetAbilityUseLimit(AbilityUseLimit.GetFloat()); + } + + public override void OnPet(PlayerControl pc) + { + if (pc.GetAbilityUseLimit() < 1f) return; + + Vector2 pos = pc.Pos(); + float range = PlantRange.GetFloat() * 2f; + + if (Plants.Exists(x => Vector2.Distance(x.Position, pos) <= range)) + { + pc.Notify(Translator.GetString("Gardener.PlantAlreadyExistsNearby")); + return; + } + + Plants.Add(new Plant(pos)); + pc.Notify(Translator.GetString("Gardener.PlantCreated")); + pc.RpcRemoveAbilityUse(); + } + + public override void AfterMeetingTasks() + { + Plants.ForEach(x => x.SpawnIfNotSpawned()); + } + + public static bool OnAnyoneCheckMurder(PlayerControl killer, PlayerControl target) + { + if (!On || Plants.Count == 0) return true; + + float range = PlantRange.GetFloat(); + List positions = [killer.Pos(), target.Pos()]; + + if (Plants.FindFirst(x => x.Spawned && positions.Exists(p => Vector2.Distance(p, x.Position) <= range), out Plant plant)) + { + plant.Despawn(); + Plants.Remove(plant); + killer.SetKillCooldown(); + return false; + } + + return true; + } +} \ No newline at end of file