]> git.deb.at Git - rhonda/theotherroles.git/commitdiff
Version 2.4.0
authorthunderstorm584 <80977292+thunderstorm584@users.noreply.github.com>
Sat, 24 Apr 2021 18:21:16 +0000 (20:21 +0200)
committerthunderstorm584 <80977292+thunderstorm584@users.noreply.github.com>
Sat, 24 Apr 2021 18:21:16 +0000 (20:21 +0200)
30 files changed:
Banner.png [deleted file]
Images/Colors.png [new file with mode: 0644]
Images/DarkerColors.png [deleted file]
Images/LighterColors.png [deleted file]
README.md
Source Code/Buttons.cs
Source Code/ChatCommands.cs [new file with mode: 0644]
Source Code/CredentialsPatch.cs
Source Code/CrowdedPatches.cs
Source Code/CustomButton.cs
Source Code/CustomOptions.cs
Source Code/FakeTasksForEveryone.cs [deleted file]
Source Code/GameStartManagerPatch.cs
Source Code/Helpers.cs
Source Code/JackInTheBox.cs
Source Code/Main.cs
Source Code/MapOptions.cs
Source Code/MeetingPatch.cs
Source Code/PlayerControlPatch.cs
Source Code/RPC.cs
Source Code/RegionMenuPatch.cs
Source Code/Resources/CurseButton.png [new file with mode: 0644]
Source Code/Resources/CurseKillButton.png [new file with mode: 0644]
Source Code/RoleAssignmentPatch.cs
Source Code/RoleInfo.cs
Source Code/TasksHandler.cs [new file with mode: 0644]
Source Code/TheOtherRoles.cs
Source Code/TheOtherRoles.csproj
Source Code/UpdatePatch.cs
Source Code/UsablesPatch.cs

diff --git a/Banner.png b/Banner.png
deleted file mode 100644 (file)
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 (file)
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 (file)
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 (file)
index 02d822a..0000000
Binary files a/Images/LighterColors.png and /dev/null differ
index 7bf4eb034addb800c4a5971671d550ac832f1ff9..da45dd81d92d019c17c7f9ea7d8b6efd4d76c721 100644 (file)
--- 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.\r
 Even more roles are coming soon :)\r
 \r
