Add Meeting Angel

This commit is contained in:
Gurge44 committed 2026-04-22 09:44:07 +02:00
1 parent e52944c4f4
commit a2f32b7045
8 files changed
+141 -31

No files matched your search

+1
View File
@@ -579,6 +579,7 @@ public enum CustomRoles
Madmate,
Magnet,
Mare,
MeetingAngel, // Ghost role
Messenger,
Mimic,
Minion, // Ghost role
+1
View File
@@ -668,6 +668,7 @@ public class Main : BasePlugin
{ CustomRoles.GA, "#8cffff" },
{ CustomRoles.Facilitator, CovenColor },
{ CustomRoles.Shade, "#060270" },
{ CustomRoles.MeetingAngel, "#a7e8b5" },
// GM
{ CustomRoles.GM, "#ff5b70" },
// Add-ons
+2 -1
View File
@@ -179,7 +179,7 @@ internal static class CheckForEndVotingPatch
bool canVote = !(CheckRole(ps.TargetPlayerId, CustomRoles.Glitch) && !Glitch.CanVote.GetBool());
if (CheckRole(ps.TargetPlayerId, CustomRoles.Shifter) && !Shifter.CanVote.GetBool()) canVote = false;
if (ps.VotedFor.GetPlayer() != null && CheckRole(ps.VotedFor, CustomRoles.Zombie)) canVote = false;
if (ps.VotedFor.GetPlayer() && CheckRole(ps.VotedFor, CustomRoles.Zombie)) canVote = false;
if (Poache.PoachedPlayers.Contains(ps.TargetPlayerId)) canVote = false;
if (Silencer.ForSilencer.Contains(ps.TargetPlayerId) && Main.AllAlivePlayerControls.Count > Silencer.MaxPlayersAliveForSilencedToVote.GetInt()) canVote = false;
@@ -243,6 +243,7 @@ internal static class CheckForEndVotingPatch
Blackmailer.ManipulateVotingResult(votingData, states);
Swapper.ManipulateVotingResult(votingData, states);
MeetingAngel.NegateVotes(votingData, states);
Assumer.OnVotingEnd(votingData);
var exileId = byte.MaxValue;
+12
View File
@@ -4014,6 +4014,18 @@
"HaunterRevealedYou": "You have been revealed by the Haunter!",
"HaunterStopped": "<#00ff00>The Haunter was successfully stopped!</color>",
"HaunterStoppedSelf": "<#ff1313>An impostor came too close to you, your role was removed.</color>",
"MeetingAngel": "MeetingAngel",
"MeetingAngelInfo": "Remove Votes",
"MeetingAngelInfoLong": "(Crewmate Ghost):\\nAs the Meeting Angel, you can use your protect button on someone every X meetings (specified by settings) to remove a certain number of votes targeting them.",
"MeetingAngel.NumVotesNegated": "Number of Votes Removed",
"MeetingAngel.NumMeetingsCooldown": "Per how many meetings can someone be protected",
"MeetingAngel.CanProtectOptions.Crewmate": "Can Protect <#8cffff>Crewmates</color>",
"MeetingAngel.CanProtectOptions.Impostor": "Can Protect <#ff1919>Impostor</color>",
"MeetingAngel.CanProtectOptions.NeutralKilling": "Can Protect <#ffab1b>Neutral</color> <#ff1919>Killers</color>",
"MeetingAngel.CanProtectOptions.NeutralEvil": "Can Protect <#ffab1b>Neutral</color> <#ff5555>Evils</color>",
"MeetingAngel.CanProtectOptions.NeutralPariah": "Can Protect <#ffab1b>Neutral</color> <#ffff44>Pariahs</color>",
"MeetingAngel.CanProtectOptions.NeutralBenign": "Can Protect <#ffab1b>Neutral</color> <#00ffa5>Benigns</color>",
"MeetingAngel.CanProtectOptions.Coven": "Can Protect <#7b3fbb>Covens</color>",
"Minion": "Minion",
"MinionInfo": "Help the Impostors",
"MinionInfoLong": "(Impostor/Neutral Ghost):\\nAs the Minion, you can protect someone (optimally a crewmate) to blind them for a few seconds.",
+106
View File
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using AmongUs.GameOptions;
using EHR.Modules;
namespace EHR.Roles;
public class MeetingAngel : IGhostRole
{
private static OptionItem NumVotesNegated;
private static OptionItem NumMeetingsCooldown;
private static readonly Dictionary<TeamOptions, OptionItem> CanProtectOptions = [];
public Team Team => Team.Crewmate;
public RoleTypes RoleTypes => RoleTypes.GuardianAngel;
public int Cooldown => 5;
private int LastUsedAtMeetingNum = int.MinValue;
public byte TargetId = 252;
public void OnProtect(PlayerControl pc, PlayerControl target)
{
if (LastUsedAtMeetingNum + NumMeetingsCooldown.GetInt() > MeetingStates.MeetingNum) return;
TeamOptions targetTeam = target.GetTeam() switch
{
Team.Crewmate => TeamOptions.Crewmate,
Team.Impostor => TeamOptions.Impostor,
Team.Coven => TeamOptions.Coven,
Team.Neutral => target.GetCustomRole().GetNeutralRoleCategory() switch
{
RoleOptionType.Neutral_Killing => TeamOptions.NeutralKilling,
RoleOptionType.Neutral_Evil => TeamOptions.NeutralEvil,
RoleOptionType.Neutral_Pariah => TeamOptions.NeutralPariah,
RoleOptionType.Neutral_Benign => TeamOptions.NeutralBenign,
_ => default(TeamOptions)
},
_ => default(TeamOptions)
};
if (CanProtectOptions.TryGetValue(targetTeam, out OptionItem optionItem) && !optionItem.GetBool()) return;
LastUsedAtMeetingNum = MeetingStates.MeetingNum;
TargetId = target.PlayerId;
}
public void OnAssign(PlayerControl pc)
{
LastUsedAtMeetingNum = int.MinValue;
TargetId = 252;
}
public void SetupCustomOption()
{
Options.SetupRoleOptions(658600, TabGroup.OtherRoles, CustomRoles.MeetingAngel);
NumVotesNegated = new IntegerOptionItem(658602, "MeetingAngel.NumVotesNegated", new(1, 30, 1), 2, TabGroup.OtherRoles)
.SetParent(Options.CustomRoleSpawnChances[CustomRoles.MeetingAngel])
.SetValueFormat(OptionFormat.Votes);
NumMeetingsCooldown = new IntegerOptionItem(658603, "MeetingAngel.NumMeetingsCooldown", new(1, 30, 1), 2, TabGroup.OtherRoles)
.SetParent(Options.CustomRoleSpawnChances[CustomRoles.MeetingAngel]);
Enum.GetValues<TeamOptions>()[1..].Do(x => CanProtectOptions[x] = new BooleanOptionItem(658603 + (int)x, $"MeetingAngel.CanProtectOptions.{x}", x == TeamOptions.NeutralBenign, TabGroup.OtherRoles)
.SetParent(Options.CustomRoleSpawnChances[CustomRoles.MeetingAngel]));
}
public static void NegateVotes(Dictionary<byte, int> votingData, MeetingHud.VoterState[] states)
{
foreach ((CustomRoles _, IGhostRole instance) in GhostRolesManager.AssignedGhostRoles.Values)
{
if (instance is not MeetingAngel { TargetId: < 252 } meetingAngel) continue;
int negateNum = NumVotesNegated.GetInt();
votingData[meetingAngel.TargetId] -= negateNum;
int negated = 0;
for (var index = 0; index < states.Length && negated < negateNum; index++)
{
ref MeetingHud.VoterState state = ref states[index];
if (state.VotedForId == meetingAngel.TargetId)
{
state.VotedForId = 254;
negated++;
}
}
Utils.SendMessage(string.Format(Translator.GetString("MeetingAngel.NegatedMsg"), negated, meetingAngel.TargetId.ColoredPlayerName()), title: CustomRoles.MeetingAngel.ToColoredString());
meetingAngel.TargetId = 252;
}
}
[SuppressMessage("ReSharper", "UnusedMember.Local")]
enum TeamOptions
{
Crewmate,
Impostor,
NeutralKilling,
NeutralEvil,
NeutralPariah,
NeutralBenign,
Coven
}
}
@@ -96,7 +96,7 @@ internal class Blackmailer : RoleBase
if (vs.SkippedVote)
vote = 253;
else if (vs.AmDead || vs.VotedForId.GetPlayer() == null)
else if (vs.AmDead || !vs.VotedForId.GetPlayer())
vote = 254;
else
vote = vs.VotedForId;
+12 -16
View File
@@ -3,11 +3,10 @@ using System.Linq;
using AmongUs.GameOptions;
using EHR.Modules;
using Hazel;
using UnityEngine;
namespace EHR.Roles;
using static EHR.Translator;
using static Translator;
public class Chainbinder : RoleBase
{
@@ -105,7 +104,7 @@ public class Chainbinder : RoleBase
public override void OnFixedUpdate(PlayerControl pc)
{
if (pc == null || pc.PlayerId != ChainbinderId || !HasLink) return;
if (!HasLink) return;
if (!pc.IsAlive() || !GameStates.IsInTask)
{
@@ -161,9 +160,7 @@ public class Chainbinder : RoleBase
public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false)
{
if (seer.PlayerId != target.PlayerId || seer.PlayerId != ChainbinderId || (seer.IsModdedClient() && !hud) || meeting) return string.Empty;
if (!HasLink) return string.Empty;
if (seer.PlayerId != target.PlayerId || seer.PlayerId != ChainbinderId || (seer.IsModdedClient() && !hud) || meeting || !HasLink) return string.Empty;
return string.Format(GetString("Chainbinder.PairSuffix"), FirstTarget.ColoredPlayerName(), SecondTarget.ColoredPlayerName());
}
@@ -177,7 +174,7 @@ public class Chainbinder : RoleBase
private void TryChainPlayers(PlayerControl binder, PlayerControl requestedTarget = null)
{
if (binder == null || binder.PlayerId != ChainbinderId || !binder.IsAlive()) return;
if (!binder || binder.PlayerId != ChainbinderId || !binder.IsAlive()) return;
if (!TryGetTargets(binder, requestedTarget, out PlayerControl first, out PlayerControl second))
{
@@ -204,11 +201,11 @@ public class Chainbinder : RoleBase
first = null;
second = null;
List<PlayerControl> validTargets = Main.AllAlivePlayerControls
List<PlayerControl> validTargets = Main.EnumerateAlivePlayerControls()
.Where(x => IsValidTarget(binder, x))
.ToList();
if (requestedTarget != null)
if (requestedTarget)
{
if (!IsValidTarget(binder, requestedTarget)) return false;
@@ -220,7 +217,7 @@ public class Chainbinder : RoleBase
.OrderBy(x => Vector2.Distance(x.Pos(), firstPosition))
.FirstOrDefault();
return second != null;
return second;
}
PlayerControl[] closestTargets = validTargets
@@ -237,7 +234,7 @@ public class Chainbinder : RoleBase
private bool IsValidTarget(PlayerControl binder, PlayerControl target)
{
if (binder == null || target == null || target.PlayerId == binder.PlayerId || !target.IsAlive()) return false;
if (!binder || !target || target.PlayerId == binder.PlayerId || !target.IsAlive()) return false;
if (Pelican.IsEaten(target.PlayerId) || target.onLadder || target.inMovingPlat || target.inVent) return false;
if (!FastVector2.DistanceWithinRange(binder.Pos(), target.Pos(), BindRange.GetFloat())) return false;
if (!CanBindImpostors.GetBool() && target.GetCustomRole().IsImpostor()) return false;
@@ -248,7 +245,7 @@ public class Chainbinder : RoleBase
{
foreach (byte playerId in PlayerIdList)
{
if (playerId == ChainbinderId || !Main.PlayerStates.TryGetValue(playerId, out PlayerState state) || state.Role is not Chainbinder cb || !cb.HasLink) continue;
if (playerId == ChainbinderId || !Main.PlayerStates.TryGetValue(playerId, out PlayerState state) || state.Role is not Chainbinder { HasLink: true } cb) continue;
if (cb.FirstTarget == targetId || cb.SecondTarget == targetId) return true;
}
@@ -257,7 +254,7 @@ public class Chainbinder : RoleBase
private static bool AreLinkedPlayersValid(PlayerControl first, PlayerControl second)
{
return first != null && second != null && first.IsAlive() && second.IsAlive() && !Pelican.IsEaten(first.PlayerId) && !Pelican.IsEaten(second.PlayerId);
return first && second && first.IsAlive() && second.IsAlive() && !Pelican.IsEaten(first.PlayerId) && !Pelican.IsEaten(second.PlayerId);
}
private void ClearLink(bool notifyBinder = false, bool sync = true, bool refresh = true)
@@ -269,10 +266,10 @@ public class Chainbinder : RoleBase
if (!hadLink) return;
PlayerControl binder = Utils.GetPlayerById(ChainbinderId);
if (notifyBinder && binder != null && binder.IsAlive())
if (notifyBinder && binder && binder.IsAlive())
binder.Notify(GetString("Chainbinder.PairEnded"));
if (refresh && binder != null)
if (refresh && binder)
Utils.NotifyRoles(SpecifySeer: binder, SpecifyTarget: binder);
}
@@ -286,7 +283,6 @@ public class Chainbinder : RoleBase
private void SyncState()
{
if (!Utils.DoRPC) return;
Utils.SendRPC(CustomRPC.SyncRoleData, ChainbinderId, FirstTarget, SecondTarget);
}
}
+6 -13
View File
@@ -1,11 +1,10 @@
using System.Collections.Generic;
using AmongUs.GameOptions;
using EHR.Modules;
using UnityEngine;
namespace EHR.Roles;
using static EHR.Translator;
using static Translator;
public class Jackpot : RoleBase
{
@@ -135,8 +134,6 @@ public class Jackpot : RoleBase
public override void OnMurder(PlayerControl killer, PlayerControl target)
{
if (killer == null || killer.PlayerId != JackpotId || !target) return;
Money += MoneyPerKill.GetInt();
killer.Notify(string.Format(GetString("Jackpot.KillReward"), MoneyPerKill.GetInt(), Money));
}
@@ -148,8 +145,6 @@ public class Jackpot : RoleBase
public override void OnFixedUpdate(PlayerControl pc)
{
if (pc == null || pc.PlayerId != JackpotId) return;
if (!GameStates.IsInTask || !pc.IsAlive())
{
if (DomainActive || IsNearMissActive || IsJackpotActive)
@@ -198,8 +193,6 @@ public class Jackpot : RoleBase
public override string GetProgressText(byte playerId, bool comms)
{
if (playerId != JackpotId) return string.Empty;
string text = IsJackpotActive
? string.Format(GetString("Jackpot.Progress.Jackpot"), Money, Mathf.CeilToInt(JackpotTimer))
: DomainActive
@@ -211,7 +204,7 @@ public class Jackpot : RoleBase
private void StartDomain(PlayerControl player)
{
if (player == null || player.PlayerId != JackpotId || !player.IsAlive()) return;
if (!player || player.PlayerId != JackpotId || !player.IsAlive()) return;
if (DomainActive || IsJackpotActive)
{
@@ -222,14 +215,14 @@ public class Jackpot : RoleBase
DomainActive = true;
SpinTimer = SpinInterval.GetFloat();
player.Notify($"{GetString("Jackpot.GameRulesTransmitted")}\n<size=80%>{string.Format(GetString("Jackpot.DomainStarted"), SpinInterval.GetFloat())}</size>", 6f);
player.Notify($"{GetString("Jackpot.GameRulesTransmitted")}\n<size=80%>{string.Format(GetString("Jackpot.DomainStarted"), SpinInterval.GetFloat())}</size>");
RefreshTemporaryBuffs(player, refreshCooldown: false);
}
// Domain phase: rolls keep happening until the player jackpots, dies, or a meeting interrupts the sequence.
private void ResolveSpin(PlayerControl player)
{
if (player == null || !player.IsAlive()) return;
if (!player || !player.IsAlive()) return;
int roll = IRandom.Instance.Next(100);
@@ -287,7 +280,7 @@ public class Jackpot : RoleBase
Main.AllPlayerSpeed[JackpotId] = speed;
if (player == null || !player.IsAlive()) return;
if (!player || !player.IsAlive()) return;
if (refreshCooldown) player.ResetKillCooldown();
else player.MarkDirtySettings();
@@ -308,7 +301,7 @@ public class Jackpot : RoleBase
RefreshTemporaryBuffs(player, refreshCooldown);
if (notifyEnd && wasJackpotActive && player != null && player.IsAlive())
if (notifyEnd && wasJackpotActive && player && player.IsAlive())
player.Notify(GetString("Jackpot.JackpotEnded"));
}
}