From: thunderstorm584 <80977292+thunderstorm584@users.noreply.github.com> Date: Sat, 24 Apr 2021 18:21:16 +0000 (+0200) Subject: Version 2.4.0 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=25eeb9b84f751f77642c90832c76a1e5888efff7;p=rhonda%2Ftheotherroles.git Version 2.4.0 --- diff --git a/Banner.png b/Banner.png deleted file mode 100644 index 12c3612..0000000 Binary files a/Banner.png and /dev/null differ diff --git a/Images/Colors.png b/Images/Colors.png new file mode 100644 index 0000000..a61a660 Binary files /dev/null and b/Images/Colors.png differ diff --git a/Images/DarkerColors.png b/Images/DarkerColors.png deleted file mode 100644 index ff7d65a..0000000 Binary files a/Images/DarkerColors.png and /dev/null differ diff --git a/Images/LighterColors.png b/Images/LighterColors.png deleted file mode 100644 index 02d822a..0000000 Binary files a/Images/LighterColors.png and /dev/null differ diff --git a/README.md b/README.md index 7bf4eb0..da45dd8 100644 --- a/README.md +++ b/README.md @@ -8,45 +8,31 @@ The **The Other Roles**, is a mod for [Amongs Us](https://store.steampowered.com/app/945360/Among_Us) which adds many new roles, new [Settings](#settings), new [Custom Hats](#custom-hats) and support for [10+ Player Lobbies](#Custom-Servers-and-10+-Players) to the game. Even more roles are coming soon :) -- [Mafia](#mafia) - - Godfather - - Janitor - - Mafioso -- [Morphling](#morphling) -- [Camouflager](#camouflager) -- [Vampire](#vampire) -- [Eraser](#eraser) -- [Trickster](#trickster) -- [Cleaner](#cleaner) -- [Lovers](#lovers) - - Lover - - ImpLover -- [Child](#child) - - Child Impostor - - Child Crewmate -- [Jackal](#jackal) -- [Sidekick](#sidekick) -- [Jester](#jester) -- [Sheriff](#sheriff) -- [Seer](#seer) -- [Engineer](#engineer) -- [Detective](#detective) -- [Lighter](#lighter) -- [Medic](#medic) -- [Mayor](#mayor) -- [Hacker](#hacker) -- [Shifter](#shifter) -- [Time Master](#time-master) -- [Swapper](#swapper) -- [Tracker](#tracker) -- [Snitch](#snitch) -- [Spy](#spy) +| Impostors | Crewmates | Neutral | +|----------|-------------|-----------------| +| [Bad Child](#child) | [Child](#child) | [Jester](#jester) | +| [Camouflager](#camouflager) | [Detective](#detective) | [Jackal](#jackal) | +| [Cleaner](#cleaner) | [Engineer](#engineer) | [Sidekick](#sidekick) | +| [Eraser](#eraser) | [Hacker](#hacker) | | +| [ImpLover](#lovers) | [Lover](#lovers) | | +| [Godfather (Mafia)](#mafia) | [Lighter](#lighter) | | +| [Mafioso (Mafia)](#mafia) | [Mayor](#mayor) | | +| [Janitor (Mafia)](#mafia) | [Medic](#medic) | | +| [Morphling](#morphling) | [Seer](#seer) | | +| [Trickster](#trickster) | [Sheriff](#sheriff) | | +| [Vampire](#vampire) | [Shifter](#shifter) | | +| [Warlock](#warlock) | [Snitch](#snitch) | | +| | [Spy](#spy) | | +| | [Swapper](#swapper) | | +| | [Time Master](#time-master) | | +| | [Tracker](#tracker) | | The [Role Assignment](#role-assignment) sections explains how the roles are being distributed among the players. # Releases | Among Us - Version| Mod Version | Link | |----------|-------------|-----------------| +| 2021.4.14s| v2.4.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.4.0/TheOtherRoles.zip) | 2021.4.14s| v2.3.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.3.0/TheOtherRoles.zip) | 2021.4.14s| v2.2.2| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.2.2/TheOtherRoles.zip) | 2021.4.12s| v2.2.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.2.1/TheOtherRoles.zip) @@ -69,6 +55,18 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
Click to show the Changelog +**Version 2.4.0** +- **New Role:** [Warlock](#warlock) +- Added an option that allows ghosts to see the roles and remaining tasks of other players +- Added options to configure Morph & Camo duration +- Added hotkeys to the custom buttons (**Q** for the buttons that are on the same place as the kill button, **F** for the buttons that are above the kill button) +- Fixed an oversight which made StreamerMode only work as host +- Fixed an oversight which required Jackals to finish Tasks, after Sidekick was promoted +- Fixed an oversight which made Sidekicks not promote, if the Jackal disconnected +- Fixed a bug where the Trickster box was invisible +- Fixed a bug where changes to the server ip and port would only be applied if the game was restarted +- Added a way to get the 2 Hidden [Colors](#colors) + **Version 2.3.0** - **New Role:** [Cleaner](#cleaner) - Added 12 new [Colors](#colors) @@ -298,6 +296,7 @@ The mod adds a few new settings to Among Us (in addition to the role settings): - **Maximum Number Of Meetings:** You can set the maximum number of meetings that can be called in total (every player still has personal maximum of buttons, but if the maximum number of meetings is reached you can't use your meetings even if you have some left, Impostor and Jackal meetings also count) - **Allow Skips On Emergency Meetings:** If set to false, there will not be a skip button in emergency meetings. If a player does not vote, he'll vote himself. - **Hide Player Names:** Hides the names of all players that have role which is unknown to you. Team Lovers/Impostors/Jackal still see the names of their teammates. Impostors can alse see the name of the Spy and everyone can still see the age of the child. +- **Ghosts Can See Roles And Remaining Tasks:** If set to true, ghosts can see the role and the number of remaining tasks of each player. # Custom Hats ## Create and submit new hat designs @@ -309,7 +308,7 @@ Here are a few instructions, on how to create a custom hat: - This is the main texture of your hat. It will usually be rendered in front of the player, if you set the `behind` parameter it will be rendered behind the player. - The name of the texture needs to follow the pattern *hatname.png*, but you can also set some additional parameters in the file name by adding *_parametername* to the file name (before the *.png*). - Parameter `bounce`: This parameter determines whether the hat will bounce while you're walking or not. - - Parameter `adaptive`: If this parameter is set, the Among Us coloring shader will be applied (the shader that replaces some colors with the colors that your character is wearing in the game). The color red (#ff0000) will be replaced with the primary color of your player and the color blue (#0000ff) with the secondary color. Also other colors will be affected and changed, you can have a look at the texture of the [Crewmate Hat](https://static.wikia.nocookie.net/among-us-wiki/images/e/e0/Crewmate_hat.png/revision/latest?cb=20210106045508) to see how this feature should be used. + - Parameter `adaptive`: If this parameter is set, the Among Us coloring shader will be applied (the shader that replaces some colors with the colors that your character is wearing in the game). The color red (#ff0000) will be replaced with the primary color of your player and the color blue (#0000ff) with the secondary color. Also other colors will be affected and changed, you can have a look at the texture of the [Crewmate Hat](https://static.wikia.nocookie.net/among-us-wiki/images/e/e0/Crewmate_hat.png) to see how this feature should be used. - Parameter `behind`: If this parameter is set, the main texture will be rendered behind the player. - `Back texture (optional)`: - This texture will be rendered behind the player. @@ -322,19 +321,7 @@ Here are a few instructions, on how to create a custom hat: - **Submission:** If you got a hat design, you can submit it on our [Discord server](https://discord.gg/77RkMJHWsM). We'll look at all the hats and add all the good ones to the game. # Colors -### **Lighter Colors:** -- Pink, Orange, Yellow, White -- Cyan, Lime, Salmon, Mint -- Lavender, Peach, Wasabi, Hot Pink - -![](./Images/LighterColors.png) - -### **Darker Colors:** -- Red, Blue, Green, Black -- Purple, Brown, Bordeaux, Olive -- Turqoise, Nougat, Gray, Petrol - -![](./Images/DarkerColors.png) +drawing # Roles @@ -387,6 +374,7 @@ The Morphling is an Impostor which can additionally scan the appearance of a pla |----------|:-------------:| | Morphling Spawn Chance | - | Morphling Cooldown | - +| Morph Duration | Time the Morphling stays morphed ----------------------- ## Camouflager @@ -407,6 +395,7 @@ are hidden and all players have the same color.\ |----------|:-------------:| | Camouflager Spawn Chance | - | Camouflager Cooldown | - +| Camo Duration | Time players stay camouflaged ----------------------- ## Vampire @@ -418,6 +407,7 @@ If a victim is near a garlic, the "Bite Button" turns into the default "Kill But **NOTE:** - If a bitten player is still alive when a meeting is being called, he dies at the start of the meeting. - The cooldown is the same as the default kill cooldown (+ the kill delay if the Vampire bites the target). +- If there is a Vampire in the game, there can't be a Warlock ### Game Options | Name | Description | @@ -486,6 +476,28 @@ The Cleaner is an Impostor who has the ability to clean up dead bodies. \ | Cleaner Cooldown | Cooldown for cleaning dead bodies ----------------------- + +## Warlock +### **Team: Impostors** +The Warlock is an Impostor, that can curse another player (the cursed player doesn't get notified).\ +If the cursed person stands next to another player, the Warlock is able to kill that player (no matter how far away he is).\ +Performing a kill with the help of a cursed player, will lift the curse and it will result in the Warlock being unable to move for a configurable amount of time. +The Warlock can still perform normal kills, but the two buttons share the same cooldown. + +\ +**NOTE:** +- The Warlock can always kill his Impostor mates (and even himselfs) using the "cursed kill" +- If there is a Warlock in the game, there can't be a Vampire +- Performing a normal kill, doesn't lift the curse + +### Game Options +| Name | Description | +|----------|:-------------:| +| Warlock Spawn Chance | - +| Warlock Cooldown | Cooldown for using the Curse and curse Kill +| Warlock Root Time | Time the Warlock is rooted in place after killing using the curse +----------------------- + ## Lovers ### **Team: Either Crewmates or Impostors** There are always two Lovers which are linked together.\ diff --git a/Source Code/Buttons.cs b/Source Code/Buttons.cs index 881a9f5..b65493d 100644 --- a/Source Code/Buttons.cs +++ b/Source Code/Buttons.cs @@ -29,6 +29,7 @@ namespace TheOtherRoles private static CustomButton placeJackInTheBoxButton; private static CustomButton lightsOutButton; public static CustomButton cleanerCleanButton; + public static CustomButton warlockCurseButton; public static void setCustomButtonCooldowns() { engineerRepairButton.MaxTimer = 0f; @@ -51,6 +52,7 @@ namespace TheOtherRoles placeJackInTheBoxButton.MaxTimer = Trickster.placeBoxCooldown; lightsOutButton.MaxTimer = Trickster.lightsOutCooldown; cleanerCleanButton.MaxTimer = Cleaner.cooldown; + warlockCurseButton.MaxTimer = Warlock.cooldown; timeMasterShieldButton.EffectDuration = TimeMaster.shieldDuration; hackerButton.EffectDuration = Hacker.duration; @@ -113,7 +115,8 @@ namespace TheOtherRoles () => {}, Engineer.getButtonSprite(), new Vector3(-1.3f, 0, 0), - __instance + __instance, + KeyCode.Q ); // Janitor Clean @@ -148,7 +151,8 @@ namespace TheOtherRoles () => { janitorCleanButton.Timer = janitorCleanButton.MaxTimer; }, Janitor.getButtonSprite(), new Vector3(-1.3f, 0, 0), - __instance + __instance, + KeyCode.Q ); // Sheriff Kill @@ -185,7 +189,8 @@ namespace TheOtherRoles () => { sheriffKillButton.Timer = sheriffKillButton.MaxTimer;}, __instance.KillButton.renderer.sprite, new Vector3(-1.3f, 0, 0), - __instance + __instance, + KeyCode.Q ); // Time Master Rewind Time @@ -204,7 +209,8 @@ namespace TheOtherRoles }, TimeMaster.getButtonSprite(), new Vector3(-1.3f, 0, 0), - __instance, + __instance, + KeyCode.Q, true, TimeMaster.shieldDuration, () => { timeMasterShieldButton.Timer = timeMasterShieldButton.MaxTimer; } @@ -225,7 +231,8 @@ namespace TheOtherRoles () => {}, Medic.getButtonSprite(), new Vector3(-1.3f, 0, 0), - __instance + __instance, + KeyCode.Q ); @@ -242,7 +249,8 @@ namespace TheOtherRoles () => { }, Shifter.getButtonSprite(), new Vector3(-1.3f, 0, 0), - __instance + __instance, + KeyCode.Q ); // Morphling morph @@ -271,8 +279,9 @@ namespace TheOtherRoles Morphling.sampledTarget = null; }, Morphling.getSampleSprite(), - new Vector3(-1.3f, 1.3f, 0f), + new Vector3(-1.3f, 1.3f, 0f), __instance, + KeyCode.F, true, Morphling.duration, () => { @@ -298,8 +307,9 @@ namespace TheOtherRoles camouflagerButton.killButtonManager.TimerText.color = Palette.EnabledColor; }, Camouflager.getButtonSprite(), - new Vector3(-1.3f, 1.3f, 0f), + new Vector3(-1.3f, 1.3f, 0f), __instance, + KeyCode.F, true, Camouflager.duration, () => { camouflagerButton.Timer = camouflagerButton.MaxTimer; } @@ -320,6 +330,7 @@ namespace TheOtherRoles Hacker.getButtonSprite(), new Vector3(-1.3f, 0, 0), __instance, + KeyCode.Q, true, 0f, () => { @@ -340,7 +351,8 @@ namespace TheOtherRoles () => { }, Tracker.getButtonSprite(), new Vector3(-1.3f, 0, 0), - __instance + __instance, + KeyCode.Q ); vampireKillButton = new CustomButton( @@ -399,6 +411,7 @@ namespace TheOtherRoles Vampire.getButtonSprite(), new Vector3(-1.3f, 0, 0), __instance, + KeyCode.Q, false, 0f, () => { @@ -425,6 +438,7 @@ namespace TheOtherRoles Vampire.getGarlicButtonSprite(), Vector3.zero, __instance, + null, true ); @@ -442,7 +456,8 @@ namespace TheOtherRoles () => { jackalSidekickButton.Timer = jackalSidekickButton.MaxTimer;}, Jackal.getSidekickButtonSprite(), new Vector3(-1.3f, 1.3f, 0f), - __instance + __instance, + KeyCode.F ); // Jackal Kill @@ -462,7 +477,8 @@ namespace TheOtherRoles () => { jackalKillButton.Timer = jackalKillButton.MaxTimer;}, __instance.KillButton.renderer.sprite, new Vector3(-1.3f, 0, 0), - __instance + __instance, + KeyCode.Q ); // Sidekick Kill @@ -483,7 +499,8 @@ namespace TheOtherRoles () => { sidekickKillButton.Timer = sidekickKillButton.MaxTimer;}, __instance.KillButton.renderer.sprite, new Vector3(-1.3f, 0, 0), - __instance + __instance, + KeyCode.Q ); // Lighter light @@ -501,6 +518,7 @@ namespace TheOtherRoles Lighter.getButtonSprite(), new Vector3(-1.3f, 0f, 0f), __instance, + KeyCode.Q, true, Lighter.duration, () => { lighterButton.Timer = lighterButton.MaxTimer; } @@ -522,7 +540,8 @@ namespace TheOtherRoles () => { eraserButton.Timer = eraserButton.MaxTimer;}, Eraser.getButtonSprite(), new Vector3(-1.3f, 1.3f, 0f), - __instance + __instance, + KeyCode.F ); placeJackInTheBoxButton = new CustomButton( @@ -544,7 +563,8 @@ namespace TheOtherRoles () => { placeJackInTheBoxButton.Timer = placeJackInTheBoxButton.MaxTimer;}, Trickster.getPlaceBoxButtonSprite(), new Vector3(-1.3f, 1.3f, 0f), - __instance + __instance, + KeyCode.F ); lightsOutButton = new CustomButton( @@ -561,8 +581,9 @@ namespace TheOtherRoles lightsOutButton.killButtonManager.TimerText.color = Palette.EnabledColor; }, Trickster.getLightsOutButtonSprite(), - new Vector3(-1.3f, 1.3f, 0f), + new Vector3(-1.3f, 1.3f, 0f), __instance, + KeyCode.F, true, Trickster.lightsOutDuration, () => { lightsOutButton.Timer = lightsOutButton.MaxTimer; } @@ -599,9 +620,57 @@ namespace TheOtherRoles () => { cleanerCleanButton.Timer = cleanerCleanButton.MaxTimer; }, Cleaner.getButtonSprite(), new Vector3(-1.3f, 1.3f, 0f), - __instance + __instance, + KeyCode.F + ); + // Warlock curse + warlockCurseButton = new CustomButton( + () => { + if (Warlock.curseVictim == null) { + // Apply Curse + Warlock.curseVictim = Warlock.currentTarget; + warlockCurseButton.Sprite = Warlock.getCurseKillButtonSprite(); + warlockCurseButton.Timer = 1f; + } else if (Warlock.curseVictim != null && Warlock.curseVictimTarget != null && Helpers.handleMurderAttempt(Warlock.curseVictimTarget)) { + // Curse Kill + Warlock.curseKillTarget = Warlock.curseVictimTarget; + + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.WarlockCurseKill, Hazel.SendOption.Reliable, -1); + writer.Write(Warlock.curseKillTarget.PlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.warlockCurseKill(Warlock.curseKillTarget.PlayerId); + + Warlock.curseVictim = null; + Warlock.curseVictimTarget = null; + warlockCurseButton.Sprite = Warlock.getCurseButtonSprite(); + Warlock.warlock.killTimer = warlockCurseButton.Timer = warlockCurseButton.MaxTimer; + + if(Warlock.rootTime > 0) { + PlayerControl.LocalPlayer.moveable = false; + PlayerControl.LocalPlayer.NetTransform.Halt(); // Stop current movement so the warlock is not just running straight into the next object + HudManager.Instance.StartCoroutine(Effects.Lerp(Warlock.rootTime, new Action((p) => { // Delayed action + if (p == 1f) { + PlayerControl.LocalPlayer.moveable = true; + } + }))); + } + } + }, + () => { return Warlock.warlock != null && Warlock.warlock == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; }, + () => { return ((Warlock.curseVictim == null && Warlock.currentTarget != null) || (Warlock.curseVictim != null && Warlock.curseVictimTarget != null)) && PlayerControl.LocalPlayer.CanMove; }, + () => { + warlockCurseButton.Timer = warlockCurseButton.MaxTimer; + warlockCurseButton.Sprite = Warlock.getCurseButtonSprite(); + Warlock.curseVictim = null; + Warlock.curseVictimTarget = null; + }, + Warlock.getCurseButtonSprite(), + new Vector3(-1.3f, 1.3f, 0f), + __instance, + KeyCode.F ); + // Set the default (or settings from the previous game) timers/durations when spawning the buttons setCustomButtonCooldowns(); } diff --git a/Source Code/ChatCommands.cs b/Source Code/ChatCommands.cs new file mode 100644 index 0000000..fb7df17 --- /dev/null +++ b/Source Code/ChatCommands.cs @@ -0,0 +1,41 @@ +using System; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.IL2CPP; +using HarmonyLib; +using UnityEngine; +using UnhollowerBaseLib; + +namespace TheOtherRoles { + [HarmonyPatch] + public static class ChatCommands { + [HarmonyPatch(typeof(ChatController), nameof(ChatController.SendChat))] + private static class SendChatPatch { + static bool Prefix(ChatController __instance) { + string text = __instance.TextArea.text; + bool handled = false; + if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) { + if (text.ToLower().Equals("i'm a panda")) { + handled = true; + byte colorId = (byte)CustomColors.pickableColors; + SaveManager.BodyColor = (byte)colorId; + if (PlayerControl.LocalPlayer) + PlayerControl.LocalPlayer.CmdCheckColor(colorId); + } else if (text.ToLower().Equals("nightsky")) { + handled = true; + byte colorId = (byte)(CustomColors.pickableColors + 1); + SaveManager.BodyColor = (byte)colorId; + if (PlayerControl.LocalPlayer) + PlayerControl.LocalPlayer.CmdCheckColor(colorId); + } + } + if (handled) { + __instance.TextArea.Clear(); + __instance.quickChatMenu.ResetGlyphs(); + System.Console.WriteLine("Chat Clear"); + } + return !handled; + } + } + } +} \ No newline at end of file diff --git a/Source Code/CredentialsPatch.cs b/Source Code/CredentialsPatch.cs index 2e3012d..8a314f6 100644 --- a/Source Code/CredentialsPatch.cs +++ b/Source Code/CredentialsPatch.cs @@ -10,13 +10,19 @@ namespace TheOtherRoles { [HarmonyPatch] public static class CredentialsPatch { - [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] + public static string fullCredentials = +$@"TheOtherRoles v{TheOtherRolesPlugin.Major}.{TheOtherRolesPlugin.Minor}.{TheOtherRolesPlugin.Patch}: +- Modded by Eisbison, + Thunderstorm584 and EndOfFile +- Balanced with Dhalucard +- Button design by Bavari"; + [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] private static class VersionShowerPatch { static void Postfix(VersionShower __instance) { string spacer = new String('\n', 8); - string fullCredentials = $"TheOtherRoles v{TheOtherRolesPlugin.Major}.{TheOtherRolesPlugin.Minor}.{TheOtherRolesPlugin.Patch}:\n- Modded by Eisbison and Thunderstorm584\n- Balanced with Dhalucard\n- Button design by Bavari"; + if (__instance.text.text.Contains(spacer)) __instance.text.text = __instance.text.text + "\n" + fullCredentials; else @@ -30,11 +36,13 @@ namespace TheOtherRoles { static void Postfix(PingTracker __instance) { - __instance.transform.localPosition = new Vector3(__instance.transform.localPosition.x, 2.675f, __instance.transform.localPosition.z); - if (AmongUsClient.Instance.GameState == InnerNet.InnerNetClient.GameStates.Started) - __instance.text.text += "TheOtherRoles\nModded by Eisbison"; - else - __instance.text.text += "\nTheOtherRoles\nModded by Eisbison\nand Thunderstorm584\nBalanced with Dhalucard\nButton design by Bavari"; + if (AmongUsClient.Instance.GameState == InnerNet.InnerNetClient.GameStates.Started) { + __instance.text.text = "TheOtherRoles\nModded by Eisbison\n" + __instance.text.text; + __instance.transform.localPosition = new Vector3(2.583f, 2.675f, __instance.transform.localPosition.z); + } else { + __instance.text.text = $"{fullCredentials}\n{__instance.text.text}"; + __instance.transform.localPosition = new Vector3(1.25f, 2.675f, __instance.transform.localPosition.z); + } } } } diff --git a/Source Code/CrowdedPatches.cs b/Source Code/CrowdedPatches.cs index 8f787c2..a6eb2ad 100644 --- a/Source Code/CrowdedPatches.cs +++ b/Source Code/CrowdedPatches.cs @@ -140,7 +140,8 @@ namespace TheOtherRoles { [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdCheckColor))] public static class PlayerControlCmdCheckColorPatch { public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)]byte bodyColor) { - if (!Helpers.isCustomServer()) return true; + if (!Helpers.isCustomServer() || Palette.PlayerColors.Count >= PlayerControl.AllPlayerControls.Count) + return true; MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetUncheckedColor, Hazel.SendOption.Reliable, -1); writer.Write(bodyColor); @@ -154,7 +155,8 @@ namespace TheOtherRoles { [HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.UpdateAvailableColors))] public static class PlayerTabUpdateAvailableColorsPatch { public static bool Prefix(PlayerTab __instance) { - if (!Helpers.isCustomServer()) return true; + if (!Helpers.isCustomServer() || Palette.PlayerColors.Count >= PlayerControl.AllPlayerControls.Count) + return true; PlayerControl.SetPlayerMaterialColors(PlayerControl.LocalPlayer.Data.ColorId, __instance.DemoImage); for (int i = 0; i < Palette.PlayerColors.Length; i++) @@ -181,4 +183,4 @@ namespace TheOtherRoles { return false; } } -} \ No newline at end of file +} diff --git a/Source Code/CustomButton.cs b/Source Code/CustomButton.cs index bfef7c5..c18999b 100644 --- a/Source Code/CustomButton.cs +++ b/Source Code/CustomButton.cs @@ -22,8 +22,9 @@ public class CustomButton public Sprite Sprite; private HudManager hudManager; private bool mirror; + private KeyCode? hotkey; - public CustomButton(Action OnClick, Func HasButton, Func CouldUse, Action OnMeetingEnds, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, bool HasEffect, float EffectDuration, Action OnEffectEnds, bool mirror = false) + public CustomButton(Action OnClick, Func HasButton, Func CouldUse, Action OnMeetingEnds, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, KeyCode? hotkey, bool HasEffect, float EffectDuration, Action OnEffectEnds, bool mirror = false) { this.hudManager = hudManager; this.OnClick = OnClick; @@ -36,33 +37,35 @@ public class CustomButton this.OnEffectEnds = OnEffectEnds; this.Sprite = Sprite; this.mirror = mirror; + this.hotkey = hotkey; Timer = 16.2f; buttons.Add(this); killButtonManager = UnityEngine.Object.Instantiate(hudManager.KillButton, hudManager.transform); PassiveButton button = killButtonManager.GetComponent(); button.OnClick.RemoveAllListeners(); - button.OnClick.AddListener((UnityEngine.Events.UnityAction)listener); + button.OnClick.AddListener((UnityEngine.Events.UnityAction)onClickEvent); - void listener() + setActive(false); + } + + public CustomButton(Action OnClick, Func HasButton, Func CouldUse, Action OnMeetingEnds, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, KeyCode? hotkey, bool mirror = false) + : this(OnClick, HasButton, CouldUse, OnMeetingEnds, Sprite, PositionOffset, hudManager, hotkey, false, 0f, () => {}, mirror) { } + + void onClickEvent() + { + if (this.Timer < 0f && HasButton() && CouldUse()) { - if (this.Timer < 0f && HasButton() && CouldUse()) - { - killButtonManager.renderer.color = new Color(1f, 1f, 1f, 0.3f); - this.OnClick(); + killButtonManager.renderer.color = new Color(1f, 1f, 1f, 0.3f); + this.OnClick(); - if (this.HasEffect && !this.isEffectActive) { - this.Timer = this.EffectDuration; - killButtonManager.TimerText.color = new Color(0F, 0.8F, 0F); - this.isEffectActive = true; - } + if (this.HasEffect && !this.isEffectActive) { + this.Timer = this.EffectDuration; + killButtonManager.TimerText.color = new Color(0F, 0.8F, 0F); + this.isEffectActive = true; } } - setActive(false); } - public CustomButton(Action OnClick, Func HasButton, Func CouldUse, Action OnMeetingEnds, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, bool mirror = false) - : this(OnClick, HasButton, CouldUse, OnMeetingEnds, Sprite, PositionOffset, hudManager, false, 0f, () => {}, mirror) { } - public static void HudUpdate() { buttons.RemoveAll(item => item.killButtonManager == null); @@ -147,7 +150,7 @@ public class CustomButton if (Timer >= 0) { if (HasEffect && isEffectActive) Timer -= Time.deltaTime; - else if (!PlayerControl.LocalPlayer.inVent) + else if (!PlayerControl.LocalPlayer.inVent && PlayerControl.LocalPlayer.moveable) Timer -= Time.deltaTime; } @@ -158,5 +161,8 @@ public class CustomButton } killButtonManager.SetCoolDown(Timer, (HasEffect && isEffectActive) ? EffectDuration : MaxTimer); + + // Trigger OnClickEvent if the hotkey is being pressed down + if (hotkey.HasValue && Input.GetKeyDown(hotkey.Value)) onClickEvent(); } } \ No newline at end of file diff --git a/Source Code/CustomOptions.cs b/Source Code/CustomOptions.cs index 4df9aa7..a3bd672 100644 --- a/Source Code/CustomOptions.cs +++ b/Source Code/CustomOptions.cs @@ -123,11 +123,18 @@ namespace TheOtherRoles { public static CustomOption cleanerSpawnRate; public static CustomOption cleanerCooldown; + + public static CustomOption warlockSpawnRate; + public static CustomOption warlockCooldown; + public static CustomOption warlockRootTime; public static CustomOption maxNumberOfMeetings; public static CustomOption blockSkippingInEmergencyMeetings; public static CustomOption noVoteIsSelfVote; public static CustomOption hidePlayerNames; + public static CustomOption showGhostInfo; + + internal static Dictionary blockedRolePairings = new Dictionary(); public static string cs(Color c, string s) { return string.Format("{4}", ToByte(c.r), ToByte(c.g), ToByte(c.b), ToByte(c.a), s); @@ -174,6 +181,10 @@ namespace TheOtherRoles { cleanerSpawnRate = CustomOption.Create(260, cs(Cleaner.color, "Cleaner"), rates, null, true); cleanerCooldown = CustomOption.Create(261, "Cleaner Cooldown", 30f, 10f, 60f, 2.5f, cleanerSpawnRate); + warlockSpawnRate = CustomOption.Create(270, cs(Cleaner.color, "Warlock"), rates, null, true); + warlockCooldown = CustomOption.Create(271, "Warlock Cooldown", 30f, 10f, 60f, 2.5f, warlockSpawnRate); + warlockRootTime = CustomOption.Create(272, "Warlock Root Time", 5f, 0f, 15f, 1f, warlockSpawnRate); + childSpawnRate = CustomOption.Create(180, cs(Child.color, "Child"), rates, null, true); childGrowingUpDuration = CustomOption.Create(181, "Child Growing Up Duration", 400f, 100f, 1500f, 100f, childSpawnRate); @@ -256,6 +267,11 @@ namespace TheOtherRoles { 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}); + } } @@ -502,9 +518,9 @@ namespace TheOtherRoles { var hudString = sb.ToString(); int defaultSettingsLines = 19; - int roleSettingsLines = defaultSettingsLines + 28; + int roleSettingsLines = defaultSettingsLines + 29; int detailedSettingsP1 = roleSettingsLines + 34; - int detailedSettingsP2 = detailedSettingsP1 + 34; + int detailedSettingsP2 = detailedSettingsP1 + 36; int end1 = hudString.TakeWhile(c => (defaultSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count(); int end2 = hudString.TakeWhile(c => (roleSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count(); int end3 = hudString.TakeWhile(c => (detailedSettingsP1 -= (c == '\n' ? 1 : 0)) > 0).Count(); @@ -521,7 +537,7 @@ namespace TheOtherRoles { gap = 4; index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count(); hudString = hudString.Insert(index, "\n"); - gap = 12; + gap = 13; index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count(); hudString = hudString.Insert(index + 1, "\n"); gap = 16; diff --git a/Source Code/FakeTasksForEveryone.cs b/Source Code/FakeTasksForEveryone.cs deleted file mode 100644 index 42e52b7..0000000 --- a/Source Code/FakeTasksForEveryone.cs +++ /dev/null @@ -1,46 +0,0 @@ -using HarmonyLib; -using static TheOtherRoles.TheOtherRoles; -using System.Collections; -using System.Collections.Generic; - -namespace TheOtherRoles { - [HarmonyPatch] - public static class FakeTasksForEveryone { - [HarmonyPatch(typeof(GameData), nameof(GameData.RecomputeTaskCounts))] - private static class GameDataRecomputeTaskCountsPatch { - private static bool Prefix(GameData __instance) { - __instance.TotalTasks = 0; - __instance.CompletedTasks = 0; - for (int i = 0; i < __instance.AllPlayers.Count; i++) { - GameData.PlayerInfo playerInfo = __instance.AllPlayers[i]; // PlayerInfo - if (!playerInfo.Disconnected && playerInfo.Tasks != null && // Disconnected | // Tasks - playerInfo.Object && // Object -> PlayerControl - (PlayerControl.GameOptions.GhostsDoTasks || !playerInfo.IsDead) && // GhostsDoTasks | IsDead - !playerInfo.IsImpostor && // IsImpostor - !Helpers.hasFakeTasks(playerInfo.Object) - ) { - - for (int j = 0; j < playerInfo.Tasks.Count; j++) { - __instance.TotalTasks++; - if (playerInfo.Tasks[j].Complete) { // Complete - __instance.CompletedTasks++; - } - } - } - } - return false; - } - } - - [HarmonyPatch(typeof(Console), nameof(Console.CanUse))] - private static class ConsoleCanUsePatch { - private static bool Prefix(ref float __result, Console __instance, [HarmonyArgument(0)] GameData.PlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { - canUse = couldUse = false; - if (__instance.AllowImpostor) return true; - if (!Helpers.hasFakeTasks(pc.Object)) return true; - __result = float.MaxValue; - return false; - } - } - } -} diff --git a/Source Code/GameStartManagerPatch.cs b/Source Code/GameStartManagerPatch.cs index 42f289e..d01d123 100644 --- a/Source Code/GameStartManagerPatch.cs +++ b/Source Code/GameStartManagerPatch.cs @@ -54,7 +54,7 @@ namespace TheOtherRoles { if (AmongUsClient.Instance.AmHost) { bool blockStart = false; string message = ""; - foreach (InnerNet.ClientData client in AmongUsClient.Instance.allClients) { + foreach (InnerNet.ClientData client in AmongUsClient.Instance.allClients.ToArray()) { if (client.Character == null) continue; var dummyComponent = client.Character.GetComponent(); if (dummyComponent != null && dummyComponent.enabled) @@ -77,6 +77,9 @@ namespace TheOtherRoles { } } + // Lobby code replacement + __instance.GameRoomName.text = TheOtherRolesPlugin.StreamerMode.Value ? $"{TheOtherRolesPlugin.StreamerModeReplacementText.Value}" : lobbyCodeText; + // Lobby timer if (!AmongUsClient.Instance.AmHost || !GameData.Instance) return; // Not host or no instance @@ -90,8 +93,6 @@ namespace TheOtherRoles { __instance.PlayerCounter.text = currentText + suffix; __instance.PlayerCounter.autoSizeTextContainer = true; - // Lobby code replacement - __instance.GameRoomName.text = TheOtherRolesPlugin.StreamerMode.Value ? $"{TheOtherRolesPlugin.StreamerModeReplacementText.Value}" : lobbyCodeText; } } diff --git a/Source Code/Helpers.cs b/Source Code/Helpers.cs index feb431a..5706551 100644 --- a/Source Code/Helpers.cs +++ b/Source Code/Helpers.cs @@ -175,10 +175,19 @@ namespace TheOtherRoles { return n != StringNames.ServerNA && n != StringNames.ServerEU && n != StringNames.ServerAS; } - public static bool hasFakeTasks(PlayerControl player) { + public static bool hasFakeTasks(this PlayerControl player) { return (player == Jester.jester || player == Jackal.jackal || player == Sidekick.sidekick); } - + + public static void clearAllTasks(this PlayerControl player) { + for (int i = 0; i < player.myTasks.Count; i++) { + PlayerTask playerTask = player.myTasks[i]; + playerTask.OnRemove(); + UnityEngine.Object.Destroy(playerTask.gameObject); + } + player.myTasks.Clear(); + } + public static string cs(Color c, string s) { return string.Format("{4}", ToByte(c.r), ToByte(c.g), ToByte(c.b), ToByte(c.a), s); } diff --git a/Source Code/JackInTheBox.cs b/Source Code/JackInTheBox.cs index 31ff5f0..72b2cfa 100644 --- a/Source Code/JackInTheBox.cs +++ b/Source Code/JackInTheBox.cs @@ -10,19 +10,14 @@ namespace TheOtherRoles{ public static System.Collections.Generic.List AllJackInTheBoxes = new System.Collections.Generic.List(); public static int JackInTheBoxLimit = 3; public static bool boxesConvertedToVents = false; - private static List boxAnimationSprites; - - public static Sprite getBoxSprite() { - List s = getBoxAnimationSprites(); - if (s.Count > 0) return s.First(); - return null; - } - - public static List getBoxAnimationSprites() { - if (boxAnimationSprites != null) return boxAnimationSprites; - boxAnimationSprites = new List(); - for (int i = 1; i < 19; i++) boxAnimationSprites.Add(Helpers.loadSpriteFromResources($"TheOtherRoles.Resources.TricksterAnimation.trickster_box_00{i:00}.png", 175f)); - return boxAnimationSprites; + public static Sprite[] boxAnimationSprites = new Sprite[18]; + + public static Sprite getBoxAnimationSprite(int index) { + if (boxAnimationSprites == null || boxAnimationSprites.Length == 0) return null; + index = Mathf.Clamp(index, 0, boxAnimationSprites.Length - 1); + if (boxAnimationSprites[index] == null) + boxAnimationSprites[index] = (Helpers.loadSpriteFromResources($"TheOtherRoles.Resources.TricksterAnimation.trickster_box_00{(index + 1):00}.png", 175f)); + return boxAnimationSprites[index]; } public static void startAnimation(int ventId) { @@ -31,11 +26,9 @@ namespace TheOtherRoles{ Vent vent = box.vent; HudManager.Instance.StartCoroutine(Effects.Lerp(0.6f, new Action((p) => { - var sprites = getBoxAnimationSprites(); - if (vent != null && vent.myRend != null && sprites != null && sprites.Count > 0) { - int index = (int)(p * sprites.Count); - vent.myRend.sprite = sprites[Mathf.Clamp(index, 0, sprites.Count-1)]; - if (p == 1f) vent.myRend.sprite = getBoxSprite(); + if (vent != null && vent.myRend != null) { + vent.myRend.sprite = getBoxAnimationSprite((int)(p * boxAnimationSprites.Length)); + if (p == 1f) vent.myRend.sprite = getBoxAnimationSprite(0); } }))); } @@ -50,7 +43,7 @@ namespace TheOtherRoles{ // Create the marker gameObject.transform.position = position; var boxRenderer = gameObject.AddComponent(); - boxRenderer.sprite = getBoxSprite(); + boxRenderer.sprite = getBoxAnimationSprite(0); // Create the vent var referenceVent = UnityEngine.Object.FindObjectOfType(); @@ -65,7 +58,7 @@ namespace TheOtherRoles{ vent.GetComponent()?.Stop(); vent.Id = ShipStatus.Instance.AllVents.Select(x => x.Id).Max() + 1; // Make sure we have a unique id var ventRenderer = vent.GetComponent(); - ventRenderer.sprite = getBoxSprite(); + ventRenderer.sprite = getBoxAnimationSprite(0); vent.myRend = ventRenderer; var allVentsList = ShipStatus.Instance.AllVents.ToList(); allVentsList.Add(vent); diff --git a/Source Code/Main.cs b/Source Code/Main.cs index 881ee6e..a6681c4 100644 --- a/Source Code/Main.cs +++ b/Source Code/Main.cs @@ -19,9 +19,9 @@ namespace TheOtherRoles public class TheOtherRolesPlugin : BasePlugin { public const string Id = "me.eisbison.theotherroles"; - public const string Version = "2.3.0"; + public const string Version = "2.4.0"; public const byte Major = 2; - public const byte Minor = 3; + public const byte Minor = 4; public const byte Patch = 0; public Harmony Harmony { get; } = new Harmony(Id); @@ -36,7 +36,17 @@ namespace TheOtherRoles public static ConfigEntry Ip { get; set; } public static ConfigEntry Port { get; set; } - public static DnsRegionInfo CustomRegion; + public static IRegionInfo[] defaultRegions; + + public static void UpdateRegions() { + ServerManager serverManager = DestroyableSingleton.Instance; + IRegionInfo[] regions = defaultRegions; + + var CustomRegion = new DnsRegionInfo(Ip.Value, "Custom", StringNames.NoTranslation, Ip.Value, Port.Value); + regions = regions.Concat(new IRegionInfo[] { CustomRegion.Cast() }).ToArray(); + ServerManager.DefaultRegions = regions; + serverManager.AvailableRegions = regions; + } public override void Load() { DebugMode = Config.Bind("Custom", "Enable Debug Mode", false); @@ -47,15 +57,9 @@ namespace TheOtherRoles Ip = Config.Bind("Custom", "Custom Server IP", "127.0.0.1"); Port = Config.Bind("Custom", "Custom Server Port", (ushort)22023); + defaultRegions = ServerManager.DefaultRegions; - CustomRegion = new DnsRegionInfo(Ip.Value, "Custom", StringNames.NoTranslation, Ip.Value, Port.Value); - ServerManager serverManager = DestroyableSingleton.Instance; - IRegionInfo[] regions = ServerManager.DefaultRegions; - - regions = regions.Concat(new IRegionInfo[] { CustomRegion.Cast() }).ToArray(); - ServerManager.DefaultRegions = regions; - serverManager.AvailableRegions = regions; - serverManager.SaveServers(); + UpdateRegions(); GameOptionsData.RecommendedImpostors = GameOptionsData.MaxImpostors = Enumerable.Repeat(3, 16).ToArray(); // Max Imp = Recommended Imp = 3 GameOptionsData.MinPlayers = Enumerable.Repeat(4, 15).ToArray(); // Min Players = 4 diff --git a/Source Code/MapOptions.cs b/Source Code/MapOptions.cs index 35ac986..e324c96 100644 --- a/Source Code/MapOptions.cs +++ b/Source Code/MapOptions.cs @@ -11,6 +11,7 @@ namespace TheOtherRoles{ public static bool blockSkippingInEmergencyMeetings = false; public static bool noVoteIsSelfVote = false; public static bool hidePlayerNames = false; + public static bool showGhostInfo = true; // Updating values public static int meetingsCount = 0; @@ -22,6 +23,7 @@ namespace TheOtherRoles{ blockSkippingInEmergencyMeetings = CustomOptionHolder.blockSkippingInEmergencyMeetings.getBool(); noVoteIsSelfVote = CustomOptionHolder.noVoteIsSelfVote.getBool(); hidePlayerNames = CustomOptionHolder.hidePlayerNames.getBool(); + showGhostInfo = CustomOptionHolder.showGhostInfo.getBool(); } } } \ No newline at end of file diff --git a/Source Code/MeetingPatch.cs b/Source Code/MeetingPatch.cs index daec581..d6dbfbb 100644 --- a/Source Code/MeetingPatch.cs +++ b/Source Code/MeetingPatch.cs @@ -302,7 +302,7 @@ namespace TheOtherRoles // Update positions area.Megaphone.transform.localPosition += Vector3.left * 0.1f; - area.NameText.transform.localPosition = new Vector3(1.2f, 0.2f, area.NameText.transform.localPosition.z); + area.NameText.transform.localPosition += new Vector3(0.25f, 0.043f, 0f); area.transform.localPosition = new Vector3(-3.63f + 2.43f * col, 1.5f - 0.76f * row, -0.9f - 0.01f * row); } } @@ -528,6 +528,8 @@ namespace TheOtherRoles __result = ExileController.Instance.exiled.PlayerName + " was The Trickster."; else if (Cleaner.cleaner != null && ExileController.Instance.exiled.Object.PlayerId == Cleaner.cleaner.PlayerId) __result = ExileController.Instance.exiled.PlayerName + " was The Cleaner."; + else if (Warlock.warlock != null && ExileController.Instance.exiled.Object.PlayerId == Warlock.warlock.PlayerId) + __result = ExileController.Instance.exiled.PlayerName + " was The Warlock."; } // Hide number of remaining impostors on Jester win diff --git a/Source Code/PlayerControlPatch.cs b/Source Code/PlayerControlPatch.cs index fa14fde..841a72a 100644 --- a/Source Code/PlayerControlPatch.cs +++ b/Source Code/PlayerControlPatch.cs @@ -15,17 +15,18 @@ namespace TheOtherRoles { { // Helpers - static PlayerControl setTarget(bool onlyCrewmates = false, bool targetPlayersInVents = false, List untargetablePlayers = null) { + static PlayerControl setTarget(bool onlyCrewmates = false, bool targetPlayersInVents = false, List untargetablePlayers = null, PlayerControl targetingPlayer = null) { PlayerControl result = null; float num = GameOptionsData.KillDistances[Mathf.Clamp(PlayerControl.GameOptions.KillDistance, 0, 2)]; if (!ShipStatus.Instance) return result; + if (targetingPlayer == null) targetingPlayer = PlayerControl.LocalPlayer; - Vector2 truePosition = PlayerControl.LocalPlayer.GetTruePosition(); + Vector2 truePosition = targetingPlayer.GetTruePosition(); Il2CppSystem.Collections.Generic.List allPlayers = GameData.Instance.AllPlayers; for (int i = 0; i < allPlayers.Count; i++) { GameData.PlayerInfo playerInfo = allPlayers[i]; - if (!playerInfo.Disconnected && playerInfo.PlayerId != PlayerControl.LocalPlayer.PlayerId && !playerInfo.IsDead && (!onlyCrewmates || !playerInfo.IsImpostor)) + if (!playerInfo.Disconnected && playerInfo.PlayerId != targetingPlayer.PlayerId && !playerInfo.IsDead && (!onlyCrewmates || !playerInfo.IsImpostor)) { PlayerControl @object = playerInfo.Object; if(untargetablePlayers != null && untargetablePlayers.Any(x => x == @object)) { @@ -61,8 +62,9 @@ namespace TheOtherRoles { foreach (PlayerControl target in PlayerControl.AllPlayerControls) { if (target == null || target.myRend == null) continue; + bool isMorphedMorphling = target == Morphling.morphling && Morphling.morphTarget != null && Morphling.morphTimer > 0f; bool hasVisibleShield = false; - if (Camouflager.camouflageTimer <= 0f && Medic.shielded != null && (target == Medic.shielded || (target == Morphling.morphling && Morphling.morphTarget == Medic.shielded && Morphling.morphTimer > 0f))) { + if (Camouflager.camouflageTimer <= 0f && Medic.shielded != null && ((target == Medic.shielded && !isMorphedMorphling) || (isMorphedMorphling && Morphling.morphTarget == Medic.shielded))) { hasVisibleShield = Medic.showShielded == 0 // Everyone || (Medic.showShielded == 1 && (PlayerControl.LocalPlayer == Medic.shielded || PlayerControl.LocalPlayer == Medic.medic)) // Shielded + Medic || (Medic.showShielded == 2 && PlayerControl.LocalPlayer == Medic.medic); // Medic only @@ -117,7 +119,7 @@ namespace TheOtherRoles { static void medicSetTarget() { if (Medic.medic == null || Medic.medic != PlayerControl.LocalPlayer) return; Medic.currentTarget = setTarget(); - if (!Medic.usedShield) setPlayerOutline(Medic.currentTarget, Medic.color); + if (!Medic.usedShield) setPlayerOutline(Medic.currentTarget, Medic.shieldedColor); } static void shifterSetTarget() { @@ -195,7 +197,7 @@ namespace TheOtherRoles { } if(Child.child != null && !Child.isGrownUp()) untargetablePlayers.Add(Child.child); // Exclude Jackal from targeting the Child unless it has grown up Jackal.currentTarget = setTarget(untargetablePlayers : untargetablePlayers); - setPlayerOutline(Jackal.currentTarget, Jackal.color); + setPlayerOutline(Jackal.currentTarget, Palette.ImpostorRed); } static void sidekickSetTarget() { @@ -204,7 +206,18 @@ namespace TheOtherRoles { if(Jackal.jackal != null) untargetablePlayers.Add(Jackal.jackal); if(Child.child != null && !Child.isGrownUp()) untargetablePlayers.Add(Child.child); // Exclude Sidekick from targeting the Child unless it has grown up Sidekick.currentTarget = setTarget(untargetablePlayers : untargetablePlayers); - if (Sidekick.canKill) setPlayerOutline(Sidekick.currentTarget, Sidekick.color); + if (Sidekick.canKill) setPlayerOutline(Sidekick.currentTarget, Palette.ImpostorRed); + } + + static void sidekickCheckPromotion() { + // If LocalPlayer is Sidekick, the Jackal is disconnected and Sidekick promotion is enabled, then trigger promotion + if (Sidekick.sidekick == null || Sidekick.sidekick != PlayerControl.LocalPlayer) return; + if (Sidekick.sidekick.Data.IsDead == true || !Sidekick.promotesToJackal) return; + if (Jackal.jackal == null || Jackal.jackal?.Data?.Disconnected == true) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SidekickPromotes, Hazel.SendOption.Reliable, -1); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.sidekickPromotes(); + } } static void eraserSetTarget() { @@ -253,6 +266,21 @@ namespace TheOtherRoles { HudManager.Instance.KillButton.SetTarget(target); // Includes setPlayerOutline(target, Palette.ImpstorRed); } + static void warlockSetTarget() { + if (Warlock.warlock == null || Warlock.warlock != PlayerControl.LocalPlayer) return; + if (Warlock.curseVictim != null && (Warlock.curseVictim.Data.Disconnected || Warlock.curseVictim.Data.IsDead)) { + // If the cursed victim is disconnected or dead reset the curse so a new curse can be applied + Warlock.resetCurse(); + } + if (Warlock.curseVictim == null) { + Warlock.currentTarget = setTarget(); + setPlayerOutline(Warlock.currentTarget, Warlock.color); + } else { + Warlock.curseVictimTarget = setTarget(targetingPlayer: Warlock.curseVictim); + setPlayerOutline(Warlock.curseVictimTarget, Warlock.color); + } + } + static void trackerUpdate() { if (Tracker.arrow?.arrow == null) return; @@ -309,6 +337,39 @@ namespace TheOtherRoles { } } + public static void updateGhostInfo() { + if (!MapOptions.showGhostInfo) return; + + foreach (PlayerControl p in PlayerControl.AllPlayerControls) { + if (p != PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead) continue; + + Transform playerGhostInfoTransform = p.transform.FindChild("GhostInfo"); + TMPro.TextMeshPro playerGhostInfo = playerGhostInfoTransform != null ? playerGhostInfoTransform.GetComponent() : null; + if (playerGhostInfo == null) { + playerGhostInfo = UnityEngine.Object.Instantiate(p.nameText, p.nameText.transform.parent); + playerGhostInfo.transform.localPosition += Vector3.up * 0.25f; + playerGhostInfo.fontSize *= 0.75f; + playerGhostInfo.gameObject.name = "GhostInfo"; + } + + PlayerVoteArea playerVoteArea = MeetingHud.Instance?.playerStates?.FirstOrDefault(x => x.TargetPlayerId == p.PlayerId); + Transform meetingGhostInfoTransform = playerVoteArea != null ? playerVoteArea.transform.FindChild("GhostInfo") : null; + TMPro.TextMeshPro meetingGhostInfo = meetingGhostInfoTransform != null ? meetingGhostInfoTransform.GetComponent() : null; + if (meetingGhostInfo == null && playerVoteArea != null) { + meetingGhostInfo = UnityEngine.Object.Instantiate(playerVoteArea.NameText, playerVoteArea.NameText.transform.parent); + meetingGhostInfo.transform.localPosition += Vector3.down * (MeetingHud.Instance.playerStates.Length > 10 ? 0.4f : 0.25f); + meetingGhostInfo.fontSize *= 0.75f; + meetingGhostInfo.gameObject.name = "GhostInfo"; + } + + 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 ? $"({tasksCompleted}/{tasksTotal})" : ""; + playerGhostInfo.text = $"{roleNames} {taskInfo}".Trim(); + if (meetingGhostInfo != null) meetingGhostInfo.text = MeetingHud.Instance.state == MeetingHud.VoteStates.Results ? "" : $"{roleNames} {taskInfo}".Trim(); + } + } + public static void Postfix(PlayerControl __instance) { if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return; @@ -322,6 +383,9 @@ namespace TheOtherRoles { // Update Role Description Helpers.refreshRoleDescription(__instance); + // Update Ghost Info + updateGhostInfo(); + // Time Master bendTimeUpdate(); // Morphling @@ -351,6 +415,10 @@ namespace TheOtherRoles { sidekickSetTarget(); // Impostor impostorSetTarget(); + // Warlock + warlockSetTarget(); + // Check for sidekick promotion on Jackal disconnect + sidekickCheckPromotion(); } } } @@ -475,6 +543,10 @@ namespace TheOtherRoles { if (resetToCrewmate) __instance.Data.IsImpostor = false; if (resetToDead) __instance.Data.IsDead = true; + // Remove fake tasks when player dies + if (target.hasFakeTasks()) + target.clearAllTasks(); + // Lover suicide trigger on murder if ((Lovers.lover1 != null && target == Lovers.lover1) || (Lovers.lover2 != null && target == Lovers.lover2)) { PlayerControl otherLover = target == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1; @@ -495,7 +567,14 @@ namespace TheOtherRoles { // Cleaner Button Sync if (Cleaner.cleaner != null && PlayerControl.LocalPlayer == Cleaner.cleaner && __instance == Cleaner.cleaner && HudManagerStartPatch.cleanerCleanButton != null) - HudManagerStartPatch.cleanerCleanButton.Timer = Cleaner.cleaner.killTimer; + HudManagerStartPatch.cleanerCleanButton.Timer = Cleaner.cleaner.killTimer; + + // Warlock Button Sync + if (Warlock.warlock != null && PlayerControl.LocalPlayer == Warlock.warlock && __instance == Warlock.warlock && HudManagerStartPatch.warlockCurseButton != null) { + if(Warlock.warlock.killTimer > HudManagerStartPatch.warlockCurseButton.Timer) { + HudManagerStartPatch.warlockCurseButton.Timer = Warlock.warlock.killTimer; + } + } // Seer show flash and add dead player position if (Seer.seer != null && PlayerControl.LocalPlayer == Seer.seer && !Seer.seer.Data.IsDead && Seer.seer != target && Seer.mode <= 1) { @@ -540,6 +619,11 @@ namespace TheOtherRoles { public static void Prefix(KillAnimation __instance, [HarmonyArgument(0)]ref PlayerControl source, [HarmonyArgument(1)]ref PlayerControl target) { if (Vampire.vampire != null && Vampire.vampire == source && Vampire.bitten != null && Vampire.bitten == target) source = target; + + if (Warlock.warlock != null && Warlock.warlock == source && Warlock.curseKillTarget != null && Warlock.curseKillTarget == target) { + source = target; + Warlock.curseKillTarget = null; // Reset here + } } } @@ -563,6 +647,10 @@ namespace TheOtherRoles { DeadPlayer deadPlayer = new DeadPlayer(__instance, DateTime.UtcNow, DeathReason.Exile, null); GameHistory.deadPlayers.Add(deadPlayer); + // Remove fake tasks when player dies + if (__instance.hasFakeTasks()) + __instance.clearAllTasks(); + // Lover suicide trigger on exile if ((Lovers.lover1 != null && __instance == Lovers.lover1) || (Lovers.lover2 != null && __instance == Lovers.lover2)) { PlayerControl otherLover = __instance == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1; diff --git a/Source Code/RPC.cs b/Source Code/RPC.cs index e6decb5..87f4b34 100644 --- a/Source Code/RPC.cs +++ b/Source Code/RPC.cs @@ -40,7 +40,8 @@ namespace TheOtherRoles Eraser, Spy, Trickster, - Cleaner + Cleaner, + Warlock } enum CustomRPC @@ -83,7 +84,8 @@ namespace TheOtherRoles SetFutureErased, SetFutureShifted, PlaceJackInTheBox, - LightsOut + LightsOut, + WarlockCurseKill } public static class RPCProcedure { @@ -207,6 +209,9 @@ namespace TheOtherRoles case RoleId.Cleaner: Cleaner.cleaner = player; break; + case RoleId.Warlock: + Warlock.warlock = player; + break; } } } @@ -535,6 +540,7 @@ namespace TheOtherRoles if (player == Eraser.eraser) Eraser.clearAndReload(); if (player == Trickster.trickster) Trickster.clearAndReload(); if (player == Cleaner.cleaner) Cleaner.clearAndReload(); + if (player == Warlock.warlock) Warlock.clearAndReload(); // Other roles if (player == Jester.jester) Jester.clearAndReload(); @@ -575,6 +581,16 @@ namespace TheOtherRoles new CustomMessage("Lights are out", Trickster.lightsOutDuration); } } + + public static void warlockCurseKill(byte targetId) { + foreach (PlayerControl player in PlayerControl.AllPlayerControls) { + if (player.PlayerId == targetId) { + Warlock.curseKillTarget = player; + Warlock.warlock.MurderPlayer(player); + return; + } + } + } } [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] @@ -711,6 +727,9 @@ namespace TheOtherRoles case (byte)CustomRPC.LightsOut: RPCProcedure.lightsOut(); break; + case (byte)CustomRPC.WarlockCurseKill: + RPCProcedure.warlockCurseKill(reader.ReadByte()); + break; } } } diff --git a/Source Code/RegionMenuPatch.cs b/Source Code/RegionMenuPatch.cs index 1c14709..e5ceeeb 100644 --- a/Source Code/RegionMenuPatch.cs +++ b/Source Code/RegionMenuPatch.cs @@ -48,6 +48,7 @@ namespace TheOtherRoles { ipField.transform.localPosition = new Vector3(0, -1f, -100f); ipField.characterLimit = 30; ipField.AllowSymbols = true; + ipField.ForceUppercase = false; ipField.SetText(TheOtherRolesPlugin.Ip.Value); __instance.StartCoroutine(Effects.Lerp(0.1f, new Action((p) => { ipField.outputText.SetText(TheOtherRolesPlugin.Ip.Value); @@ -55,12 +56,18 @@ namespace TheOtherRoles { }))); ipField.ClearOnFocus = false; - ipField.OnFocusLost = ipField.OnEnter = ipField.OnChange = new Button.ButtonClickedEvent(); - ipField.OnChange.AddListener((UnityAction)ipEvent); + ipField.OnEnter = ipField.OnChange = new Button.ButtonClickedEvent(); + ipField.OnFocusLost = new Button.ButtonClickedEvent(); + ipField.OnChange.AddListener((UnityAction)onEnterOrIpChange); + ipField.OnFocusLost.AddListener((UnityAction)onFocusLost); - void ipEvent() { + void onEnterOrIpChange() { TheOtherRolesPlugin.Ip.Value = ipField.text; - TheOtherRolesPlugin.CustomRegion.DefaultIp = ipField.text; + } + + void onFocusLost() { + TheOtherRolesPlugin.UpdateRegions(); + __instance.ChooseOption(ServerManager.DefaultRegions[ServerManager.DefaultRegions.Length - 1]); } } if (portField == null || portField.gameObject == null) { @@ -78,19 +85,25 @@ namespace TheOtherRoles { portField.ClearOnFocus = false; - portField.OnFocusLost = portField.OnEnter = portField.OnChange = new Button.ButtonClickedEvent(); - portField.OnChange.AddListener((UnityAction)portEvent); + portField.OnEnter = portField.OnChange = new Button.ButtonClickedEvent(); + portField.OnFocusLost = new Button.ButtonClickedEvent(); + portField.OnChange.AddListener((UnityAction)onEnterOrPortFieldChange); + portField.OnFocusLost.AddListener((UnityAction)onFocusLost); - void portEvent() { + void onEnterOrPortFieldChange() { ushort port = 0; if (ushort.TryParse(portField.text, out port)) { TheOtherRolesPlugin.Port.Value = port; - TheOtherRolesPlugin.CustomRegion.Port = port; portField.outputText.color = Color.white; } else { portField.outputText.color = Color.red; } } + + void onFocusLost() { + TheOtherRolesPlugin.UpdateRegions(); + __instance.ChooseOption(ServerManager.DefaultRegions[ServerManager.DefaultRegions.Length - 1]); + } } } } diff --git a/Source Code/Resources/CurseButton.png b/Source Code/Resources/CurseButton.png new file mode 100644 index 0000000..fd49cc2 Binary files /dev/null and b/Source Code/Resources/CurseButton.png differ diff --git a/Source Code/Resources/CurseKillButton.png b/Source Code/Resources/CurseKillButton.png new file mode 100644 index 0000000..670f792 Binary files /dev/null and b/Source Code/Resources/CurseKillButton.png differ diff --git a/Source Code/RoleAssignmentPatch.cs b/Source Code/RoleAssignmentPatch.cs index ae783dc..e511cba 100644 --- a/Source Code/RoleAssignmentPatch.cs +++ b/Source Code/RoleAssignmentPatch.cs @@ -58,6 +58,7 @@ namespace TheOtherRoles impSettings.Add((byte)RoleId.Eraser, CustomOptionHolder.eraserSpawnRate.getSelection()); impSettings.Add((byte)RoleId.Trickster, CustomOptionHolder.tricksterSpawnRate.getSelection()); impSettings.Add((byte)RoleId.Cleaner, CustomOptionHolder.cleanerSpawnRate.getSelection()); + impSettings.Add((byte)RoleId.Warlock, CustomOptionHolder.warlockSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Jester, CustomOptionHolder.jesterSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Mayor, CustomOptionHolder.mayorSpawnRate.getSelection()); @@ -112,27 +113,49 @@ namespace TheOtherRoles List crewTickets = new List(); List impTickets = new List(); - foreach (KeyValuePair entry in crewSettings) { + for (int i = 0; i < crewSettings.Count; i++) { + var entry = crewSettings.ElementAt(i); if (entry.Value == 0) { // Never } else if (entry.Value == 10) { // Always if (crewmates.Count > 0 && maxCrewmateRoles > 0) { setRoleToRandomPlayer(entry.Key, crewmates); maxCrewmateRoles--; + if(CustomOptionHolder.blockedRolePairings.ContainsKey(entry.Key)) { + foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[entry.Key]) { + if(impSettings.ContainsKey(blockedRoleId)) { + impSettings[blockedRoleId] = 0; // Set Chance to Never for that role + } + if(crewSettings.ContainsKey(blockedRoleId)) { + crewSettings[blockedRoleId] = 0; // Set Chance to Never for that role + } + } + } } } else { // Other - for (int i = 0; i < entry.Value; i++) crewTickets.Add(entry.Key); + for (int j = 0; j < entry.Value; j++) crewTickets.Add(entry.Key); } } - foreach (KeyValuePair entry in impSettings) { + for (int i = 0; i < impSettings.Count; i++) { + var entry = impSettings.ElementAt(i); if (entry.Value == 0) { // Never } else if (entry.Value == 10) { // Always if (impostors.Count > 0 && maxImpostorRoles > 0) { setRoleToRandomPlayer(entry.Key, impostors); + if(CustomOptionHolder.blockedRolePairings.ContainsKey(entry.Key)) { + foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[entry.Key]) { + if(impSettings.ContainsKey(blockedRoleId)) { + impSettings[blockedRoleId] = 0; // Set Chance to Never for that role + } + if(crewSettings.ContainsKey(blockedRoleId)) { + crewSettings[blockedRoleId] = 0; // Set Chance to Never for that role + } + } + } maxImpostorRoles--; } } else { // Other - for (int i = 0; i < entry.Value; i++) impTickets.Add(entry.Key); + for (int j = 0; j < entry.Value; j++) impTickets.Add(entry.Key); } } @@ -144,6 +167,13 @@ namespace TheOtherRoles byte roleId = crewTickets[index]; crewTickets.RemoveAll(x => x == roleId); setRoleToRandomPlayer(roleId, crewmates); + + if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) { + foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) { + crewTickets.RemoveAll(x => x == blockedRoleId); + impTickets.RemoveAll(x => x == blockedRoleId); + } + } } } @@ -153,6 +183,13 @@ namespace TheOtherRoles byte roleId = impTickets[index]; impTickets.RemoveAll(x => x == roleId); setRoleToRandomPlayer(roleId, impostors); + + if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) { + foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) { + crewTickets.RemoveAll(x => x == blockedRoleId); + impTickets.RemoveAll(x => x == blockedRoleId); + } + } } } } diff --git a/Source Code/RoleInfo.cs b/Source Code/RoleInfo.cs index d94f89e..e56172c 100644 --- a/Source Code/RoleInfo.cs +++ b/Source Code/RoleInfo.cs @@ -107,6 +107,12 @@ namespace TheOtherRoles "Kill everyone and leave no traces", "Clean up dead bodies")); } + if (Warlock.warlock != null && p == Warlock.warlock) { + infos.Add(new RoleInfo("Warlock", + Warlock.color, + "Curse other players and kill everyone", + "Curse and kill everyone")); + } if (Detective.detective != null && p == Detective.detective) { infos.Add(new RoleInfo("Detective", Detective.color, @@ -146,8 +152,8 @@ namespace TheOtherRoles if (Hacker.hacker != null && p == Hacker.hacker) { infos.Add(new RoleInfo("Hacker", Hacker.color, - "Hacke to find the Impostors", - "Hacke to find the Impostors")); + "Hack to find the Impostors", + "Hack to find the Impostors")); } if (Child.child != null && p == Child.child) { infos.Add(new RoleInfo(p.Data.IsImpostor ? "Bad Child" : "Good Child", @@ -206,4 +212,4 @@ namespace TheOtherRoles return infos; } } -} \ No newline at end of file +} diff --git a/Source Code/TasksHandler.cs b/Source Code/TasksHandler.cs new file mode 100644 index 0000000..af1ebf3 --- /dev/null +++ b/Source Code/TasksHandler.cs @@ -0,0 +1,46 @@ +using HarmonyLib; +using static TheOtherRoles.TheOtherRoles; +using System.Collections; +using System.Collections.Generic; +using System; + +namespace TheOtherRoles { + [HarmonyPatch] + public static class TasksHandler { + + public static Tuple taskInfo(GameData.PlayerInfo playerInfo) { + int TotalTasks = 0; + int CompletedTasks = 0; + if (!playerInfo.Disconnected && playerInfo.Tasks != null && + playerInfo.Object && + (PlayerControl.GameOptions.GhostsDoTasks || !playerInfo.IsDead) && + !playerInfo.IsImpostor && + !Helpers.hasFakeTasks(playerInfo.Object) + ) { + + for (int j = 0; j < playerInfo.Tasks.Count; j++) { + TotalTasks++; + if (playerInfo.Tasks[j].Complete) { + CompletedTasks++; + } + } + } + return Tuple.Create(CompletedTasks, TotalTasks); + } + + [HarmonyPatch(typeof(GameData), nameof(GameData.RecomputeTaskCounts))] + private static class GameDataRecomputeTaskCountsPatch { + private static bool Prefix(GameData __instance) { + __instance.TotalTasks = 0; + __instance.CompletedTasks = 0; + for (int i = 0; i < __instance.AllPlayers.Count; i++) { + GameData.PlayerInfo playerInfo = __instance.AllPlayers[i]; + var (playerCompleted, playerTotal) = taskInfo(playerInfo); + __instance.TotalTasks += playerTotal; + __instance.CompletedTasks += playerCompleted; + } + return false; + } + } + } +} diff --git a/Source Code/TheOtherRoles.cs b/Source Code/TheOtherRoles.cs index 438f9fc..727e0ab 100644 --- a/Source Code/TheOtherRoles.cs +++ b/Source Code/TheOtherRoles.cs @@ -47,6 +47,7 @@ namespace TheOtherRoles Spy.clearAndReload(); Trickster.clearAndReload(); Cleaner.clearAndReload(); + Warlock.clearAndReload(); } public static class Jester { @@ -619,7 +620,7 @@ namespace TheOtherRoles } public static void removeCurrentJackal() { - if (!formerJackals.Contains(jackal)) formerJackals.Add(jackal); + if (jackal != null && !formerJackals.Contains(jackal)) formerJackals.Add(jackal); jackal = null; currentTarget = null; fakeSidekick = null; @@ -759,4 +760,55 @@ namespace TheOtherRoles cooldown = CustomOptionHolder.janitorCooldown.getFloat(); } } + + public static class Warlock { + + public static PlayerControl warlock; + public static Color color = Palette.ImpostorRed; + + public static PlayerControl currentTarget; + public static PlayerControl curseVictim; + public static PlayerControl curseVictimTarget; + public static PlayerControl curseKillTarget; + + public static float cooldown = 30f; + public static float rootTime = 5f; + + private static Sprite curseButtonSprite; + private static Sprite curseKillButtonSprite; + + public static Sprite getCurseButtonSprite() { + if (curseButtonSprite) return curseButtonSprite; + curseButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.CurseButton.png", 115f); + return curseButtonSprite; + } + + public static Sprite getCurseKillButtonSprite() { + if (curseKillButtonSprite) return curseKillButtonSprite; + curseKillButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.CurseKillButton.png", 115f); + return curseKillButtonSprite; + } + + public static void clearAndReload() { + warlock = null; + currentTarget = null; + curseVictim = null; + curseVictimTarget = null; + curseKillTarget = null; + cooldown = CustomOptionHolder.warlockCooldown.getFloat(); + rootTime = CustomOptionHolder.warlockRootTime.getFloat(); + } + + public static void resetCurse() { + HudManagerStartPatch.warlockCurseButton.Timer = HudManagerStartPatch.warlockCurseButton.MaxTimer; + HudManagerStartPatch.warlockCurseButton.Sprite = Warlock.getCurseButtonSprite(); + HudManagerStartPatch.warlockCurseButton.killButtonManager.TimerText.color = Palette.EnabledColor; + currentTarget = null; + curseVictim = null; + curseVictimTarget = null; + curseKillTarget = null; + } + + } + } \ No newline at end of file diff --git a/Source Code/TheOtherRoles.csproj b/Source Code/TheOtherRoles.csproj index ca122d8..203c8d4 100644 --- a/Source Code/TheOtherRoles.csproj +++ b/Source Code/TheOtherRoles.csproj @@ -1,7 +1,7 @@ netstandard2.1 - 2.3.0 + 2.4.0 TheOtherRoles Eisbison diff --git a/Source Code/UpdatePatch.cs b/Source Code/UpdatePatch.cs index 0e47c46..37ddf20 100644 --- a/Source Code/UpdatePatch.cs +++ b/Source Code/UpdatePatch.cs @@ -183,8 +183,6 @@ namespace TheOtherRoles Morphling.morphling.nameText.text = hidePlayerName(PlayerControl.LocalPlayer, Morphling.morphling) ? "" : Morphling.morphTarget.Data.PlayerName; Morphling.morphling.myRend.material.SetColor("_BackColor", Palette.ShadowColors[Morphling.morphTarget.Data.ColorId]); Morphling.morphling.myRend.material.SetColor("_BodyColor", Palette.PlayerColors[Morphling.morphTarget.Data.ColorId]); - Morphling.morphling.myRend.material.SetFloat("_Outline", Morphling.morphTarget.myRend.material.GetFloat("_Outline")); - Morphling.morphling.myRend.material.SetColor("_OutlineColor", Morphling.morphTarget.myRend.material.GetColor("_OutlineColor")); Morphling.morphling.HatRenderer.SetHat(Morphling.morphTarget.Data.HatId, Morphling.morphTarget.Data.ColorId); Morphling.morphling.nameText.transform.localPosition = new Vector3(0f, (Morphling.morphTarget.Data.HatId == 0U) ? 0.7f : 1.05f, -0.5f); @@ -282,14 +280,8 @@ namespace TheOtherRoles if (Snitch.snitch == null || Snitch.snitch.Data.IsDead) return; - int numberOfTasks = 0; - GameData.PlayerInfo playerInfo = Snitch.snitch.Data; - if (!playerInfo.Disconnected && playerInfo.Tasks != null) { - for (int i = 0; i < playerInfo.Tasks.Count; i++) { - if (!playerInfo.Tasks[i].Complete) - numberOfTasks++; - } - } + var (playerCompleted, playerTotal) = TasksHandler.taskInfo(Snitch.snitch.Data); + int numberOfTasks = playerTotal - playerCompleted; if (PlayerControl.LocalPlayer.Data.IsImpostor && numberOfTasks <= Snitch.taskCountForImpostors) { if (Snitch.localArrows.Count == 0) Snitch.localArrows.Add(new Arrow(Color.blue)); diff --git a/Source Code/UsablesPatch.cs b/Source Code/UsablesPatch.cs index d0074a2..3f19dae 100644 --- a/Source Code/UsablesPatch.cs +++ b/Source Code/UsablesPatch.cs @@ -165,6 +165,21 @@ namespace TheOtherRoles } } + + [HarmonyPatch(typeof(Console), nameof(Console.CanUse))] + public static class ConsoleCanUsePatch { + public static bool Prefix(ref float __result, Console __instance, [HarmonyArgument(0)] GameData.PlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) { + canUse = couldUse = false; + if (Swapper.swapper != null && Swapper.swapper == PlayerControl.LocalPlayer) + return !__instance.TaskTypes.Any(x => x == TaskTypes.FixLights || x == TaskTypes.FixComms); + if (__instance.AllowImpostor) return true; + if (!Helpers.hasFakeTasks(pc.Object)) return true; + __result = float.MaxValue; + return false; + } + } + + [HarmonyPatch(typeof(TuneRadioMinigame), nameof(TuneRadioMinigame.Begin))] class CommsMinigameBeginPatch { static void Postfix(TuneRadioMinigame __instance) {