-- [Mafia](#mafia)\r
-  - Godfather\r
-  - Janitor\r
-  - Mafioso\r
-- [Morphling](#morphling)\r
-- [Camouflager](#camouflager)\r
-- [Vampire](#vampire)\r
-- [Eraser](#eraser)\r
-- [Trickster](#trickster)\r
-- [Cleaner](#cleaner)\r
-- [Lovers](#lovers)\r
-  - Lover\r
-  - ImpLover\r
-- [Child](#child)\r
-  - Child Impostor\r
-  - Child Crewmate\r
-- [Jackal](#jackal)\r
-- [Sidekick](#sidekick)\r
-- [Jester](#jester)\r
-- [Sheriff](#sheriff)\r
-- [Seer](#seer)\r
-- [Engineer](#engineer)\r
-- [Detective](#detective)\r
-- [Lighter](#lighter)\r
-- [Medic](#medic)\r
-- [Mayor](#mayor)\r
-- [Hacker](#hacker)\r
-- [Shifter](#shifter)\r
-- [Time Master](#time-master)\r
-- [Swapper](#swapper)\r
-- [Tracker](#tracker)\r
-- [Snitch](#snitch)\r
-- [Spy](#spy)\r
+| Impostors | Crewmates | Neutral |\r
+|----------|-------------|-----------------|\r
+| [Bad Child](#child) | [Child](#child) | [Jester](#jester) |\r
+| [Camouflager](#camouflager) | [Detective](#detective) | [Jackal](#jackal) |\r
+| [Cleaner](#cleaner) | [Engineer](#engineer) | [Sidekick](#sidekick) |\r
+| [Eraser](#eraser) | [Hacker](#hacker) |  |\r
+| [ImpLover](#lovers) | [Lover](#lovers) |  |\r
+| [Godfather (Mafia)](#mafia) | [Lighter](#lighter) |  |\r
+| [Mafioso (Mafia)](#mafia) | [Mayor](#mayor) |  |\r
+| [Janitor (Mafia)](#mafia)  | [Medic](#medic) |  |\r
+| [Morphling](#morphling) | [Seer](#seer) |  |\r
+| [Trickster](#trickster) | [Sheriff](#sheriff) |  |\r
+| [Vampire](#vampire) | [Shifter](#shifter) |  |\r
+| [Warlock](#warlock) | [Snitch](#snitch) |  |\r
+|  | [Spy](#spy) |  |\r
+|  | [Swapper](#swapper) |  |\r
+|  | [Time Master](#time-master) |  |\r
+|  |  [Tracker](#tracker) |  |\r
 \r
 The [Role Assignment](#role-assignment) sections explains how the roles are being distributed among the players.\r
 \r
 # Releases\r
 | Among Us - Version| Mod Version | Link |\r
 |----------|-------------|-----------------|\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
 | 2021.4.12s| v2.2.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.2.1/TheOtherRoles.zip)\r
@@ -69,6 +55,18 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 <details>\r
   <summary>Click to show the Changelog</summary>\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
+- Added options to configure Morph & Camo duration\r
+- 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)\r
+- Fixed an oversight which made StreamerMode only work as host\r
+- Fixed an oversight which required Jackals to finish Tasks, after Sidekick was promoted\r
+- Fixed an oversight which made Sidekicks not promote, if the Jackal disconnected\r
+- Fixed a bug where the Trickster box was invisible\r
+- Fixed a bug where changes to the server ip and port would only be applied if the game was restarted\r
+- Added a way to get the 2 Hidden [Colors](#colors)\r
+\r
 **Version 2.3.0**\r
 - **New Role:** [Cleaner](#cleaner)\r
 - Added 12 new [Colors](#colors)\r
@@ -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) \r
 - **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.\r
 - **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.\r
+- **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.\r
 \r
 # Custom Hats\r
 ## Create and submit new hat designs\r
@@ -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.\r
     - 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*).\r
     - Parameter `bounce`: This parameter determines whether the hat will bounce while you're walking or not.\r
-    - 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.\r
+    - 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.\r
     - Parameter `behind`: If this parameter is set, the main texture will be rendered behind the player.\r
   - `Back texture (optional)`:\r
     - This texture will be rendered behind the player.\r
@@ -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.\r
 \r
 # Colors\r
-### **Lighter Colors:** \r
-- Pink, Orange, Yellow, White\r
-- Cyan, Lime, Salmon, Mint\r
-- Lavender, Peach, Wasabi, Hot Pink\r
-\r
-![](./Images/LighterColors.png)\r
-\r
-### **Darker Colors:** \r
-- Red, Blue, Green, Black\r
-- Purple, Brown, Bordeaux, Olive\r
-- Turqoise, Nougat, Gray, Petrol\r
-\r
-![](./Images/DarkerColors.png)\r
+<img src="./Images/Colors.png" alt="drawing" width="300px"/>\r
 \r
 # Roles\r
 \r
@@ -387,6 +374,7 @@ The Morphling is an Impostor which can additionally scan the appearance of a pla
 |----------|:-------------:|\r
 | Morphling Spawn Chance | -\r
 | Morphling Cooldown | -\r
+| Morph Duration | Time the Morphling stays morphed\r
 -----------------------\r
 \r
 ## Camouflager\r
@@ -407,6 +395,7 @@ are hidden and all players have the same color.\
 |----------|:-------------:|\r
 | Camouflager Spawn Chance | -\r
 | Camouflager Cooldown | -\r
+| Camo Duration | Time players stay camouflaged\r
 -----------------------\r
 \r
 ## Vampire\r
@@ -418,6 +407,7 @@ If a victim is near a garlic, the "Bite Button" turns into the default "Kill But
 **NOTE:**\r
 - If a bitten player is still alive when a meeting is being called, he dies at the start of the meeting.\r
 - The cooldown is the same as the default kill cooldown (+ the kill delay if the Vampire bites the target).\r
+- If there is a Vampire in the game, there can't be a Warlock\r
 \r
 ### Game Options\r
 | Name | Description |\r
@@ -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\r
 -----------------------\r
 \r
+\r
+## Warlock\r
+### **Team: Impostors**\r
+The Warlock is an Impostor, that can curse another player (the cursed player doesn't get notified).\\r
+If the cursed person stands next to another player, the Warlock is able to kill that player (no matter how far away he is).\\r
+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.\r
+The Warlock can still perform normal kills, but the two buttons share the same cooldown.\r
+\r
+\\r
+**NOTE:**\r
+- The Warlock can always kill his Impostor mates (and even himselfs) using the "cursed kill"\r
+- If there is a Warlock in the game, there can't be a Vampire\r
+- Performing a normal kill, doesn't lift the curse\r
+\r
+### Game Options\r
+| Name | Description |\r
+|----------|:-------------:|\r
+| Warlock Spawn Chance | -\r
+| Warlock Cooldown | Cooldown for using the Curse and curse Kill\r
+| Warlock Root Time | Time the Warlock is rooted in place after killing using the curse\r
+-----------------------\r
+\r
 ## Lovers\r
 ### **Team: Either Crewmates or Impostors**\r
 There are always two Lovers which are linked together.\\r
index 881a9f50a5d9a9786cb2dcb387cccf03d3959088..b65493d4fe59586ff3f68bbd7d12607016986234 100644 (file)
@@ -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<float>((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 (file)
index 0000000..fb7df17
--- /dev/null
@@ -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
index 2e3012df8782bd1e40251c2fad2b92a86ed5ce94..8a314f65933aa9817a6d798e6f01edb208b91421 100644 (file)
@@ -10,13 +10,19 @@ namespace TheOtherRoles
 {
     [HarmonyPatch]
     public static class CredentialsPatch {
-        [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))]
+        public static string fullCredentials = 
+$@"<color=#FCCE03FF>TheOtherRoles</color> v{TheOtherRolesPlugin.Major}.{TheOtherRolesPlugin.Minor}.{TheOtherRolesPlugin.Patch}:
+- Modded by <color=#FCCE03FF>Eisbison</color>,
+  <color=#FFEB91FF>Thunderstorm584</color> and <color=#FFEB91FF>EndOfFile</color>
+- Balanced with <color=#FFEB91FF>Dhalucard</color>
+- Button design by <color=#FFEB91FF>Bavari</color>";
 
+        [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))]
         private static class VersionShowerPatch
         {
             static void Postfix(VersionShower __instance) {
                 string spacer = new String('\n', 8);
-                string fullCredentials = $"<color=#FCCE03FF>TheOtherRoles</color> v{TheOtherRolesPlugin.Major}.{TheOtherRolesPlugin.Minor}.{TheOtherRolesPlugin.Patch}:\n- Modded by <color=#FCCE03FF>Eisbison</color> and <color=#FFEB91FF>Thunderstorm584</color>\n- Balanced with <color=#FFEB91FF>Dhalucard</color>\n- Button design by <color=#FFEB91FF>Bavari</color>";
+
                 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 += "<color=#FCCE03FF>TheOtherRoles</color>\nModded by <color=#FCCE03FF>Eisbison</color>";
-                else
-                    __instance.text.text += "\n<color=#FCCE03FF>TheOtherRoles</color>\nModded by <color=#FCCE03FF>Eisbison</color>\nand <color=#FFEB91FF>Thunderstorm584</color>\nBalanced with <color=#FFEB91FF>Dhalucard</color>\nButton design by <color=#FFEB91FF>Bavari</color>";
+                if (AmongUsClient.Instance.GameState == InnerNet.InnerNetClient.GameStates.Started) {
+                    __instance.text.text = "<color=#FCCE03FF>TheOtherRoles</color>\nModded by <color=#FCCE03FF>Eisbison</color>\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);
+                }
             }
         }
     }
index 8f787c2f484efce6d540d5dccda1f108b13e2c61..a6eb2ade307c84eb9f8595083505c8580dcaec92 100644 (file)
@@ -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
+}
index bfef7c53b77a1ab8b79785427394d6a4cfcce5c8..c18999bf75eb44101a69002c64998e3aa20b68f2 100644 (file)
@@ -22,8 +22,9 @@ public class CustomButton
     public Sprite Sprite;
     private HudManager hudManager;
     private bool mirror;
+    private KeyCode? hotkey;
 
-    public CustomButton(Action OnClick, Func<bool> HasButton, Func<bool> CouldUse, Action OnMeetingEnds, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, bool HasEffect, float EffectDuration, Action OnEffectEnds, bool mirror = false)
+    public CustomButton(Action OnClick, Func<bool> HasButton, Func<bool> 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<PassiveButton>();
         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<bool> HasButton, Func<bool> 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<bool> HasButton, Func<bool> 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
index 4df9aa73561632ea93d260f17612c5c8a5164f89..a3bd672395f942a4de2117b77b09cb686d9f7c99 100644 (file)
@@ -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<byte, byte[]> blockedRolePairings = new Dictionary<byte, byte[]>();
 
         public static string cs(Color c, string s) {
             return string.Format("<color=#{0:X2}{1:X2}{2:X2}{3:X2}>{4}</color>", 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 (file)
index 42e52b7..0000000
+++ /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;
-            }
-        }
-    }
-}
index 42f289e5c5140c9c692b4063a6a3347884ebb4d9..d01d123582039b8c68cf09e1332207bd2b19c853 100644 (file)
@@ -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<DummyBehaviour>();
                         if (dummyComponent != null && dummyComponent.enabled)
@@ -77,6 +77,9 @@ namespace TheOtherRoles {
                     }
                 }
 
+                // Lobby code replacement
+                __instance.GameRoomName.text = TheOtherRolesPlugin.StreamerMode.Value ? $"<color={TheOtherRolesPlugin.StreamerModeReplacementColor.Value}>{TheOtherRolesPlugin.StreamerModeReplacementText.Value}</color>" : 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 ? $"<color={TheOtherRolesPlugin.StreamerModeReplacementColor.Value}>{TheOtherRolesPlugin.StreamerModeReplacementText.Value}</color>" : lobbyCodeText;
             }
         }
 
index feb431a1a552d2d8c9f4eee41e7e4bb0a71665b8..570655148aa968f48a03e03658e4d88a33c72bd1 100644 (file)
@@ -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("<color=#{0:X2}{1:X2}{2:X2}{3:X2}>{4}</color>", ToByte(c.r), ToByte(c.g), ToByte(c.b), ToByte(c.a), s);
         }
index 31ff5f06b2c24833742771a7944f2c5695cbfaa2..72b2cfac69ecaf3ed5950a1dae3fd7b96ff337a8 100644 (file)
@@ -10,19 +10,14 @@ namespace TheOtherRoles{
         public static System.Collections.Generic.List<JackInTheBox> AllJackInTheBoxes = new System.Collections.Generic.List<JackInTheBox>();
         public static int JackInTheBoxLimit = 3;
         public static bool boxesConvertedToVents = false;
-        private static List<Sprite> boxAnimationSprites;
-
-        public static Sprite getBoxSprite() {
-            List<Sprite> s = getBoxAnimationSprites();
-            if (s.Count > 0) return s.First();
-            return null;
-        }
-
-        public static List<Sprite> getBoxAnimationSprites() {
-            if (boxAnimationSprites != null) return boxAnimationSprites;
-            boxAnimationSprites = new List<Sprite>();
-            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<float>((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<SpriteRenderer>();
-            boxRenderer.sprite = getBoxSprite();
+            boxRenderer.sprite = getBoxAnimationSprite(0);
 
             // Create the vent
             var referenceVent = UnityEngine.Object.FindObjectOfType<Vent>(); 
@@ -65,7 +58,7 @@ namespace TheOtherRoles{
             vent.GetComponent<PowerTools.SpriteAnim>()?.Stop();
             vent.Id = ShipStatus.Instance.AllVents.Select(x => x.Id).Max() + 1; // Make sure we have a unique id
             var ventRenderer = vent.GetComponent<SpriteRenderer>();
-            ventRenderer.sprite = getBoxSprite();
+            ventRenderer.sprite = getBoxAnimationSprite(0);
             vent.myRend = ventRenderer;
             var allVentsList = ShipStatus.Instance.AllVents.ToList();
             allVentsList.Add(vent);
index 881ee6e7e60d30be6424f261615761b3b996966d..a6681c42a70b8942aa02e8a3b208e6a80db12aca 100644 (file)
@@ -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<string> Ip { get; set; }
         public static ConfigEntry<ushort> Port { get; set; }
 
-        public static DnsRegionInfo CustomRegion;
+        public static IRegionInfo[] defaultRegions;
+
+        public static void UpdateRegions() {
+            ServerManager serverManager = DestroyableSingleton<ServerManager>.Instance;
+            IRegionInfo[] regions = defaultRegions;
+
+            var CustomRegion = new DnsRegionInfo(Ip.Value, "Custom", StringNames.NoTranslation, Ip.Value, Port.Value);
+            regions = regions.Concat(new IRegionInfo[] { CustomRegion.Cast<IRegionInfo>() }).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<ServerManager>.Instance;
-            IRegionInfo[] regions = ServerManager.DefaultRegions;
-
-            regions = regions.Concat(new IRegionInfo[] { CustomRegion.Cast<IRegionInfo>() }).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
index 35ac9864e83a28595f68bb8b0e6961d68e727af9..e324c96305ab0cac5c9bfeb14aa815f8b07d6ff6 100644 (file)
@@ -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
index daec581e670bb803689f48a14f4f1ea356683fa4..d6dbfbbb267ebf63c416b87f07abe1783362601e 100644 (file)
@@ -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
index fa14fdec861c9b0f91ae96a39dce1eb386568ba5..841a72a98c8be2304b832adff6ea5e99aa37d4a2 100644 (file)
@@ -15,17 +15,18 @@ namespace TheOtherRoles {
     {
         // Helpers
 
-        static PlayerControl setTarget(bool onlyCrewmates = false, bool targetPlayersInVents = false, List<PlayerControl> untargetablePlayers = null) {
+        static PlayerControl setTarget(bool onlyCrewmates = false, bool targetPlayersInVents = false, List<PlayerControl> 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<GameData.PlayerInfo> 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<TMPro.TextMeshPro>() : 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<TMPro.TextMeshPro>() : 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 ? $"<color=#FAD934FF>({tasksCompleted}/{tasksTotal})</color>" : "";
+                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;
index e6decb566a1c69c996c42884fd9b44e52cc08b5a..87f4b34cf947e206206a604e0f8c771f42c621c3 100644 (file)
@@ -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;
             }
         }
     }
index 1c147097ce2b2dc6751b08d490de99f065359de3..e5ceeeb3a56436741d3b064c25f07e07bf2d606e 100644 (file)
@@ -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<float>((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 (file)
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 (file)
index 0000000..670f792
Binary files /dev/null and b/Source Code/Resources/CurseKillButton.png differ
index ae783dc4ba7a8836ebf6b68fcf351c074c31e9ac..e511cbab4d8075afed1779c2a20ff5380457777a 100644 (file)
@@ -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<byte> crewTickets = new List<byte>();
             List<byte> impTickets = new List<byte>();
 
-            foreach (KeyValuePair<byte, int> 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<byte, int> 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);
+                        }
+                    }
                 }
             }
         }
index d94f89e23f2ffbdac03062acfa623580f7345eda..e56172c782d9d4b09b10c679c920820c04b2a866 100644 (file)
@@ -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 <color=#FF1919FF>Impostors</color>",
-                "Hacke to find the Impostors"));
+                "Hack to find the <color=#FF1919FF>Impostors</color>",
+                "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 (file)
index 0000000..af1ebf3
--- /dev/null
@@ -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<int, int> 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;
+            }
+        }
+    }
+}
index 438f9fca477e30dfd6a22ca56f055e46592be33a..727e0abce2a6189075956afc6971e127c5e01ff9 100644 (file)
@@ -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
index ca122d89414cd5376d59ee51c567341d02edae72..203c8d4df05696b4f02002ac371513638c675b12 100644 (file)
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <TargetFramework>netstandard2.1</TargetFramework>
-        <Version>2.3.0</Version>
+        <Version>2.4.0</Version>
         <Description>TheOtherRoles</Description>
         <Authors>Eisbison</Authors>
     </PropertyGroup>
index 0e47c46074db79e6e4ccb2d85dd28f05b3808ee1..37ddf20b825bf6e133774dce233aacf610e4da23 100644 (file)
@@ -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));
index d0074a2a7d6d4ef5fa200039cf7fb380252989ff..3f19daea4a861341709815c91ed6c49128f14d4a 100644 (file)
@@ -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) {