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