New Role: Farmer (Crewmate Support)

This commit is contained in:
Gurge44 committed 2025-09-05 13:09:04 +02:00
1 parent 35d32584c0
commit c86a5db692
15 files changed
+250 -6

No files matched your search

+5
View File
@@ -84,6 +84,7 @@ public enum CustomRoles
Overheat,
Parasite,
Penguin,
Psychopath,
Puppeteer,
QuickShooter,
Refugee,
@@ -178,6 +179,7 @@ public enum CustomRoles
Enigma,
Escort,
Express,
Farmer,
Farseer,
Divinator, // Fortune Teller
Gardener,
@@ -288,6 +290,7 @@ public enum CustomRoles
Demon,
Doppelganger,
Doomsayer,
Duality,
Eclipse,
Enderman,
Evolver,
@@ -507,6 +510,7 @@ public enum CustomRoles
Knighted,
LastImpostor,
Lazy,
Listener,
Lovers,
Loyal,
Lucky,
@@ -545,6 +549,7 @@ public enum CustomRoles
Brakar, // Tiebreaker
Torch,
Truant,
Unbound,
Undead,
Underdog,
Unlucky,
+1
View File
@@ -179,6 +179,7 @@ public enum CustomRoles
Enigma,
Escort,
Express,
Farmer,
Farseer,
Divinator, // Fortune Teller
Gardener,
+1 -1
View File
@@ -8,7 +8,7 @@
<Description>EHR</Description>
<Authors>Gurge44</Authors>
<langVersion>preview</langVersion>
<AmongUs>D:\AmongUs</AmongUs>
<AmongUs>C:\Program Files\Epic Games\AmongUs</AmongUs>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Optimize>True</Optimize>
<AssemblyName>$(MSBuildProjectName)</AssemblyName>
+1
View File
@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using AmongUs.GameOptions;
using EHR.Modules;
using EHR.Neutral;
using HarmonyLib;
using UnityEngine;
+1
View File
@@ -411,6 +411,7 @@ public class Main : BasePlugin
{ CustomRoles.Helper, "#fcf1bd" },
{ CustomRoles.Astral, "#b329d6" },
{ CustomRoles.Gardener, "#00ff00" },
{ CustomRoles.Farmer, "#FFDE59" },
{ CustomRoles.Transmitter, "#c9a11e" },
{ CustomRoles.Imitator, "#c99e28" },
{ CustomRoles.PortalMaker, "#700078" },
+20
View File
@@ -660,6 +660,26 @@ namespace EHR
}
}
internal sealed class Seed : CustomNetObject
{
public bool Spawned;
private readonly string Color;
public Seed(Vector2 position, string color)
{
Position = position;
Spawned = false;
Color = color;
}
public void SpawnIfNotSpawned()
{
if (Spawned) return;
CreateNetObject($"<size=100%><line-height=67%><alpha=#00>█<#{Color}>█<alpha=#00>█<#{Color}>█<alpha=#00>█<#{Color}>█<br><alpha=#00>█<#{Color}>█<alpha=#00>█<#{Color}>█<alpha=#00>█<#{Color}>█<br><#{Color}>█<#{Color}>█<alpha=#00>█<#{Color}>█<alpha=#00>█<#{Color}>█<br><#{Color}>█<alpha=#00>█<alpha=#00>█<#{Color}>█<alpha=#00>█<#{Color}>█<br><#{Color}>█<alpha=#00>█<#{Color}>█<#{Color}>█<alpha=#00>█<#{Color}>█<br><#{Color}>█<alpha=#00>█<#{Color}>█<alpha=#00>█<alpha=#00>█<#{Color}>█<br></line-height></size>", Position);
Spawned = true;
}
}
internal sealed class ShapeshiftMenuElement : CustomNetObject
{
private readonly byte VisibleTo;
+5 -3
View File
@@ -38,6 +38,7 @@ internal static class CustomRolesHelper
CustomRoles.Wiper,
CustomRoles.PortalMaker,
CustomRoles.Gardener,
CustomRoles.Farmer,
// Add-ons
CustomRoles.Energetic,
@@ -228,6 +229,7 @@ internal static class CustomRolesHelper
CustomRoles.Benefactor => CustomRoles.Crewmate,
CustomRoles.MeetingManager => CustomRoles.Crewmate,
CustomRoles.Bane => CustomRoles.Crewmate,
CustomRoles.Farmer => CustomRoles.Crewmate,
CustomRoles.Transmitter => CustomRoles.Crewmate,
CustomRoles.Gardener => CustomRoles.Crewmate,
CustomRoles.Imitator => CustomRoles.Crewmate,
@@ -573,12 +575,12 @@ internal static class CustomRolesHelper
public static bool IsNonNK(this CustomRoles role, bool check = false)
{
return (!check && role == CustomRoles.Arsonist && CanCheck && Options.IsLoaded && Options.ArsonistCanIgniteAnytime != null && !Options.ArsonistCanIgniteAnytime.GetBool()) || role.GetNeutralRoleCategory() is RoleOptionType.Neutral_Benign or RoleOptionType.Neutral_Evil or RoleOptionType.Neutral_Pariah;
return (!check && role == CustomRoles.Arsonist && CanCheck && Options.IsLoaded && Options.ArsonistKeepsGameGoing != null && !Options.ArsonistKeepsGameGoing.GetBool()) || role.GetNeutralRoleCategory() is RoleOptionType.Neutral_Benign or RoleOptionType.Neutral_Evil or RoleOptionType.Neutral_Pariah;
}
public static bool IsNK(this CustomRoles role, bool check = false)
{
return (role == CustomRoles.Arsonist && (check || !CanCheck || !Options.IsLoaded || Options.ArsonistCanIgniteAnytime == null || Options.ArsonistCanIgniteAnytime.GetBool())) || role is
return (role == CustomRoles.Arsonist && (check || !CanCheck || !Options.IsLoaded || Options.ArsonistKeepsGameGoing == null || Options.ArsonistKeepsGameGoing.GetBool())) || role is
CustomRoles.Jackal or
CustomRoles.Glitch or
CustomRoles.Sidekick or
@@ -1358,7 +1360,6 @@ internal static class CustomRolesHelper
CustomRoles.Refugee => CountTypes.Impostor,
CustomRoles.Gaslighter => Gaslighter.WinCondition.GetValue() == 2 ? CountTypes.Gaslighter : CountTypes.Crew,
CustomRoles.DarkHide when DarkHide.SnatchesWin.GetBool() => CountTypes.Crew,
CustomRoles.Arsonist when !Options.ArsonistKeepsGameGoing.GetBool() => CountTypes.Crew,
CustomRoles.SchrodingersCat => SchrodingersCat.WinsWithCrewIfNotAttacked.GetBool() ? CountTypes.Crew : CountTypes.OutOfGame,
CustomRoles.DarkHide => !DarkHide.SnatchesWin.GetBool() ? CountTypes.DarkHide : CountTypes.Crew,
CustomRoles.Arsonist => Options.ArsonistKeepsGameGoing.GetBool() ? CountTypes.Arsonist : CountTypes.Crew,
@@ -1732,6 +1733,7 @@ internal static class CustomRolesHelper
CustomRoles.DovesOfNeace => RoleOptionType.Crewmate_Support,
CustomRoles.Electric => RoleOptionType.Crewmate_Support,
CustomRoles.Escort => RoleOptionType.Crewmate_Support,
CustomRoles.Farmer => RoleOptionType.Crewmate_Support,
CustomRoles.Gardener => RoleOptionType.Crewmate_Support,
CustomRoles.Gaulois => RoleOptionType.Crewmate_Support,
CustomRoles.Grappler => RoleOptionType.Crewmate_Support,
+1 -1
View File
@@ -346,7 +346,7 @@ internal static class EAC
return true;
}
if ((MeetingHud.Instance && MeetingHud.Instance.state != MeetingHud.VoteStates.Animating) || ExileController.Instance)
if ((MeetingHud.Instance && MeetingHud.Instance.state != MeetingHud.VoteStates.Animating && !ReportDeadBodyPatch.MeetingStarted) || ExileController.Instance)
{
WarnHost();
Report(pc, "Doing task during meeting");
@@ -305,6 +305,7 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS
break;
}
Farmer.OnAnyoneApplyGameOptions(opt, player);
Siren.ApplyGameOptionsForOthers(opt, player.PlayerId);
Chef.ApplyGameOptionsForOthers(opt, player.PlayerId);
President.OnAnyoneApplyGameOptions(opt);
+1
View File
@@ -3216,6 +3216,7 @@ public static class Utils
int cd = role switch
{
CustomRoles.Farmer => 2,
CustomRoles.PortalMaker => 5,
CustomRoles.Mole => Mole.CD.GetInt(),
CustomRoles.Monitor => Monitor.VentCooldown.GetInt(),
+1
View File
@@ -466,6 +466,7 @@ internal static class CheckMurderPatch
Randomizer.IsShielded(target) ||
Aid.ShieldedPlayers.ContainsKey(target.PlayerId) ||
Gaslighter.IsShielded(target) ||
!Farmer.OnAnyoneCheckMurder(target) ||
!PotionMaster.OnAnyoneCheckMurder(target) ||
!Grappler.OnAnyoneCheckMurder(target) ||
!Adventurer.OnAnyoneCheckMurder(target) ||
+14
View File
@@ -9481,6 +9481,20 @@
"Duality.MustKill": "<#ffff00>Must kill within </color>{0}s",
"Duality.MustDoTask": "<#ffff00>Must complete a task within </color>{0}s",
"Duality.Time": "Time to do a task or kill",
"Farmer": "Farmer",
"FarmerInfo": "Plant Seeds & Sell Them For Buffs",
"FarmerInfoLong": "(Crewmates):\\nAs the Farmer, each task you complete gives you a new seed. You can plant it anywhere by petting. After the next meeting, every planted seed will be grown. Pet near one to harvest and sell it. Each seed gives you or the crew different buffs.<size=75%>\\n\\n- Wheat: Increased vision for all crewmates\\n- Carrot: Shield for all crewmates\\n- Apple: Invisibility (for you)\\n- Tomato: Speed boost (for you)\\n- Potato: Reset all killers' cooldowns\\n- Blueberry: Arrow to all dead bodies (for you)</size>",
"Farmer.HarvestRange": "Harvest Range",
"Farmer.InvisibilityDuration": "Invisibility Duration",
"Farmer.SelectedSeed": "<{0}>Pet</color> to plant {1} seed",
"Farmer.Seed.Wheat": "Wheat",
"Farmer.Seed.Carrot": "Carrot",
"Farmer.Seed.Apple": "Apple",
"Farmer.Seed.Tomato": "Tomato",
"Farmer.Seed.Potato": "Potato",
"Farmer.Seed.Blueberry": "Blueberry",
"Farmer.CompleteTaskToGetSeed": "<size=80%><{0}>Complete a task to get a seed</color></size>",
"Farmer.MoreSeeds": "<size=80%>{0} more seeds in store</size>",
"Astral": "Astral",
"AstralInfo": "Become a Ghost Temporarily",
"AstralInfoLong": "(Crewmates):\\nAs the Astral, you can vent/pet to become a ghost for a short time. After that, you will be revived.",
+195
View File
@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AmongUs.GameOptions;
namespace EHR.Crewmate;
public class Farmer : RoleBase
{
public static bool On;
private static List<Farmer> Instances = [];
public override bool IsEnable => On;
private List<Seed> Seeds;
private List<(Seed Seed, EHR.Seed NetObject)> SeedPositions;
private Dictionary<Seed, long> ActiveSeeds;
private byte FarmerId;
private static OptionItem HarvestRange;
private static OptionItem IncreasedVision;
private static OptionItem IncreasedVisionDuration;
private static OptionItem IncreasedSpeed;
private static OptionItem IncreasedSpeedDuration;
private static OptionItem ShieldDuration;
private static OptionItem InvisibilityDuration;
public override void SetupCustomOption()
{
StartSetup(654000)
.AutoSetupOption(ref HarvestRange, 1.5f, new FloatValueRule(0.25f, 5f, 0.25f), OptionFormat.Multiplier)
.AutoSetupOption(ref IncreasedVision, 1f, new FloatValueRule(0.1f, 2f, 0.1f), OptionFormat.Multiplier)
.AutoSetupOption(ref IncreasedVisionDuration, 15, new IntegerValueRule(1, 60, 1), OptionFormat.Seconds)
.AutoSetupOption(ref IncreasedSpeed, 2f, new FloatValueRule(0.25f, 3f, 0.25f), OptionFormat.Multiplier)
.AutoSetupOption(ref IncreasedSpeedDuration, 30, new IntegerValueRule(1, 60, 1), OptionFormat.Seconds)
.AutoSetupOption(ref ShieldDuration, 15, new IntegerValueRule(1, 60, 1), OptionFormat.Seconds)
.AutoSetupOption(ref InvisibilityDuration, 30, new IntegerValueRule(1, 60, 1), OptionFormat.Seconds);
}
public override void Init()
{
On = false;
Instances = [];
}
public override void Add(byte playerId)
{
On = true;
Seeds = [];
SeedPositions = [];
ActiveSeeds = [];
FarmerId = playerId;
Instances.Add(this);
}
public override void Remove(byte playerId)
{
Instances.Remove(this);
}
public override void OnTaskComplete(PlayerControl pc, int completedTaskCount, int totalTaskCount)
{
Seeds.Add(Enum.GetValues<Seed>().RandomElement());
}
public override void OnPet(PlayerControl pc)
{
Vector2 pos = pc.Pos();
float range = HarvestRange.GetFloat();
if (SeedPositions.FindFirst(x => x.NetObject.Spawned && Vector2.Distance(pos, x.NetObject.Position) < range, out var existing))
{
switch (existing.Seed)
{
case Seed.Wheat:
Utils.MarkEveryoneDirtySettings();
goto default;
case Seed.Apple:
pc.RpcMakeInvisible();
goto default;
case Seed.Tomato:
Main.AllPlayerSpeed[pc.PlayerId] = IncreasedSpeed.GetFloat();
pc.MarkDirtySettings();
goto default;
case Seed.Potato:
Main.AllAlivePlayerControls.DoIf(x => !x.Is(Team.Crewmate), x => x.SetKillCooldown(Main.AllPlayerKillCooldown.GetValueOrDefault(x.PlayerId, -1f)));
break;
case Seed.Blueberry:
Object.FindObjectsOfType<DeadBody>().Do(x => LocateArrow.Add(pc.PlayerId, x.TruePosition));
Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc);
break;
default:
ActiveSeeds[existing.Seed] = Utils.TimeStamp + existing.Seed switch
{
Seed.Wheat => IncreasedVisionDuration.GetInt(),
Seed.Carrot => ShieldDuration.GetInt(),
Seed.Apple => InvisibilityDuration.GetInt(),
Seed.Tomato => IncreasedSpeedDuration.GetInt(),
_ => 0
};
break;
}
existing.NetObject.Despawn();
}
if (Seeds.Count == 0) return;
Seed seed = Seeds[0];
SeedPositions.Add((seed, new(pos, GetHexColor(seed))));
Seeds.RemoveAt(0);
Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc);
}
private static string GetHexColor(Seed s) => s switch
{
Seed.Wheat => "FFFF00",
Seed.Carrot => "FFA500",
Seed.Apple => "FF0000",
Seed.Tomato => "FF6347",
Seed.Potato => "D2B48C",
Seed.Blueberry => "0000FF",
_ => "FFFFFF"
};
public override void OnFixedUpdate(PlayerControl pc)
{
if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks) return;
Seed[] toRemove = ActiveSeeds.Where(x => x.Value <= Utils.TimeStamp).Select(x => x.Key).ToArray();
foreach (Seed seed in toRemove)
{
ActiveSeeds.Remove(seed);
switch (seed)
{
case Seed.Wheat:
Utils.MarkEveryoneDirtySettings();
break;
case Seed.Apple:
pc.RpcMakeVisible();
break;
case Seed.Tomato:
Main.AllPlayerSpeed[pc.PlayerId] = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod);
pc.MarkDirtySettings();
break;
}
}
}
public override void AfterMeetingTasks()
{
SeedPositions.ForEach(x => x.NetObject.SpawnIfNotSpawned());
}
public override void OnReportDeadBody()
{
LocateArrow.RemoveAllTarget(FarmerId);
}
public static void OnAnyoneApplyGameOptions(IGameOptions opt, PlayerControl player)
{
if (On && player.Is(Team.Crewmate) && Instances.Exists(x => x.ActiveSeeds.ContainsKey(Seed.Wheat)))
{
opt.SetFloat(FloatOptionNames.CrewLightMod, IncreasedVision.GetFloat());
opt.SetFloat(FloatOptionNames.ImpostorLightMod, IncreasedVision.GetFloat());
}
}
public static bool OnAnyoneCheckMurder(PlayerControl target)
{
return !On || !target.Is(Team.Crewmate) || !Instances.Exists(x => x.ActiveSeeds.ContainsKey(Seed.Carrot));
}
enum Seed
{
Wheat, // Increased vision for all crewmates
Carrot, // Shield for all crewmates
Apple, // Invisibility (self only)
Tomato, // Speed boost (self only)
Potato, // Reset all killers' cooldowns
Blueberry // Arrow to all dead bodies
}
public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false)
{
if (seer.PlayerId != FarmerId || seer.PlayerId != target.PlayerId || (seer.IsModdedClient() && !hud) || meeting) return string.Empty;
StringBuilder sb = new();
sb.AppendLine(Seeds.Count > 0 ? string.Format(Translator.GetString("Farmer.SelectedSeed"), Main.RoleColors[CustomRoles.Farmer], $"<#{GetHexColor(Seeds[0])}>{Translator.GetString($"Farmer.Seed.{Seeds[0]}")}</color>") : string.Format(Translator.GetString("Farmer.CompleteTaskToGetSeed"), Main.RoleColors[CustomRoles.Farmer]));
if (Seeds.Count > 1) sb.AppendLine(string.Format(Translator.GetString("Farmer.MoreSeeds"), Seeds.Count - 1));
sb.Append(LocateArrow.GetArrows(seer));
return sb.ToString().Trim();
}
}
+2
View File
@@ -206,6 +206,8 @@ public class NiceSwapper : RoleBase
List<MeetingHud.VoterState> votedFor2 = states.Where(x => x.VotedForId == SwapTargets.Item2).ToList();
votedFor1.ForEach(x => x.VotedForId = SwapTargets.Item2);
votedFor2.ForEach(x => x.VotedForId = SwapTargets.Item1);
Utils.SendMessage(string.Format(GetString("SwapVote"), SwapTargets.Item1.ColoredPlayerName(), SwapTargets.Item2.ColoredPlayerName()), title: Utils.ColorString(Utils.GetRoleColor(CustomRoles.NiceSwapper), GetString("SwapTitle")));
}
catch (Exception e) { Utils.ThrowException(e); }
}
+1 -1
View File
@@ -123,6 +123,6 @@ public class Duality : RoleBase
public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false)
{
if (seer.PlayerId != DualityId || seer.PlayerId != target.PlayerId || (!hud && seer.IsModdedClient()) || meeting || !seer.IsAlive()) return string.Empty;
return string.Format(Translator.GetString(KillingPhase ? "Duality.MustKill" : "Duality.MustDoTask"), TimerEndTS - Utils.TimeStamp);
return $"<size=80%>{string.Format(Translator.GetString(KillingPhase ? "Duality.MustKill" : "Duality.MustDoTask"), TimerEndTS - Utils.TimeStamp)}</size>";
}
}