[](https://discord.gg/77RkMJHWsM)\r
\r
-\r
+\r
\r
\r
# The Other Roles\r
# Releases\r
| Among Us - Version| Mod Version | Link |\r
|----------|-------------|-----------------|\r
+| 2021.4.14s| v2.5.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.5.0/TheOtherRoles.zip)\r
| 2021.4.14s| v2.4.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.4.0/TheOtherRoles.zip)\r
| 2021.4.14s| v2.3.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.3.0/TheOtherRoles.zip)\r
| 2021.4.14s| v2.2.2| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.2.2/TheOtherRoles.zip)\r
<details>\r
<summary>Click to show the Changelog</summary>\r
\r
+**Version 2.5.0**\r
+- **New Role:** [Security Guard](#security-guard)\r
+- Fixed a bug where the game would stop after the first meeting\r
+- Fixed a bug where killing with the hotkey Q ignored shields\r
+\r
**Version 2.4.0**\r
- **New Role:** [Warlock](#warlock)\r
- Added an option that allows ghosts to see the roles and remaining tasks of other players\r
| Impostors Can Kill Anyone If There Is A Spy | This allows the Impostors to kill both the Spy and their Impostor partners\r
-----------------------\r
\r
+## Security Guard\r
+### **Team: Crewmates**\r
+The Security Guard is a Crewmate that has a certain amount of screws that he can use for either sealing vents or for placing new cameras.\\r
+Placing a new camera and sealing vents takes a configurable amount of screws. The total number of screws that a SecurityGuard has can also be configured.\\r
+The new camera will be visible after the next meeting and accessible by everyone.\\r
+The vents will be sealed after the next meeting, players can't enter or exit sealed vents, but they can still "move to them" underground.\\r
+**NOTE:**\r
+- Tickster boxes can't be sealed\r
+- The remaining number of screws can be seen above his special button\r
+- On Skeld the four cameras will be replaced every 3 seconds (with the next four cameras). You can also navigate manually using the arrow keys.\r
+\r
+\r
+### Game Options\r
+| Name | Description\r
+|----------|:-------------:|\r
+| Security Guard Spawn Chance |\r
+| Security Guard Cooldown | \r
+| Security Guard Number Of Screws | The number of screws that a Security Guard can use in a game\r
+| Number Of Screws Per Cam | The number of screws it takes to place a camera\r
+| Number Of Screws Per Vent | The number of screws it takes to seal a vent\r
+-----------------------\r
+\r
\r
# Source code\r
It's bad I know, this is a side project and my second week of modding. So there are no best practices around here.\r
private static CustomButton lightsOutButton;
public static CustomButton cleanerCleanButton;
public static CustomButton warlockCurseButton;
+ public static CustomButton securityGuardButton;
+ public static TMPro.TMP_Text securityGuardButtonScrewsText;
public static void setCustomButtonCooldowns() {
engineerRepairButton.MaxTimer = 0f;
lightsOutButton.MaxTimer = Trickster.lightsOutCooldown;
cleanerCleanButton.MaxTimer = Cleaner.cooldown;
warlockCurseButton.MaxTimer = Warlock.cooldown;
+ securityGuardButton.MaxTimer = SecurityGuard.cooldown;
timeMasterShieldButton.EffectDuration = TimeMaster.shieldDuration;
hackerButton.EffectDuration = Hacker.duration;
() => {
if (Helpers.handleMurderAttempt(Vampire.currentTarget)) {
if (Vampire.targetNearGarlic) {
- PlayerControl.LocalPlayer.RpcMurderPlayer(Vampire.currentTarget);
+ MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1);
+ writer.Write(Vampire.vampire.PlayerId);
+ writer.Write(Vampire.currentTarget.PlayerId);
+ AmongUsClient.Instance.FinishRpcImmediately(writer);
+ RPCProcedure.uncheckedMurderPlayer(Vampire.vampire.PlayerId, Vampire.currentTarget.PlayerId);
+
vampireKillButton.HasEffect = false; // Block effect on this click
vampireKillButton.Timer = vampireKillButton.MaxTimer;
} else {
__instance,
KeyCode.F
);
+
// Warlock curse
warlockCurseButton = new CustomButton(
() => {
KeyCode.F
);
+ // SecurityGuard button
+ securityGuardButton = new CustomButton(
+ () => {
+ if (SecurityGuard.ventTarget == null) { // Place camera
+ var pos = PlayerControl.LocalPlayer.transform.position;
+ byte[] buff = new byte[sizeof(float) * 2];
+ Buffer.BlockCopy(BitConverter.GetBytes(pos.x), 0, buff, 0*sizeof(float), sizeof(float));
+ Buffer.BlockCopy(BitConverter.GetBytes(pos.y), 0, buff, 1*sizeof(float), sizeof(float));
+
+ MessageWriter writer = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.PlaceCamera, Hazel.SendOption.Reliable);
+ writer.WriteBytesAndSize(buff);
+ writer.EndMessage();
+ RPCProcedure.placeCamera(buff);
+ } else { // Seal vent
+ MessageWriter writer = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SealVent, Hazel.SendOption.Reliable);
+ writer.WritePacked(SecurityGuard.ventTarget.Id);
+ writer.EndMessage();
+ RPCProcedure.sealVent(SecurityGuard.ventTarget.Id);
+ SecurityGuard.ventTarget = null;
+ }
+ securityGuardButton.Timer = securityGuardButton.MaxTimer;
+ },
+ () => { return SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; },
+ () => {
+ securityGuardButton.killButtonManager.renderer.sprite = (SecurityGuard.ventTarget == null) ? SecurityGuard.getPlaceCameraButtonSprite() : SecurityGuard.getCloseVentButtonSprite();
+ if (securityGuardButtonScrewsText != null) securityGuardButtonScrewsText.text = $"{SecurityGuard.remainingScrews}/{SecurityGuard.totalScrews}";
+ return SecurityGuard.remainingScrews >= (SecurityGuard.ventTarget == null ? SecurityGuard.camPrice : SecurityGuard.ventPrice) && PlayerControl.LocalPlayer.CanMove;
+ },
+ () => { securityGuardButton.Timer = securityGuardButton.MaxTimer; },
+ SecurityGuard.getPlaceCameraButtonSprite(),
+ new Vector3(-1.3f, 0f, 0f),
+ __instance,
+ KeyCode.Q
+ );
+
+ // SecurityGuard button screws counter
+ securityGuardButtonScrewsText = GameObject.Instantiate(securityGuardButton.killButtonManager.TimerText, securityGuardButton.killButtonManager.TimerText.transform.parent);
+ securityGuardButtonScrewsText.text = "";
+ securityGuardButtonScrewsText.enableWordWrapping = false;
+ securityGuardButtonScrewsText.transform.localScale = Vector3.one * 0.5f;
+ securityGuardButtonScrewsText.transform.localPosition += new Vector3(-0.05f, 0.7f, 0);
// Set the default (or settings from the previous game) timers/durations when spawning the buttons
setCustomButtonCooldowns();
if (handled) {
__instance.TextArea.Clear();
__instance.quickChatMenu.ResetGlyphs();
- System.Console.WriteLine("Chat Clear");
}
return !handled;
}
}
}
-}
\ No newline at end of file
+}
--- /dev/null
+
+using HarmonyLib;
+using UnityEngine;
+using System.Collections.Generic;
+using Hazel;
+using System;
+using UnityEngine.UI;
+using UnityEngine.Events;
+
+namespace TheOtherRoles {
+ [HarmonyPatch(typeof(OptionsMenuBehaviour), nameof(OptionsMenuBehaviour.Start))]
+ public class OptionsMenuBehaviourStartPatch {
+ private static Vector3? origin;
+ private static ToggleButtonBehaviour streamerModeButton;
+ private static ToggleButtonBehaviour ghostsSeeTasksButton;
+ private static ToggleButtonBehaviour ghostsSeeRolesButton;
+
+ private static void updateToggle(ToggleButtonBehaviour button, string text, bool on) {
+ if (button == null || button.gameObject == null) return;
+
+ Color color = on ? new Color(0f, 1f, 0.16470589f, 1f) : Color.white;
+ button.Background.color = color;
+ button.Text.text = $"{text}{(on ? "On" : "Off")}";
+ if (button.Rollover) button.Rollover.ChangeOutColor(color);
+ }
+
+ private static ToggleButtonBehaviour createCustomToggle(string text, bool on, Vector3 offset, UnityEngine.Events.UnityAction onClick, OptionsMenuBehaviour __instance) {
+ if (__instance.CensorChatButton != null) {
+ var button = UnityEngine.Object.Instantiate(__instance.CensorChatButton, __instance.CensorChatButton.transform.parent);
+ button.transform.localPosition = (origin ?? Vector3.zero) + offset;
+ PassiveButton passiveButton = button.GetComponent<PassiveButton>();
+ passiveButton.OnClick = new Button.ButtonClickedEvent();
+ passiveButton.OnClick.AddListener(onClick);
+ updateToggle(button, text, on);
+
+ return button;
+ }
+ return null;
+ }
+
+ public static void Postfix(OptionsMenuBehaviour __instance) {
+ if (__instance.CensorChatButton != null) {
+ if (origin == null) origin = __instance.CensorChatButton.transform.localPosition + Vector3.up * 0.25f;
+ __instance.CensorChatButton.transform.localPosition = origin.Value + Vector3.left * 1.3f;
+ }
+
+ if ((streamerModeButton == null || streamerModeButton.gameObject == null)) {
+ streamerModeButton = createCustomToggle("Streamer Mode: ", TheOtherRolesPlugin.StreamerMode.Value, Vector3.right * 1.3f, (UnityEngine.Events.UnityAction)streamerModeToggle, __instance);
+
+ void streamerModeToggle() {
+ TheOtherRolesPlugin.StreamerMode.Value = !TheOtherRolesPlugin.StreamerMode.Value;
+ updateToggle(streamerModeButton, "Streamer Mode: ", TheOtherRolesPlugin.StreamerMode.Value);
+ }
+ }
+
+ if ((ghostsSeeTasksButton == null || ghostsSeeTasksButton.gameObject == null)) {
+ ghostsSeeTasksButton = createCustomToggle("Ghosts See Remaining Tasks: ", TheOtherRolesPlugin.GhostsSeeTasks.Value, new Vector2(-1.3f, -0.5f), (UnityEngine.Events.UnityAction)ghostsSeeTaskToggle, __instance);
+
+ void ghostsSeeTaskToggle() {
+ TheOtherRolesPlugin.GhostsSeeTasks.Value = !TheOtherRolesPlugin.GhostsSeeTasks.Value;
+ MapOptions.ghostsSeeTasks = TheOtherRolesPlugin.GhostsSeeTasks.Value;
+ updateToggle(ghostsSeeTasksButton, "Ghosts See Remaining Tasks: ", TheOtherRolesPlugin.GhostsSeeTasks.Value);
+ }
+ }
+
+ if ((ghostsSeeRolesButton == null || ghostsSeeRolesButton.gameObject == null)) {
+ ghostsSeeRolesButton = createCustomToggle("Ghosts See Roles: ", TheOtherRolesPlugin.GhostsSeeRoles.Value, new Vector2(1.3f, -0.5f), (UnityEngine.Events.UnityAction)ghostsSeeRolesToggle, __instance);
+
+ void ghostsSeeRolesToggle() {
+ TheOtherRolesPlugin.GhostsSeeRoles.Value = !TheOtherRolesPlugin.GhostsSeeRoles.Value;
+ MapOptions.ghostsSeeRoles = TheOtherRolesPlugin.GhostsSeeRoles.Value;
+ updateToggle(ghostsSeeRolesButton, "Ghosts See Roles: ", TheOtherRolesPlugin.GhostsSeeRoles.Value);
+ }
+ }
+ }
+ }
+
+ [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.SetText))]
+ public static class HiddenTextPatch
+ {
+ private static void Postfix(TextBoxTMP __instance)
+ {
+ bool flag = TheOtherRolesPlugin.StreamerMode.Value && (__instance.name == "GameIdText" || __instance.name == "IpTextBox" || __instance.name == "PortTextBox");
+ if (flag) __instance.outputText.text = new string('*', __instance.text.Length);
+ }
+ }
+}
\ No newline at end of file
using System.IO;
using System.Reflection;
using UnityEngine;
+using UnityEngine.UI;
public class CustomButton
{
buttons.Add(this);
killButtonManager = UnityEngine.Object.Instantiate(hudManager.KillButton, hudManager.transform);
PassiveButton button = killButtonManager.GetComponent<PassiveButton>();
- button.OnClick.RemoveAllListeners();
+ button.OnClick = new Button.ButtonClickedEvent();
button.OnClick.AddListener((UnityEngine.Events.UnityAction)onClickEvent);
setActive(false);
public static CustomOption warlockCooldown;
public static CustomOption warlockRootTime;
+ public static CustomOption securityGuardSpawnRate;
+ public static CustomOption securityGuardCooldown;
+ public static CustomOption securityGuardTotalScrews;
+ public static CustomOption securityGuardCamPrice;
+ public static CustomOption securityGuardVentPrice;
+
public static CustomOption maxNumberOfMeetings;
public static CustomOption blockSkippingInEmergencyMeetings;
public static CustomOption noVoteIsSelfVote;
public static CustomOption hidePlayerNames;
- public static CustomOption showGhostInfo;
internal static Dictionary<byte, byte[]> blockedRolePairings = new Dictionary<byte, byte[]>();
spyCanDieToSheriff = CustomOption.Create(241, "Spy Can Die To Sheriff", false, spySpawnRate);
spyImpostorsCanKillAnyone = CustomOption.Create(242, "Impostors Can Kill Anyone If There Is A Spy", true, spySpawnRate);
+ securityGuardSpawnRate = CustomOption.Create(280, cs(SecurityGuard.color, "Security Guard"), rates, null, true);
+ securityGuardCooldown = CustomOption.Create(281, "Security Guard Cooldown", 30f, 10f, 60f, 2.5f, securityGuardSpawnRate);
+ securityGuardTotalScrews = CustomOption.Create(282, "Security Guard Number Of Screws", 7f, 1f, 15f, 1f, securityGuardSpawnRate);
+ securityGuardCamPrice = CustomOption.Create(283, "Number Of Screws Per Cam", 2f, 1f, 15f, 1f, securityGuardSpawnRate);
+ securityGuardVentPrice = CustomOption.Create(284, "Number Of Screws Per Vent", 1f, 1f, 15f, 1f, securityGuardSpawnRate);
+
// Other options
maxNumberOfMeetings = CustomOption.Create(3, "Number Of Meetings (excluding Mayor meeting)", 10, 0, 15, 1, null, true);
blockSkippingInEmergencyMeetings = CustomOption.Create(4, "Block Skipping In Emergency Meetings", false);
noVoteIsSelfVote = CustomOption.Create(5, "No Vote Is Self Vote", false, blockSkippingInEmergencyMeetings);
hidePlayerNames = CustomOption.Create(6, "Hide Player Names", false);
- showGhostInfo = CustomOption.Create(7, "Ghosts Can See Roles And Remaining Tasks", true);
blockedRolePairings.Add((byte)RoleId.Vampire, new [] { (byte)RoleId.Warlock});
blockedRolePairings.Add((byte)RoleId.Warlock, new [] { (byte)RoleId.Vampire});
var hudString = sb.ToString();
int defaultSettingsLines = 19;
- int roleSettingsLines = defaultSettingsLines + 29;
+ int roleSettingsLines = defaultSettingsLines + 30;
int detailedSettingsP1 = roleSettingsLines + 34;
int detailedSettingsP2 = detailedSettingsP1 + 36;
int end1 = hudString.TakeWhile(c => (defaultSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count();
continue;
else if (!playerVersions.ContainsKey(client.Id)) {
blockStart = true;
- message += $"<color=#FF0000FF>{client.Character.Data.PlayerName} has an outdated or no version of The Other Roles\n</color>";
+ message += $"<color=#FF0000FF>{client.Character.Data.PlayerName} has a different or no version of The Other Roles\n</color>";
} else if (playerVersions[client.Id].Item1 != TheOtherRolesPlugin.Major || playerVersions[client.Id].Item2 != TheOtherRolesPlugin.Minor || playerVersions[client.Id].Item3 != TheOtherRolesPlugin.Patch) {
blockStart = true;
- message += $"<color=#FF0000FF>{client.Character.Data.PlayerName} has an outdated version (v{playerVersions[client.Id].Item1}.{playerVersions[client.Id].Item2}.{playerVersions[client.Id].Item3}) of The Other Roles\n</color>";
+ message += $"<color=#FF0000FF>{client.Character.Data.PlayerName} has a different version (v{playerVersions[client.Id].Item1}.{playerVersions[client.Id].Item2}.{playerVersions[client.Id].Item3}) of The Other Roles\n</color>";
}
}
if (blockStart) {
}
public static void clearAllTasks(this PlayerControl player) {
+ if (player == null) return;
for (int i = 0; i < player.myTasks.Count; i++) {
PlayerTask playerTask = player.myTasks[i];
playerTask.OnRemove();
UnityEngine.Object.Destroy(playerTask.gameObject);
}
player.myTasks.Clear();
+
+ if (player.Data != null && player.Data.Tasks != null)
+ player.Data.Tasks.Clear();
}
public static string cs(Color c, string s) {
public class TheOtherRolesPlugin : BasePlugin
{
public const string Id = "me.eisbison.theotherroles";
- public const string Version = "2.4.0";
+ public const string Version = "2.5.0";
public const byte Major = 2;
- public const byte Minor = 4;
+ public const byte Minor = 5;
public const byte Patch = 0;
public Harmony Harmony { get; } = new Harmony(Id);
public static ConfigEntry<bool> DebugMode { get; private set; }
public static ConfigEntry<bool> StreamerMode { get; set; }
+ public static ConfigEntry<bool> GhostsSeeTasks { get; set; }
+ public static ConfigEntry<bool> GhostsSeeRoles { get; set; }
+ public static ConfigEntry<bool> HostSeesVotesLog { get; set; }
public static ConfigEntry<string> StreamerModeReplacementText { get; set; }
public static ConfigEntry<string> StreamerModeReplacementColor { get; set; }
public static ConfigEntry<string> Ip { get; set; }
}
public override void Load() {
- DebugMode = Config.Bind("Custom", "Enable Debug Mode", false);
- StreamerMode = Config.Bind("Custom", "Enable Streamer Mode", false);
+ DebugMode = Config.Bind("Custom", "Enable Debug Mode", false);
+ StreamerMode = Config.Bind("Custom", "Enable Streamer Mode", false);
+ GhostsSeeTasks = Config.Bind("Custom", "Ghosts See Remaining Tasks", true);
+ GhostsSeeRoles = Config.Bind("Custom", "Ghosts See Roles", true);
+ HostSeesVotesLog = Config.Bind("Custom", "Host Sees Votes Log", false);
StreamerModeReplacementText = Config.Bind("Custom", "Streamer Mode Replacement Text", "\n\nThe Other Roles");
StreamerModeReplacementColor = Config.Bind("Custom", "Streamer Mode Replacement Text Hex Color", "#87AAF5FF");
[HarmonyPatch(typeof(ChatController), nameof(ChatController.Awake))]
public static class ChatControllerAwakePatch {
private static void Prefix() {
- SaveManager.chatModeType = 1;
- SaveManager.isGuest = false;
+ if (!EOSManager.Instance.IsMinor()) {
+ SaveManager.chatModeType = 1;
+ SaveManager.isGuest = false;
+ }
}
}
public static bool blockSkippingInEmergencyMeetings = false;
public static bool noVoteIsSelfVote = false;
public static bool hidePlayerNames = false;
- public static bool showGhostInfo = true;
+ public static bool ghostsSeeRoles = true;
+ public static bool ghostsSeeTasks = true;
// Updating values
public static int meetingsCount = 0;
+ public static List<SurvCamera> camerasToAdd = new List<SurvCamera>();
+ public static List<Vent> ventsToSeal = new List<Vent>();
public static void clearAndReloadMapOptions() {
meetingsCount = 0;
+ camerasToAdd = new List<SurvCamera>();
+ ventsToSeal = new List<Vent>();
maxNumberOfMeetings = Mathf.RoundToInt(CustomOptionHolder.maxNumberOfMeetings.getSelection());
blockSkippingInEmergencyMeetings = CustomOptionHolder.blockSkippingInEmergencyMeetings.getBool();
noVoteIsSelfVote = CustomOptionHolder.noVoteIsSelfVote.getBool();
hidePlayerNames = CustomOptionHolder.hidePlayerNames.getBool();
- showGhostInfo = CustomOptionHolder.showGhostInfo.getBool();
+ ghostsSeeRoles = TheOtherRolesPlugin.GhostsSeeRoles.Value;
+ ghostsSeeTasks = TheOtherRolesPlugin.GhostsSeeTasks.Value;
}
}
}
\ No newline at end of file
[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.CheckForEndVoting))]
class MeetingCalculateVotesPatch {
private static byte[] calculateVotes(MeetingHud __instance) {
- byte[] array = new byte[__instance.playerStates.Length + 1];
+ byte[] array = new byte[16];
for (int i = 0; i < __instance.playerStates.Length; i++)
{
PlayerVoteArea playerVoteArea = __instance.playerStates[i];
return result;
}
- static bool Prefix(MeetingHud __instance)
- {
- if (__instance.playerStates.All((PlayerVoteArea ps) => ps.isDead || ps.didVote))
- {
+ static bool Prefix(MeetingHud __instance) {
+ if (__instance.playerStates.All((PlayerVoteArea ps) => ps.isDead || ps.didVote)) {
// If skipping is disabled, replace skipps/no-votes with self vote
if (target == null && blockSkippingInEmergencyMeetings && noVoteIsSelfVote) {
foreach (PlayerVoteArea playerVoteArea in __instance.playerStates) {
break;
}
}
- byte[] array = new byte[__instance.playerStates.Length];
- for (int i = 0; i < __instance.playerStates.Length; i++)
- {
+ byte[] array = new byte[15];
+ for (int i = 0; i < __instance.playerStates.Length; i++) {
PlayerVoteArea playerVoteArea = __instance.playerStates[i];
array[(int)playerVoteArea.TargetPlayerId] = playerVoteArea.GetState();
}
+
// RPCVotingComplete
if (AmongUsClient.Instance.AmClient)
__instance.VotingComplete(array, exiled, tie);
__instance.SkipVoteButton.gameObject.SetActive(false);
}
}
+
+ [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.CastVote))]
+ class MeetingHudCastVotePatch {
+ static void Postfix([HarmonyArgument(0)]byte srcPlayerId, [HarmonyArgument(1)]sbyte suspectPlayerId) {
+ var source = Helpers.playerById(srcPlayerId);
+ if (source != null && source.Data != null && AmongUsClient.Instance.AmHost && TheOtherRolesPlugin.HostSeesVotesLog.Value) {
+ string target = null;
+ if (suspectPlayerId == -2) target = "didn't vote";
+ else if (suspectPlayerId == -1) target = "skipped";
+ else if (suspectPlayerId >= 0) {
+ System.Console.WriteLine(suspectPlayerId);
+ System.Console.WriteLine((byte)suspectPlayerId);
+ var targetPlayer = Helpers.playerById((byte)suspectPlayerId);
+ if (targetPlayer != null && targetPlayer.Data != null) target = $"voted {targetPlayer.Data.PlayerName}";
+ }
+
+ if (target != null) System.Console.WriteLine($"{source.Data.PlayerName} {target}");
+ }
+ }
+ }
}
[HarmonyPatch(typeof(ExileController), "Begin")]
if (Trickster.trickster != null && JackInTheBox.hasJackInTheBoxLimitReached()) {
JackInTheBox.convertToVents();
}
+
+ // SecurityGuard vents and cameras
+ MapOptions.camerasToAdd.ForEach(x => x.gameObject.SetActive(true));
+ var allCameras = ShipStatus.Instance.AllCameras.ToList();
+ allCameras.AddRange(MapOptions.camerasToAdd);
+ ShipStatus.Instance.AllCameras = allCameras.ToArray();
+ MapOptions.camerasToAdd = new List<SurvCamera>();
+
+ foreach (Vent vent in MapOptions.ventsToSeal) {
+ PowerTools.SpriteAnim animator = vent.GetComponent<PowerTools.SpriteAnim>();
+ animator?.Stop();
+ vent.myRend.sprite = animator == null ? SecurityGuard.getStaticVentSealedSprite() : SecurityGuard.getAnimatedVentSealedSprite();
+ vent.name = "SealedVent_" + vent.name;
+ }
+ MapOptions.ventsToSeal = new List<Vent>();
}
}
__result = ExileController.Instance.exiled.PlayerName + " was The Sidekick.";
else if(Spy.spy != null && ExileController.Instance.exiled.Object.PlayerId == Spy.spy.PlayerId)
__result = ExileController.Instance.exiled.PlayerName + " was The Spy.";
+ else if(SecurityGuard.securityGuard != null && ExileController.Instance.exiled.Object.PlayerId == SecurityGuard.securityGuard.PlayerId)
+ __result = ExileController.Instance.exiled.PlayerName + " was The SecurityGuard.";
else
__result = ExileController.Instance.exiled.PlayerName + " was not The Impostor.";
}
float num = GameOptionsData.KillDistances[Mathf.Clamp(PlayerControl.GameOptions.KillDistance, 0, 2)];
if (!ShipStatus.Instance) return result;
if (targetingPlayer == null) targetingPlayer = PlayerControl.LocalPlayer;
-
+ if (targetingPlayer.Data.IsDead) return result;
+
Vector2 truePosition = targetingPlayer.GetTruePosition();
Il2CppSystem.Collections.Generic.List<GameData.PlayerInfo> allPlayers = GameData.Instance.AllPlayers;
for (int i = 0; i < allPlayers.Count; i++)
}
public static void updateGhostInfo() {
- if (!MapOptions.showGhostInfo) return;
-
foreach (PlayerControl p in PlayerControl.AllPlayerControls) {
if (p != PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead) continue;
var (tasksCompleted, tasksTotal) = TasksHandler.taskInfo(p.Data);
string roleNames = String.Join(", ", RoleInfo.getRoleInfoForPlayer(p).Select(x => Helpers.cs(x.color, x.name)).ToArray());
string taskInfo = tasksTotal > 0 ? $"<color=#FAD934FF>({tasksCompleted}/{tasksTotal})</color>" : "";
- playerGhostInfo.text = $"{roleNames} {taskInfo}".Trim();
- if (meetingGhostInfo != null) meetingGhostInfo.text = MeetingHud.Instance.state == MeetingHud.VoteStates.Results ? "" : $"{roleNames} {taskInfo}".Trim();
+
+ string info = "";
+ if (p == PlayerControl.LocalPlayer || (MapOptions.ghostsSeeRoles && MapOptions.ghostsSeeTasks))
+ info = $"{roleNames} {taskInfo}".Trim();
+ else if (MapOptions.ghostsSeeTasks)
+ info = $"{taskInfo}".Trim();
+ else if (MapOptions.ghostsSeeRoles)
+ info = $"{roleNames}";
+
+ playerGhostInfo.text = info;
+ playerGhostInfo.gameObject.SetActive(p.Visible);
+ if (meetingGhostInfo != null) meetingGhostInfo.text = MeetingHud.Instance.state == MeetingHud.VoteStates.Results ? "" : info;
}
}
+ public static void securityGuardSetTarget() {
+ if (SecurityGuard.securityGuard == null || SecurityGuard.securityGuard != PlayerControl.LocalPlayer || ShipStatus.Instance == null || ShipStatus.Instance.AllVents == null) return;
+
+ Vent target = null;
+ Vector2 truePosition = PlayerControl.LocalPlayer.GetTruePosition();
+ float closestDistance = float.MaxValue;
+ for (int i = 0; i < ShipStatus.Instance.AllVents.Length; i++) {
+ Vent vent = ShipStatus.Instance.AllVents[i];
+ if (vent.gameObject.name.StartsWith("JackInTheBoxVent_") || vent.gameObject.name.StartsWith("SealedVent_")) continue;
+ float distance = Vector2.Distance(vent.transform.position, truePosition);
+ if (distance <= vent.UsableDistance && distance < closestDistance) {
+ closestDistance = distance;
+ target = vent;
+ }
+ }
+ SecurityGuard.ventTarget = target;
+ }
+
public static void Postfix(PlayerControl __instance) {
if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return;
warlockSetTarget();
// Check for sidekick promotion on Jackal disconnect
sidekickCheckPromotion();
+ // SecurityGuard
+ securityGuardSetTarget();
}
}
}
}
}
}
- [HarmonyPatch(typeof(KillButtonManager), nameof(KillButtonManager.PerformKill))]
- class PerformKillPatch {
- public static bool Prefix(KillButtonManager __instance) {
- if (__instance.isActiveAndEnabled && __instance.CurrentTarget && !__instance.isCoolingDown && !PlayerControl.LocalPlayer.Data.IsDead && PlayerControl.LocalPlayer.CanMove) { // Among Us default checks
- if (Helpers.handleMurderAttempt(__instance.CurrentTarget)) { // Custom checks
- if (Child.child != null && PlayerControl.LocalPlayer == Child.child) { // Not checked by official servers
- MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1);
- writer.Write(PlayerControl.LocalPlayer.PlayerId);
- writer.Write(__instance.CurrentTarget.PlayerId);
- AmongUsClient.Instance.FinishRpcImmediately(writer);
- RPCProcedure.uncheckedMurderPlayer(PlayerControl.LocalPlayer.PlayerId, __instance.CurrentTarget.PlayerId);
- } else { // Checked by official servers
- PlayerControl.LocalPlayer.RpcMurderPlayer(__instance.CurrentTarget);
- }
- __instance.SetTarget(null);
+ [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcMurderPlayer))]
+ class RpcMurderPlayer {
+ public static bool Prefix([HarmonyArgument(0)]PlayerControl target) {
+ if (Helpers.handleMurderAttempt(target)) { // Custom checks
+ if (Child.child != null && PlayerControl.LocalPlayer == Child.child) { // Not checked by official servers
+ MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1);
+ writer.Write(PlayerControl.LocalPlayer.PlayerId);
+ writer.Write(target.PlayerId);
+ AmongUsClient.Instance.FinishRpcImmediately(writer);
+ RPCProcedure.uncheckedMurderPlayer(PlayerControl.LocalPlayer.PlayerId, target.PlayerId);
+ } else { // Checked by official servers
+ return true;
}
- }
+ }
return false;
}
}
Spy,
Trickster,
Cleaner,
- Warlock
+ Warlock,
+ SecurityGuard
}
enum CustomRPC
SetFutureShifted,
PlaceJackInTheBox,
LightsOut,
- WarlockCurseKill
+ WarlockCurseKill,
+ PlaceCamera,
+ SealVent
}
public static class RPCProcedure {
case RoleId.Warlock:
Warlock.warlock = player;
break;
+ case RoleId.SecurityGuard:
+ SecurityGuard.securityGuard = player;
+ break;
}
}
}
Snitch.snitch = oldShifter;
} else if (Spy.spy != null && Spy.spy == player) {
Spy.spy = oldShifter;
+ } else if (SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == player) {
+ SecurityGuard.securityGuard = oldShifter;
} else { // Crewmate
}
if (player == Snitch.snitch) Snitch.clearAndReload();
if (player == Swapper.swapper) Swapper.clearAndReload();
if (player == Spy.spy) Spy.clearAndReload();
+ if (player == SecurityGuard.securityGuard) SecurityGuard.clearAndReload();
// Impostor roles
if (player == Morphling.morphling) Morphling.clearAndReload();
}
}
}
+
+ public static void placeCamera(byte[] buff) {
+ var referenceCamera = UnityEngine.Object.FindObjectOfType<SurvCamera>();
+ if (referenceCamera == null) return; // Mira HQ
+
+ SecurityGuard.remainingScrews -= SecurityGuard.camPrice;
+ SecurityGuard.placedCameras++;
+
+ Vector3 position = Vector3.zero;
+ position.x = BitConverter.ToSingle(buff, 0*sizeof(float));
+ position.y = BitConverter.ToSingle(buff, 1*sizeof(float));
+
+ var camera = UnityEngine.Object.Instantiate<SurvCamera>(referenceCamera);
+ camera.transform.position = new Vector3(position.x, position.y, referenceCamera.transform.position.z - 1f);
+ camera.CamName = $"Security Guard Camera {SecurityGuard.placedCameras}";
+ if (PlayerControl.GameOptions.MapId == 2 || PlayerControl.GameOptions.MapId == 4) camera.transform.localRotation = new Quaternion(0, 0, 1, 1); // Polus and Airship
+ camera.gameObject.SetActive(false);
+ MapOptions.camerasToAdd.Add(camera);
+ }
+
+ public static void sealVent(int ventId) {
+ Vent vent = ShipStatus.Instance.AllVents.FirstOrDefault((x) => x != null && x.Id == ventId);
+ if (vent == null) return;
+
+ SecurityGuard.remainingScrews -= SecurityGuard.ventPrice;
+ MapOptions.ventsToSeal.Add(vent);
+ }
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))]
case (byte)CustomRPC.WarlockCurseKill:
RPCProcedure.warlockCurseKill(reader.ReadByte());
break;
+ case (byte)CustomRPC.PlaceCamera:
+ RPCProcedure.placeCamera(reader.ReadBytesAndSize());
+ break;
+ case (byte)CustomRPC.SealVent:
+ RPCProcedure.sealVent(reader.ReadPackedInt32());
+ break;
}
}
}
crewSettings.Add((byte)RoleId.Snitch, CustomOptionHolder.snitchSpawnRate.getSelection());
crewSettings.Add((byte)RoleId.Jackal, CustomOptionHolder.jackalSpawnRate.getSelection());
crewSettings.Add((byte)RoleId.Spy, CustomOptionHolder.spySpawnRate.getSelection());
+ crewSettings.Add((byte)RoleId.SecurityGuard, CustomOptionHolder.securityGuardSpawnRate.getSelection());
// Set special roles
if (impostors.Count >= 3 && maxImpostorRoles >= 3 && (rnd.Next(1, 101) <= CustomOptionHolder.mafiaSpawnRate.getSelection() * 10)) {
"Finish your tasks to find the <color=#FF1919FF>Impostors</color>",
"Finish your tasks"));
}
- if (Jackal.jackal != null && p == Jackal.jackal) {
+ if ((Jackal.jackal != null && p == Jackal.jackal) || (Jackal.formerJackals != null && Jackal.formerJackals.Contains(p))) {
infos.Add(new RoleInfo("Jackal",
Jackal.color,
"Kill all Crewmates and <color=#FF1919FF>Impostors</color> to win",
"Confuse the <color=#FF1919FF>Impostors</color>",
"Confuse the Impostors"));
}
+ if (SecurityGuard.securityGuard != null && p == SecurityGuard.securityGuard) {
+ infos.Add(new RoleInfo("Security Guard",
+ SecurityGuard.color,
+ "Seal vents and place cameras",
+ "Seal vents and place cameras"));
+ }
+
if (infos.Count == 0 && p.Data.IsImpostor) { // Just Impostor
infos.Add(new RoleInfo("Impostor",
Palette.ImpostorRed,
+++ /dev/null
-
-using HarmonyLib;
-using UnityEngine;
-using System.Collections.Generic;
-using Hazel;
-using System;
-using UnityEngine.UI;
-using UnityEngine.Events;
-
-namespace TheOtherRoles {
- [HarmonyPatch(typeof(OptionsMenuBehaviour), nameof(OptionsMenuBehaviour.Start))]
- public class OptionsMenuBehaviourStartPatch {
- public static ToggleButtonBehaviour streamerModeButton;
-
- private static void updateStreamerModeButton() {
- if (streamerModeButton == null || streamerModeButton.gameObject == null) return;
-
- bool on = TheOtherRolesPlugin.StreamerMode.Value;
- Color color = on ? new Color(0f, 1f, 0.16470589f, 1f) : Color.white;
- streamerModeButton.Background.color = color;
- streamerModeButton.Text.text = $"Streamer Mode: {(on ? "On" : "Off")}";
- if (streamerModeButton.Rollover) streamerModeButton.Rollover.ChangeOutColor(color);
- }
-
- public static void Postfix(OptionsMenuBehaviour __instance) {
- if ((streamerModeButton == null || streamerModeButton.gameObject == null) && __instance.CensorChatButton != null) {
- streamerModeButton = UnityEngine.Object.Instantiate(__instance.CensorChatButton, __instance.CensorChatButton.transform.parent);
- streamerModeButton.transform.localPosition += Vector3.down * 0.25f;
- __instance.CensorChatButton.transform.localPosition += Vector3.up * 0.25f;
- PassiveButton button = streamerModeButton.GetComponent<PassiveButton>();
- button.OnClick = new Button.ButtonClickedEvent();
- button.OnClick.AddListener((UnityEngine.Events.UnityAction)onClick);
- updateStreamerModeButton();
- }
-
- void onClick() {
- TheOtherRolesPlugin.StreamerMode.Value = !TheOtherRolesPlugin.StreamerMode.Value;
- updateStreamerModeButton();
- }
- }
- }
-
- [HarmonyPatch(typeof(TextBoxTMP), nameof(TextBoxTMP.SetText))]
- public static class HiddenTextPatch
- {
- private static void Postfix(TextBoxTMP __instance)
- {
- bool flag = TheOtherRolesPlugin.StreamerMode.Value && (__instance.name == "GameIdText" || __instance.name == "IpTextBox" || __instance.name == "PortTextBox");
- if (flag) __instance.outputText.text = new string('*', __instance.text.Length);
- }
- }
-}
\ No newline at end of file
Trickster.clearAndReload();
Cleaner.clearAndReload();
Warlock.clearAndReload();
+ SecurityGuard.clearAndReload();
}
public static class Jester {
}
public static void removeCurrentJackal() {
- if (jackal != null && !formerJackals.Contains(jackal)) formerJackals.Add(jackal);
+ if (!formerJackals.Contains(jackal)) formerJackals.Add(jackal);
jackal = null;
currentTarget = null;
fakeSidekick = null;
curseVictimTarget = null;
curseKillTarget = null;
}
-
}
+ public static class SecurityGuard {
+ public static PlayerControl securityGuard;
+ public static Color color = new Color(171/255f, 159f/255f, 55f/255f, 1f);
+
+ public static float cooldown = 30f;
+ public static int remainingScrews = 7;
+ public static int totalScrews = 7;
+ public static int ventPrice = 1;
+ public static int camPrice = 2;
+ public static int placedCameras = 0;
+ public static Vent ventTarget = null;
+
+ private static Sprite closeVentButtonSprite;
+ public static Sprite getCloseVentButtonSprite() {
+ if (closeVentButtonSprite) return closeVentButtonSprite;
+ closeVentButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.CloseVentButton.png", 115f);
+ return closeVentButtonSprite;
+ }
+
+ private static Sprite placeCameraButtonSprite;
+ public static Sprite getPlaceCameraButtonSprite() {
+ if (placeCameraButtonSprite) return placeCameraButtonSprite;
+ placeCameraButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.PlaceCameraButton.png", 115f);
+ return placeCameraButtonSprite;
+ }
+
+ private static Sprite animatedVentSealedSprite;
+ public static Sprite getAnimatedVentSealedSprite() {
+ if (animatedVentSealedSprite) return animatedVentSealedSprite;
+ animatedVentSealedSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.AnimatedVentSealed.png", 160f); // Change sprite and pixelPerUnit
+ return animatedVentSealedSprite;
+ }
+
+ private static Sprite staticVentSealedSprite;
+ public static Sprite getStaticVentSealedSprite() {
+ if (staticVentSealedSprite) return staticVentSealedSprite;
+ staticVentSealedSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.StaticVentSealed.png", 160f); // Change sprite and pixelPerUnit
+ return staticVentSealedSprite;
+ }
+
+ public static void clearAndReload() {
+ securityGuard = null;
+ ventTarget = null;
+ placedCameras = 0;
+ cooldown = CustomOptionHolder.securityGuardCooldown.getFloat();
+ totalScrews = remainingScrews = Mathf.RoundToInt(CustomOptionHolder.securityGuardTotalScrews.getFloat());
+ camPrice = Mathf.RoundToInt(CustomOptionHolder.securityGuardCamPrice.getFloat());
+ ventPrice = Mathf.RoundToInt(CustomOptionHolder.securityGuardVentPrice.getFloat());
+ }
+ }
}
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
- <Version>2.4.0</Version>
+ <Version>2.5.0</Version>
<Description>TheOtherRoles</Description>
<Authors>Eisbison</Authors>
</PropertyGroup>
}
else if (Spy.spy != null && Spy.spy == PlayerControl.LocalPlayer) {
setPlayerNameColor(Spy.spy, Spy.color);
+ } else if (SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == PlayerControl.LocalPlayer) {
+ setPlayerNameColor(SecurityGuard.securityGuard, SecurityGuard.color);
}
// No else if here, as a Lover of team Jackal needs the colors
}
// Crewmate roles with no changes: Child
- // Impostor roles with no changes: Morphling, Camouflager, Vampire, Godfather, Eraser, Janitor and Mafioso
+ // Impostor roles with no changes: Morphling, Camouflager, Vampire, Godfather, Eraser, Janitor, Cleaner, Warlock and Mafioso
}
static void setMafiaNameTags() {
// Reduce the usable distance to reduce the risk of gettings stuck while trying to jump into the box if it's placed near objects
usableDistance = 0.4f;
}
+ } else if (__instance.name.StartsWith("SealedVent_")) {
+ canUse = couldUse = false;
+ __result = num;
+ return false;
}
- couldUse = ((@object.inVent || roleCouldUse) && !pc.IsDead && (@object.CanMove || @object.inVent));
+ couldUse = (@object.inVent || roleCouldUse) && !pc.IsDead && (@object.CanMove || @object.inVent);
canUse = couldUse;
if (canUse)
{
}
}
-
[HarmonyPatch(typeof(TuneRadioMinigame), nameof(TuneRadioMinigame.Begin))]
class CommsMinigameBeginPatch {
static void Postfix(TuneRadioMinigame __instance) {
}
}
}
+
+ [HarmonyPatch]
+ class SurveillanceMinigamePatch {
+ private static int page = 0;
+ private static float timer = 0f;
+ [HarmonyPatch(typeof(SurveillanceMinigame), nameof(SurveillanceMinigame.Begin))]
+ class SurveillanceMinigameBeginPatch {
+ public static void Postfix(SurveillanceMinigame __instance) {
+ // Add securityGuard cameras
+ page = 0;
+ timer = 0;
+ if (ShipStatus.Instance.AllCameras.Length > 4 && __instance.FilteredRooms.Length > 0) {
+ __instance.textures = __instance.textures.ToList().Concat(new RenderTexture[ShipStatus.Instance.AllCameras.Length - 4]).ToArray();
+ for (int i = 4; i < ShipStatus.Instance.AllCameras.Length; i++) {
+ SurvCamera surv = ShipStatus.Instance.AllCameras[i];
+ Camera camera = UnityEngine.Object.Instantiate<Camera>(__instance.CameraPrefab);
+ camera.transform.SetParent(__instance.transform);
+ camera.transform.position = new Vector3(surv.transform.position.x, surv.transform.position.y, 8f);
+ camera.orthographicSize = 2.35f;
+ RenderTexture temporary = RenderTexture.GetTemporary(256, 256, 16, 0);
+ __instance.textures[i] = temporary;
+ camera.targetTexture = temporary;
+ }
+ }
+ }
+ }
+
+ [HarmonyPatch(typeof(SurveillanceMinigame), nameof(SurveillanceMinigame.Update))]
+ class SurveillanceMinigameUpdatePatch {
+
+ public static bool Prefix(SurveillanceMinigame __instance) {
+ // Update normal and securityGuard cameras
+ timer += Time.deltaTime;
+ int numberOfPages = Mathf.CeilToInt(ShipStatus.Instance.AllCameras.Length / 4f);
+
+ bool update = false;
+
+ if (timer > 3f || Input.GetKeyDown(KeyCode.RightArrow)) {
+ update = true;
+ timer = 0f;
+ page = (page + 1) % numberOfPages;
+ } else if (Input.GetKeyDown(KeyCode.LeftArrow)) {
+ page = (page + numberOfPages - 1) % numberOfPages;
+ update = true;
+ timer = 0f;
+ }
+
+ if ((__instance.isStatic || update) && !PlayerTask.PlayerHasTaskOfType<IHudOverrideTask>(PlayerControl.LocalPlayer)) {
+ __instance.isStatic = false;
+ for (int i = 0; i < __instance.ViewPorts.Length; i++) {
+ __instance.ViewPorts[i].sharedMaterial = __instance.DefaultMaterial;
+ __instance.SabText[i].gameObject.SetActive(false);
+ if (page * 4 + i < __instance.textures.Length)
+ __instance.ViewPorts[i].material.SetTexture("_MainTex", __instance.textures[page * 4 + i]);
+ else
+ __instance.ViewPorts[i].sharedMaterial = __instance.StaticMaterial;
+ }
+ } else if (!__instance.isStatic && PlayerTask.PlayerHasTaskOfType<HudOverrideTask>(PlayerControl.LocalPlayer)) {
+ __instance.isStatic = true;
+ for (int j = 0; j < __instance.ViewPorts.Length; j++) {
+ __instance.ViewPorts[j].sharedMaterial = __instance.StaticMaterial;
+ __instance.SabText[j].gameObject.SetActive(true);
+ }
+ }
+ return false;
+ }
+ }
+ }
}
\ No newline at end of file