diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs
index 03bb2ce6b..d6287be76 100644
--- a/Modules/ExtendedPlayerControl.cs
+++ b/Modules/ExtendedPlayerControl.cs
@@ -760,7 +760,7 @@ internal static class ExtendedPlayerControl
LateTask.New(() =>
{
Main.PlayerStates[player.PlayerId].IsBlackOut = false; // Cancel blackout
- player.SyncSettings();
+ player.MarkDirtySettings();
}, duration, "RemoveKillFlash");
if (player.AmOwner)
@@ -773,9 +773,9 @@ internal static class ExtendedPlayerControl
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.KillFlash, SendOption.Reliable, player.OwnerId);
AmongUsClient.Instance.FinishRpcImmediately(writer);
}
- else if (!reactorCheck) player.ReactorFlash(); // Reactor flash
+ else if (!reactorCheck) player.ReactorFlash(canBlind: false); // Reactor flash
- player.SyncSettings();
+ player.MarkDirtySettings();
}
public static void RpcGuardAndKill(this PlayerControl killer, PlayerControl target = null, bool forObserver = false, bool fromSetKCD = false)
@@ -1229,7 +1229,7 @@ internal static class ExtendedPlayerControl
}, 1f + (AmongUsClient.Instance.Ping / 1000f), log: false);
}
- public static void ReactorFlash(this PlayerControl pc, float delay = 0f, float flashDuration = float.NaN)
+ public static void ReactorFlash(this PlayerControl pc, float delay = 0f, float flashDuration = float.NaN, bool canBlind = true)
{
if (pc == null) return;
@@ -1242,15 +1242,15 @@ internal static class ExtendedPlayerControl
_ => SystemTypes.Reactor
};
- if (IsActive(systemtypes))
+ if (canBlind && IsActive(systemtypes))
{
Main.PlayerStates[pc.PlayerId].IsBlackOut = true;
- pc.SyncSettings();
+ pc.MarkDirtySettings();
LateTask.New(() =>
{
Main.PlayerStates[pc.PlayerId].IsBlackOut = false;
- pc.SyncSettings();
+ pc.MarkDirtySettings();
}, (float.IsNaN(flashDuration) ? Options.KillFlashDuration.GetFloat() : flashDuration) + delay, "Fix BlackOut Reactor Flash");
return;
diff --git a/Modules/FixedUpdateCaller.cs b/Modules/FixedUpdateCaller.cs
index 81949dce1..13d8d75b6 100644
--- a/Modules/FixedUpdateCaller.cs
+++ b/Modules/FixedUpdateCaller.cs
@@ -214,15 +214,18 @@ public static class FixedUpdateCaller
try
{
- Main.GameTimer += Time.fixedDeltaTime;
-
- if (Options.EnableGameTimeLimit.GetBool() && Main.GameTimer > Options.GameTimeLimit.GetInt() && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.NaturalDisasters)
+ if (Options.EnableGameTimeLimit.GetBool())
{
- Main.GameTimer = 0f;
- CustomWinnerHolder.ResetAndSetWinner(CustomWinner.None);
+ Main.GameTimer += Time.fixedDeltaTime;
- if (Options.CurrentGameMode == CustomGameMode.NaturalDisasters)
- CustomWinnerHolder.WinnerIds.UnionWith(Main.AllAlivePlayerControls.Select(x => x.PlayerId));
+ if (Main.GameTimer > Options.GameTimeLimit.GetInt() && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.NaturalDisasters)
+ {
+ Main.GameTimer = 0f;
+ CustomWinnerHolder.ResetAndSetWinner(CustomWinner.None);
+
+ if (Options.CurrentGameMode == CustomGameMode.NaturalDisasters)
+ CustomWinnerHolder.WinnerIds.UnionWith(Main.AllAlivePlayerControls.Select(x => x.PlayerId));
+ }
}
}
catch (Exception e) { Utils.ThrowException(e); }
diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs
index cf82be548..9e6c471a7 100644
--- a/Modules/GuessManager.cs
+++ b/Modules/GuessManager.cs
@@ -1116,6 +1116,7 @@ public static class GuessManager
private static bool CanGuess(PlayerControl lp, bool restrictions)
{
+ if (Banshee.Instances.Exists(x => x.ScreechedPlayers.Contains(lp.PlayerId))) return false; // Vanilla clients can't guess with their chat hidden, so don't let modded clients guess for fairness
return lp.Is(CustomRoles.Guesser) || lp.GetCustomRole() switch
{
CustomRoles.EvilGuesser => true,
diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs
index 1a81a7f80..5556e069a 100644
--- a/Patches/CheckGameEndPatch.cs
+++ b/Patches/CheckGameEndPatch.cs
@@ -598,7 +598,7 @@ internal static class GameEndChecker
foreach (PlayerState playerState in statesCoutingAsCrew)
{
- if (playerState.Player == null || !playerState.Player.IsAlive() || !Options.CrewAdvancedGameEndCheckingSettings.TryGetValue(playerState.MainRole, out var option) || !option.GetBool()) continue;
+ if (playerState.IsDead || !Options.CrewAdvancedGameEndCheckingSettings.TryGetValue(playerState.MainRole, out var option) || !option.GetBool()) continue;
playerState.Role.ManipulateGameEndCheckCrew(out bool keepGameGoing, out int countsAs);
crewKeepsGameGoing |= keepGameGoing;
crew += countsAs - 1;
diff --git a/Patches/ControlPatch.cs b/Patches/ControlPatch.cs
index b0b88036a..74490d031 100644
--- a/Patches/ControlPatch.cs
+++ b/Patches/ControlPatch.cs
@@ -86,15 +86,11 @@ internal static class ControllerManagerUpdatePatch
if (KeysDown(KeyCode.LeftAlt, KeyCode.C) && !Input.GetKey(KeyCode.LeftShift) && !GameStates.IsNotJoined)
Utils.CopyCurrentSettings();
-
- bool admin = ChatCommands.IsPlayerAdmin(PlayerControl.LocalPlayer.FriendCode);
- if (!AmongUsClient.Instance.AmHost && !admin) return;
-
- if (KeysDown(KeyCode.Return, KeyCode.C, KeyCode.LeftShift))
+ if (KeysDown(KeyCode.Return, KeyCode.C, KeyCode.LeftShift) && ChatCommands.IsPlayerAdmin(PlayerControl.LocalPlayer.FriendCode))
HudManager.Instance.Chat.SetVisible(true);
- if (KeysDown(KeyCode.Return, KeyCode.L, KeyCode.LeftShift) && GameStates.IsInGame)
+ if (KeysDown(KeyCode.Return, KeyCode.L, KeyCode.LeftShift) && GameStates.IsInGame && ChatCommands.IsPlayerAdmin(PlayerControl.LocalPlayer.FriendCode))
{
CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Draw);
GameEndChecker.CheckCustomEndCriteria();
diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs
index 06b9c9a7b..433cc90bc 100644
--- a/Patches/IntroPatch.cs
+++ b/Patches/IntroPatch.cs
@@ -1070,7 +1070,6 @@ internal static class IntroCutsceneDestroyPatch
{
foreach (PlayerControl pc in aapc)
{
- pc.SyncSettings();
pc.RpcResetAbilityCooldown();
if (pc.GetCustomRole().UsesPetInsteadOfKill())
diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json
index 38a8e250d..950280585 100644
--- a/Resources/Lang/en_US.json
+++ b/Resources/Lang/en_US.json
@@ -2130,6 +2130,7 @@
"ForensicNoticeDeathReason": "\\nThe victim's cause of death is {0}",
"ForensicNoticeAddons": "\\nThe victim's add-ons are: {0}",
"ForensicNoticeKillTime": "\\nThe victim died {0} seconds ago",
+ "ForensicNoticeColorType": "\\nThe killer has a {0} color ({1})",
"SleuthMsg": "{0}'s role is: {1}",
"GodNoticeAlive": "During the meeting, each player felt a revelation from heaven, and it turned out that God is still alive!",
"WorkaholicAdviceAlive": "It's not recommended to kill or vote {0} out. Doing so will help them finish their tasks quicker.",
@@ -3689,6 +3690,11 @@
"WhispererInfo.Time": "I died {0} seconds before the meeting started",
"WhispererInfo.Role": "I was {0}",
"WhispererInfo.KillerRole": "My killer was {0}",
+ "WhispererInfo.AloneInRoomAtDeath": "I was alone with the killer when I died",
+ "WhispererInfo.PlayersInSameRoomAtDeath": "{0} were in the same room as me when I died",
+ "WhispererInfo.NoSabotageAtDeath": "No sabotage was ongoing when I died",
+ "WhispererInfo.SabotageAtDeath": "I died during {0}",
+ "WhispererInfo.DeathReason": "My death reason is {0}",
"WhispererQuestioning": "Questioning {0}... {1}%",
"Leery": "Leery",
"LeeryInfo": "Investigate Who Isn't a Crewmate",
diff --git a/Roles/Coven/Banshee.cs b/Roles/Coven/Banshee.cs
index e2794f185..995b5a7ec 100644
--- a/Roles/Coven/Banshee.cs
+++ b/Roles/Coven/Banshee.cs
@@ -9,7 +9,7 @@ namespace EHR.Coven;
public class Banshee : Coven
{
public static bool On;
- private static List Instances = [];
+ public static List Instances = [];
private static OptionItem AbilityCooldown;
private static OptionItem Radius;
diff --git a/Roles/Crewmate/CopyCat.cs b/Roles/Crewmate/CopyCat.cs
index 48c608049..5e1d423d7 100644
--- a/Roles/Crewmate/CopyCat.cs
+++ b/Roles/Crewmate/CopyCat.cs
@@ -115,6 +115,22 @@ public class CopyCat : RoleBase
CustomRoles.Vindicator or CustomRoles.Pickpocket => CustomRoles.Mayor,
CustomRoles.Councillor => CustomRoles.Judge,
CustomRoles.EvilGuesser or CustomRoles.Doomsayer => CustomRoles.NiceGuesser,
+ CustomRoles.Vengeance => CustomRoles.Adrenaline,
+ CustomRoles.Occultist => CustomRoles.Altruist,
+ CustomRoles.Dealer => CustomRoles.Merchant,
+ CustomRoles.Romantic => CustomRoles.Aid,
+ CustomRoles.Chemist => CustomRoles.Alchemist,
+ CustomRoles.Bomber or CustomRoles.Nuker => CustomRoles.Tree,
+ CustomRoles.Siren => CustomRoles.Rhapsode,
+ CustomRoles.Consort => CustomRoles.Escort,
+ CustomRoles.Parasite => CustomRoles.DoubleAgent,
+ CustomRoles.BloodKnight or CustomRoles.Wildling => CustomRoles.Safeguard,
+ CustomRoles.PlagueBearer => CustomRoles.Socialite,
+ CustomRoles.Demon => CustomRoles.Spy,
+ CustomRoles.Kidnapper => CustomRoles.Autocrat,
+ CustomRoles.Capitalism => CustomRoles.Helper,
+ CustomRoles.Technician or CustomRoles.Saboteur => CustomRoles.Mechanic,
+ CustomRoles.Magician => CustomRoles.Wizard,
_ => role
};
}
diff --git a/Roles/Crewmate/Forensic.cs b/Roles/Crewmate/Forensic.cs
index fe527aa19..9d4cfc53e 100644
--- a/Roles/Crewmate/Forensic.cs
+++ b/Roles/Crewmate/Forensic.cs
@@ -10,6 +10,7 @@ internal class Forensic : RoleBase
public static OptionItem ForensicCanknowDeathReason;
public static OptionItem ForensicCanknowAddons;
public static OptionItem ForensicCanknowKillTime;
+ public static OptionItem ForensicCanknowColorType;
public static Dictionary ForensicNotify = [];
@@ -30,6 +31,9 @@ internal class Forensic : RoleBase
ForensicCanknowKillTime = new BooleanOptionItem(6613, "ForensicCanknowKillTime", true, TabGroup.CrewmateRoles)
.SetParent(Options.CustomRoleSpawnChances[CustomRoles.Forensic]);
+
+ ForensicCanknowColorType = new BooleanOptionItem(6614, "ForensicCanknowColorType", false, TabGroup.CrewmateRoles)
+ .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Forensic]);
}
public override void Init() { }
@@ -40,10 +44,10 @@ internal class Forensic : RoleBase
{
string msg = string.Format(Translator.GetString("ForensicNoticeVictim"), tpc.GetRealName(), tpc.GetCustomRole().ToColoredString());
+ PlayerControl realKiller = tpc.GetRealKiller();
+
if (ForensicCanknowKiller.GetBool())
{
- PlayerControl realKiller = tpc.GetRealKiller();
-
if (realKiller == null)
msg += ";" + Translator.GetString("ForensicNoticeKillerNotFound");
else
@@ -62,6 +66,16 @@ internal class Forensic : RoleBase
msg += ";" + string.Format(Translator.GetString("ForensicNoticeKillTime"), (int)timeSpanSeconds);
}
+ if (ForensicCanknowColorType.GetBool() && realKiller != null)
+ {
+ var darker = new List { 0, 1, 2, 6, 8, 9, 12, 15 };
+ bool isDarker = darker.Contains(realKiller.CurrentOutfit.ColorId);
+ Func selector = x => Utils.ColorString(Palette.PlayerColors[x], Palette.GetColorName(x));
+ var colors = isDarker ? string.Join('/', darker.Select(selector)) : string.Join('/', Enumerable.Range(0, 18).Except(darker).Select(selector));
+ var str = Translator.GetString(isDarker ? "WhispererInfo.ColorDark" : "WhispererInfo.ColorLight");
+ msg += ";" + string.Format(Translator.GetString("ForensicNoticeColorType"), str, colors);
+ }
+
ForensicNotify[player.PlayerId] = msg;
}
}
\ No newline at end of file
diff --git a/Roles/Crewmate/Whisperer.cs b/Roles/Crewmate/Whisperer.cs
index e23dcd736..6d02f87c1 100644
--- a/Roles/Crewmate/Whisperer.cs
+++ b/Roles/Crewmate/Whisperer.cs
@@ -19,12 +19,14 @@ public class Whisperer : RoleBase
public static OptionItem AbilityChargesWhenFinishedTasks;
private static DateTime LastMeetingStart;
+ private static Dictionary DeathInfo = [];
private int Count;
private (string Name, int Percent) CurrentlyQuestioning;
private List Info;
private List Souls;
private byte WhispererId;
+
public override bool IsEnable => On;
public override void SetupCustomOption()
@@ -36,7 +38,7 @@ public class Whisperer : RoleBase
.SetParent(Options.CustomRoleSpawnChances[CustomRoles.Whisperer])
.SetValueFormat(OptionFormat.Seconds);
- Duration = new IntegerOptionItem(++id, "WhispererDuration", new(0, 60, 1), 7, TabGroup.CrewmateRoles)
+ Duration = new IntegerOptionItem(++id, "WhispererDuration", new(0, 60, 1), 4, TabGroup.CrewmateRoles)
.SetParent(Options.CustomRoleSpawnChances[CustomRoles.Whisperer])
.SetValueFormat(OptionFormat.Seconds);
@@ -44,7 +46,7 @@ public class Whisperer : RoleBase
.SetParent(Options.CustomRoleSpawnChances[CustomRoles.Whisperer])
.SetValueFormat(OptionFormat.Times);
- WhispererAbilityUseGainWithEachTaskCompleted = new FloatOptionItem(++id, "AbilityUseGainWithEachTaskCompleted", new(0f, 5f, 0.05f), 1f, TabGroup.CrewmateRoles)
+ WhispererAbilityUseGainWithEachTaskCompleted = new FloatOptionItem(++id, "AbilityUseGainWithEachTaskCompleted", new(0f, 5f, 0.05f), 3.5f, TabGroup.CrewmateRoles)
.SetParent(Options.CustomRoleSpawnChances[CustomRoles.Whisperer])
.SetValueFormat(OptionFormat.Times);
@@ -57,6 +59,7 @@ public class Whisperer : RoleBase
{
On = false;
Instances = [];
+ DeathInfo = [];
}
public override void Add(byte playerId)
@@ -100,7 +103,7 @@ public class Whisperer : RoleBase
byte soulPlayerId = soul.Player.PlayerId;
- if (Vector2.Distance(pc.Pos(), soul.Position) > 1f)
+ if (Vector2.Distance(pc.Pos(), soul.Position) > 1.5f)
{
soul.QuestioningTime = 0f;
Utils.SendRPC(CustomRPC.SyncRoleData, WhispererId, 3, soulPlayerId, soul.QuestioningTime);
@@ -124,15 +127,22 @@ public class Whisperer : RoleBase
{
PlayerState state = Main.PlayerStates[soulPlayerId];
(DateTime TimeStamp, byte ID) killer = state.RealKiller;
- int next = IRandom.Instance.Next(4);
+ int next = IRandom.Instance.Next(7);
if (state.deathReason == PlayerState.DeathReason.Disconnected) next = 2;
+ (byte[] SameRoomPlayers, SystemTypes? ActiveSabotage) deathInfo = ([], null);
+ if (next > 3 && !DeathInfo.TryGetValue(soulPlayerId, out deathInfo)) next = IRandom.Instance.Next(4);
+ if (pc.Is(CustomRoles.Autopsy) || pc.Is(CustomRoles.Doctor) || Options.EveryoneSeesDeathReasons.GetBool()) next = IRandom.Instance.Next(6);
+ PlayerState killerState = Main.PlayerStates[killer.ID];
info = next switch
{
0 => string.Format(Translator.GetString("WhispererInfo.Color"), GetColorInfo(Utils.GetPlayerInfoById(killer.ID).DefaultOutfit.ColorId, out string colors), colors),
1 => string.Format(Translator.GetString("WhispererInfo.Time"), (int)Math.Round((LastMeetingStart - killer.TimeStamp).TotalSeconds)),
- 2 => string.Format(Translator.GetString("WhispererInfo.Role"), state.MainRole.ToColoredString()),
- 3 => string.Format(Translator.GetString("WhispererInfo.KillerRole"), Main.PlayerStates[killer.ID].MainRole.ToColoredString()),
+ 2 => string.Format(Translator.GetString("WhispererInfo.Role"), state.MainRole.ToColoredString() + (state.SubRoles.Count == 0 ? string.Empty : string.Join(' ', state.SubRoles.ConvertAll(x => x.ToColoredString())))),
+ 3 => string.Format(Translator.GetString("WhispererInfo.KillerRole"), killerState.MainRole.ToColoredString() + (killerState.SubRoles.Count == 0 ? string.Empty : string.Join(' ', killerState.SubRoles.ConvertAll(x => x.ToColoredString())))),
+ 4 => string.Format(Translator.GetString(deathInfo.SameRoomPlayers.Length == 0 ? "WhispererInfo.AloneInRoomAtDeath" : "WhispererInfo.PlayersInSameRoomAtDeath"), string.Join(", ", deathInfo.SameRoomPlayers.Select(x => x.ColoredPlayerName()))),
+ 5 => string.Format(Translator.GetString(deathInfo.ActiveSabotage.HasValue ? "WhispererInfo.NoSabotageAtDeath" : "WhispererInfo.SabotageAtDeath"), Translator.GetString(deathInfo.ActiveSabotage.HasValue ? deathInfo.ActiveSabotage.ToString() : "None")),
+ 6 => string.Format(Translator.GetString("WhispererInfo.DeathReason"), Translator.GetString($"DeathReason.{state.deathReason}")),
_ => string.Empty
};
@@ -188,6 +198,9 @@ public class Whisperer : RoleBase
instance.Souls.Add(new(target));
Utils.SendRPC(CustomRPC.SyncRoleData, instance.WhispererId, 5, target.PlayerId);
}
+
+ var room = target.GetPlainShipRoom();
+ DeathInfo[target.PlayerId] = (room == null ? [] : Main.AllAlivePlayerControls.Where(x => x.GetPlainShipRoom() == room).Select(x => x.PlayerId).ToArray(), new[] { SystemTypes.Electrical, SystemTypes.Reactor, SystemTypes.Laboratory, SystemTypes.LifeSupp, SystemTypes.Comms, SystemTypes.HeliSabotage, SystemTypes.MushroomMixupSabotage, (SystemTypes)SubmergedCompatibility.SubmergedSystemTypes.Ballast }.FindFirst(Utils.IsActive, out var sabotage) ? sabotage : null);
}
public override void OnReportDeadBody()
@@ -233,7 +246,6 @@ public class Whisperer : RoleBase
public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false)
{
if (seer.PlayerId != target.PlayerId || seer.PlayerId != WhispererId || meeting || (seer.IsModdedClient() && !hud)) return string.Empty;
-
return "" + string.Join('\n', Info) + (CurrentlyQuestioning.Percent > 0 ? "\n" + string.Format(Translator.GetString("WhispererQuestioning"), CurrentlyQuestioning.Name, CurrentlyQuestioning.Percent) : string.Empty) + "";
}