]> git.deb.at Git - rhonda/theotherroles.git/commitdiff
Add version 2.6.0
authorEisbison <eisbison.creator@gmail.com>
Sat, 8 May 2021 15:53:26 +0000 (17:53 +0200)
committerEisbison <eisbison.creator@gmail.com>
Sat, 8 May 2021 15:53:26 +0000 (17:53 +0200)
28 files changed:
Images/Preview_2.5.1.png [deleted file]
Images/Preview_2.6.0.png [new file with mode: 0644]
README.md
Source Code/AUFixes.cs [new file with mode: 0644]
Source Code/Buttons.cs
Source Code/ChatCommands.cs
Source Code/CredentialsPatch.cs
Source Code/CustomColors.cs
Source Code/CustomHats.cs
Source Code/CustomOptions.cs
Source Code/EndGamePatch.cs
Source Code/GameStartManagerPatch.cs
Source Code/Helpers.cs
Source Code/IntroPatch.cs
Source Code/Main.cs
Source Code/MeetingPatch.cs
Source Code/ModUpdater.cs [new file with mode: 0644]
Source Code/PlayerControlPatch.cs
Source Code/RPC.cs
Source Code/Resources/AdminTableIcon.png [deleted file]
Source Code/Resources/DouseButton.png [new file with mode: 0644]
Source Code/Resources/IgniteButton.png [new file with mode: 0644]
Source Code/RoleAssignmentPatch.cs
Source Code/RoleInfo.cs
Source Code/TheOtherRoles.cs
Source Code/TheOtherRoles.csproj
Source Code/UpdatePatch.cs
Source Code/UsablesPatch.cs

diff --git a/Images/Preview_2.5.1.png b/Images/Preview_2.5.1.png
deleted file mode 100644 (file)
index c0684b3..0000000
Binary files a/Images/Preview_2.5.1.png and /dev/null differ
diff --git a/Images/Preview_2.6.0.png b/Images/Preview_2.6.0.png
new file mode 100644 (file)
index 0000000..51d5f53
Binary files /dev/null and b/Images/Preview_2.6.0.png differ
index df97db9ceb2dc446b0849542243bc7b8604a29c3..d7c6e0f79333ed5db6e79d52e9535ee538a1f1f6 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 [![Discord](https://discord.com/assets/e4923594e694a21542a489471ecffa50.svg)](https://discord.gg/77RkMJHWsM)\r
 ![eisbison banner](./Images/Banner.png)\r
-![eisbison infographic](./Images/Preview_2.5.1.png)\r
+![eisbison infographic](./Images/Preview_2.6.0.png)\r
 \r
 \r
 # The Other Roles\r
@@ -10,10 +10,10 @@ Even more roles are coming soon :)
 \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
+| [Bad Child](#child) | [Child](#child) | [Arsonist](#arsonist) |\r
+| [Camouflager](#camouflager) | [Detective](#detective) | [Jester](#jester) |\r
+| [Cleaner](#cleaner) | [Engineer](#engineer) | [Jackal](#jackal) |\r
+| [Eraser](#eraser) | [Hacker](#hacker) | [Sidekick](#sidekick) |\r
 | [ImpLover](#lovers) | [Lover](#lovers) |  |\r
 | [Godfather (Mafia)](#mafia) | [Lighter](#lighter) |  |\r
 | [Mafioso (Mafia)](#mafia) | [Mayor](#mayor) |  |\r
@@ -33,6 +33,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 # Releases\r
 | Among Us - Version| Mod Version | Link |\r
 |----------|-------------|-----------------|\r
+| 2021.4.14s| v2.6.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.6.0/TheOtherRoles.zip)\r
 | 2021.4.14s| v2.5.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.5.1/TheOtherRoles.zip)\r
 | 2021.4.14s| v2.5.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.5.0/TheOtherRoles.zip)\r
 | 2021.4.14s| v2.4.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.4.0/TheOtherRoles.zip)\r
@@ -58,6 +59,19 @@ 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.6.0**\r
+- **New Role:** [Arsonist](#arsonist)\r
+- Added an Ingame Updater, to make it easier to update the Mod\r
+- Added synchronization for Airship toilet doors. Doors now open/close for everyone\r
+- Changed Shifter to also die when shifting a neutral role (Jester, Arsonist, Jackal, ...)\r
+- Changed the option "Jester Can Die To Sheriff" to "Neutrals Can Die To Sheriff"\r
+- Changed the role assignment system. You can now set how many neutral roles you want in your game\r
+- Changed Hacker to see colors more clearly on Admin Table\r
+- Changed version handshake to give more clear info\r
+- Fixed a problem with the Hat Tab leaving too much space between categories\r
+- Fixed an Among Us bug, which made the selected region always show "North America"\r
+- Fixed an Among Us bug, which made the disconnect info be off-screen. (hopefully)\r
+\r
 **Version 2.5.1**\r
 - **New Hats:** We added the support for custom hats and there are already a few hats inside the game. We can add new hats without updating the mod and we're awaiting your hat designs on our discord server.\r
 - Changed Lovers to ignore Lover's Tasks for Taskwin, while an ImpLover is alive\r
@@ -301,7 +315,8 @@ docker run -d -p 22023:22023/udp --env IMPOSTOR_AntiCheat__Enabled=false --env I
 [TooManyRolesMods](https://github.com/Hardel-DW/TooManyRolesMods) - Idea for the Detective and Time Master roles comes from **Hardel-DW**. Also some code snippets of the implementation were used.\\r
 [TownOfUs](https://github.com/slushiegoose/Town-Of-Us) - Idea for the Swapper, Shifter and a similar Mayor role come from **Slushiegoose**\\r
 [Ottomated](https://twitter.com/ottomated_) - Idea for the Morphling, Snitch and Camouflager role come from **Ottomated**\\r
-[Crowded-Mod](https://github.com/CrowdedMods/CrowdedMod) - Our implementation for 10+ player lobbies is inspired by the one from the **Crowded Mod Team**\r
+[Crowded-Mod](https://github.com/CrowdedMods/CrowdedMod) - Our implementation for 10+ player lobbies is inspired by the one from the **Crowded Mod Team**\\r
+[Town-Of-Impostors](https://github.com/AJMix/TownOfImpostors) - Idea for the Arsonist role comes from  **AJMix**\r
 \r
 # Settings\r
 The mod adds a few new settings to Among Us (in addition to the role settings):\r
@@ -344,10 +359,11 @@ Here are a few instructions, on how to create a custom hat:
 We are still improving the role assignment system. It's not that intuitive right now, but it's more flexible than the older one\r
 if you're using it right.\r
 \r
-First you need to choose how many special roles you want in the game (one option for Impostor roles and one option for the rest).\r
+First you need to choose how many special roles of each kind (Impostor/Neutral/Crewmate) you want in the game.\r
 The count you set will only be reached, if there are enough Crewmates/Impostors in the game and if enough roles are set to be in the game (i.e. they are set to > 0%). The roles are then being distributed as follows:\r
 - First all roles that are set to 100% are being assigned to arbitrary players\r
-- After that each role that has 10%-90% selected adds 1-9 tickets to a ticket pool (one pool for Impostors, one for Crewmates). Then the roles will be selected randomly from that pool as long as possible (until the selected number is reached, until there are no more Crewmates or until there are no more tickets). If a role is selected from the pool, obviously all the tickets of that role are being removed.\r
+- After that each role that has 10%-90% selected adds 1-9 tickets to a ticket pool (there exists a ticket pool for Crewmates, Neutrals and Impostors). Then the roles will be selected randomly from the pools as long it's possible (until the selected number is reached, until there are no more Crewmates/Impostors or until there are no more tickets). If a role is selected from the pool, obviously all the tickets of that role are being removed.\r
+- The Mafia, Lovers and Child are being selected independently (without using the ticket system) according to the spawn chance you selected. After that all Neutral roles are being selected, then all Crewmate roles and in the very end all Impostor roles.\r
 \r
 **Example:**\\r
 Settings: 2 special Crewmate roles, Snitch: 100%, Hacker: 10%, Tracker: 30%\\r
@@ -552,7 +568,7 @@ If they try to kill a Crewmate, they die instead.
 |----------|:-------------:|\r
 | Sheriff Spawn Chance | -\r
 | Sheriff Cooldown | -\r
-| Jester Can Die To Sheriff | -\r
+| Neutrals Can Die To Sheriff | -\r
 -----------------------\r
 \r
 ## Jester\r
@@ -566,6 +582,21 @@ The Jester does not have any tasks. They win the game as a solo, if they get vot
 | Jester can call emergency meeting | Option to disable the emergency button for the Jester\r
 -----------------------\r
 \r
+## Arsonist\r
+### **Team: Neutral**\r
+The Arsonist does not have any tasks, he has to win the game as a solo.\\r
+The Arsonist can douse other players by pressing the douse button and remaining next to the player for a few seconds.\\r
+If the player that the Arsonist douses walks out of range, the cooldown will reset to 0.\\r
+After dousing everyone alive the Arsonist can ignite all the players which results in an Arsonist win.\r
+\r
+### Game Options\r
+| Name | Description |\r
+|----------|:-------------:|\r
+| Arsonist Spawn Chance | -\r
+| Arsonist Countdown | -\r
+| Arsonist Douse Duration | The time it takes to douse a player\r
+-----------------------\r
+\r
 ## Seer\r
 ### **Team: Crewmates**\r
 The Seer has two abilities (one can activate one of them or both in the options).\r
@@ -713,11 +744,10 @@ Otherwise he sees the same information as everyone else.
 \r
 ## Shifter\r
 ### **Team: Crewmates**\r
-The Shifter can take over the role of another Crewmate or Neutral, the other player will transform into a Crewmate.\\r
+The Shifter can take over the role of another Crewmate, the other player will transform into a Crewmate.\\r
 The Shift will always be performed at the end of the next meeting right before a player is exiled. The target needs to be chosen during the round.\\r
 Even if the Shifter or the target dies before the meeting, the Shift will still be performed.\\r
-Swapping roles with a Crewmate or Neutral gives them their role, the other player becomes a Crewmate.\\r
-Swapping roles with an Impostor or a custom Impostor role fails and the Shifter commits suicide after the next meeting (there won't be any body).\\r
+Swapping roles with an Impostor or Neutral fails and the Shifter commits suicide after the next meeting (there won't be any body).\\r
 The Shifter aims to save roles from leaving the game, by e.g. taking over a Sheriff or Medic that is known to the Impostors.\\r
 This works especially well against the Eraser, but also gives the Eraser the possiblity to act like a Shifter.\\r
 The **special interactions** with the Shifter are noted in the chapters of the respective roles.\\r
@@ -814,12 +844,13 @@ The team Jackal enables multiple new outcomes of the game, listing some examples
 The priority of the win conditions is the following:\r
 1. Crewmate Child lose by vote\r
 2. Jester wins by vote\r
-3. Team Impostor wins by sabotage\r
-4. Team Crew wins by tasks (also possible if the whole Crew is dead)\r
-5. Lovers among the last three players win\r
-6. Team Jackal wins by outnumbering (When the team Jackal contains an equal or greater amount of players than the Crew and there are 0 Impostors left and team Jackal contains no Lover)\r
-7. Team Impostor wins by outnumbering (When the team Impostors contains an equal or greater amount of players than the Crew and there are 0 players of the team Jackal left and team Impostors contains no Lover)\r
-8. Team Crew wins by outnumbering (When there is no player of the team Jackal and the team Impostrs left)\r
+3. Arsonist win\r
+4. Team Impostor wins by sabotage\r
+5. Team Crew wins by tasks (also possible if the whole Crew is dead)\r
+6. Lovers among the last three players win\r
+7. Team Jackal wins by outnumbering (When the team Jackal contains an equal or greater amount of players than the Crew and there are 0 Impostors left and team Jackal contains no Lover)\r
+8. Team Impostor wins by outnumbering (When the team Impostors contains an equal or greater amount of players than the Crew and there are 0 players of the team Jackal left and team Impostors contains no Lover)\r
+9. Team Crew wins by outnumbering (When there is no player of the team Jackal and the team Impostrs left)\r
 \r
 **NOTE:**\r
 - The Jackal (and his Sidekick) may be killed by a Sheriff.\r
@@ -881,7 +912,7 @@ You can set whether the Sheriff can kill the Spy or not (in order to keep the li
 ## Security Guard\r
 ### **Team: Crewmates**\r
 The Security Guard is a Crewmate that has a certain amount of screws that he can use for either sealing vents or for placing new cameras.\\r
-Placing a new camera and sealing vents takes a configurable amount of screws. The total number of screws that a SecurityGuard has can also be configured.\\r
+Placing a new camera and sealing vents takes a configurable amount of screws. The total number of screws that a Security Guard has can also be configured.\\r
 The new camera will be visible after the next meeting and accessible by everyone.\\r
 The vents will be sealed after the next meeting, players can't enter or exit sealed vents, but they can still "move to them" underground.\\r
 **NOTE:**\r
diff --git a/Source Code/AUFixes.cs b/Source Code/AUFixes.cs
new file mode 100644 (file)
index 0000000..74dc6b9
--- /dev/null
@@ -0,0 +1,43 @@
+using HarmonyLib;
+using Hazel;
+using UnityEngine;
+using TMPro;
+using System;
+
+namespace TheOtherRoles {
+    [HarmonyPatch(typeof(OpenDoorConsole), nameof(OpenDoorConsole.Use))]
+    class ToiletDoorFix {
+        public static void Prefix(OpenDoorConsole __instance) {
+            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.OpenToiletDoor, SendOption.None, -1);
+            writer.Write(__instance.MyDoor.Id);
+            AmongUsClient.Instance.FinishRpcImmediately(writer);
+        }
+    }
+
+    [HarmonyPatch(typeof(MatchMaker), nameof(MatchMaker.Start))]
+    public static class MatchMakerPatch { // Reactor Region Fix
+        public static void Prefix(MatchMaker __instance) {
+            var parent = __instance.GetComponentInParent<Transform>().parent;
+            if (parent == null) return; // Local
+            var tmps = parent.GetComponentsInChildren<TextMeshPro>();
+            foreach (var tmp in tmps) {
+                if (tmp.name == "RegionText_TMP") {
+                    var region = DestroyableSingleton<ServerManager>.Instance.CurrentRegion;
+                    var name = DestroyableSingleton<TranslationController>.Instance.GetStringWithDefault(region.TranslateName, region.Name, Array.Empty<Il2CppSystem.Object>());
+                    tmp.text = name;
+                    break;
+                }
+            }
+        }
+    }
+
+    [HarmonyPatch(typeof(NotificationPopper), nameof(NotificationPopper.Update))]
+    public static class NotificationPopperUpdatePatch { // Reactor Region Fix
+        public static void Postfix(NotificationPopper __instance) {
+            if (__instance.alphaTimer > 0f) {
+                var pos = __instance.transform.localPosition;
+                __instance.transform.localPosition = new Vector3(pos.x + 0.5f, pos.y, pos.z);
+            }
+        }
+    }
+}
\ No newline at end of file
index 368d18b12faf0040e54600abc3daf0e5bd122dcd..3fb0eebd6419016abe70eca7282643277bb5352e 100644 (file)
@@ -31,6 +31,7 @@ namespace TheOtherRoles
         public static CustomButton cleanerCleanButton;
         public static CustomButton warlockCurseButton;
         public static CustomButton securityGuardButton;
+        public static CustomButton arsonistButton;
         public static TMPro.TMP_Text securityGuardButtonScrewsText;
 
         public static void setCustomButtonCooldowns() {
@@ -56,6 +57,7 @@ namespace TheOtherRoles
             cleanerCleanButton.MaxTimer = Cleaner.cooldown;
             warlockCurseButton.MaxTimer = Warlock.cooldown;
             securityGuardButton.MaxTimer = SecurityGuard.cooldown;
+            arsonistButton.MaxTimer = Arsonist.cooldown;
 
             timeMasterShieldButton.EffectDuration = TimeMaster.shieldDuration;
             hackerButton.EffectDuration = Hacker.duration;
@@ -63,7 +65,8 @@ namespace TheOtherRoles
             lighterButton.EffectDuration = Lighter.duration; 
             camouflagerButton.EffectDuration = Camouflager.duration;
             morphlingButton.EffectDuration = Morphling.duration;
-            lightsOutButton.EffectDuration = Trickster.lightsOutDuration; 
+            lightsOutButton.EffectDuration = Trickster.lightsOutDuration;
+            arsonistButton.EffectDuration = Arsonist.duration;
 
             // Already set the timer to the max, as the button is enabled during the game and not available at the start
             lightsOutButton.Timer = lightsOutButton.MaxTimer;
@@ -170,10 +173,8 @@ namespace TheOtherRoles
 
                     byte targetId = 0;
                     if ((Sheriff.currentTarget.Data.IsImpostor && (Sheriff.currentTarget != Child.child || Child.isGrownUp())) || 
-                        Sheriff.currentTarget == Jackal.jackal || 
-                        Sheriff.currentTarget == Sidekick.sidekick || 
-                        (Sheriff.spyCanDieToSheriff && Spy.spy != null && Spy.spy == Sheriff.currentTarget) ||
-                        (Sheriff.jesterCanDieToSheriff && Jester.jester != null && Jester.jester == Sheriff.currentTarget)) {
+                        (Sheriff.spyCanDieToSheriff && Spy.spy == Sheriff.currentTarget) ||
+                        (Sheriff.canKillNeutrals && (Jester.jester == Sheriff.currentTarget || Jackal.jackal == Sheriff.currentTarget || Sidekick.sidekick == Sheriff.currentTarget))) {
                         targetId = Sheriff.currentTarget.PlayerId;
                     }
                     else {
@@ -679,7 +680,7 @@ namespace TheOtherRoles
                 KeyCode.F
             );
 
-            // SecurityGuard button
+            // Security Guard button
             securityGuardButton = new CustomButton(
                 () => {
                     if (SecurityGuard.ventTarget != null) { // Seal vent
@@ -701,7 +702,7 @@ namespace TheOtherRoles
                     }
                     securityGuardButton.Timer = securityGuardButton.MaxTimer;
                 },
-                () => { return SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => { return SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead && SecurityGuard.remainingScrews >= Mathf.Min(SecurityGuard.ventPrice, SecurityGuard.camPrice); },
                 () => {
                     securityGuardButton.killButtonManager.renderer.sprite = (SecurityGuard.ventTarget == null && PlayerControl.GameOptions.MapId != 1) ? SecurityGuard.getPlaceCameraButtonSprite() : SecurityGuard.getCloseVentButtonSprite(); 
                     if (securityGuardButtonScrewsText != null) securityGuardButtonScrewsText.text = $"{SecurityGuard.remainingScrews}/{SecurityGuard.totalScrews}";
@@ -716,14 +717,70 @@ namespace TheOtherRoles
                 __instance,
                 KeyCode.Q
             );
-
-            // SecurityGuard button screws counter
+            
+            // Security Guard button screws counter
             securityGuardButtonScrewsText = GameObject.Instantiate(securityGuardButton.killButtonManager.TimerText, securityGuardButton.killButtonManager.TimerText.transform.parent);
             securityGuardButtonScrewsText.text = "";
             securityGuardButtonScrewsText.enableWordWrapping = false;
             securityGuardButtonScrewsText.transform.localScale = Vector3.one * 0.5f;
             securityGuardButtonScrewsText.transform.localPosition += new Vector3(-0.05f, 0.7f, 0);
 
+            // Arsonist button
+            arsonistButton = new CustomButton(
+                () => {
+                    bool dousedEveryoneAlive = Arsonist.dousedEveryoneAlive();
+                    if (dousedEveryoneAlive) {
+                        MessageWriter winWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ArsonistWin, Hazel.SendOption.Reliable, -1);
+                        AmongUsClient.Instance.FinishRpcImmediately(winWriter);
+                        RPCProcedure.arsonistWin();
+                        arsonistButton.HasEffect = false;
+                    } else if (Arsonist.currentTarget != null) {
+                        Arsonist.douseTarget = Arsonist.currentTarget;
+                        arsonistButton.HasEffect = true;              
+                    }
+                },
+                () => { return Arsonist.arsonist != null && Arsonist.arsonist == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => {
+                    bool dousedEveryoneAlive = Arsonist.dousedEveryoneAlive();
+                    if (dousedEveryoneAlive) arsonistButton.killButtonManager.renderer.sprite = Arsonist.getIgniteSprite();
+                    
+                    if (arsonistButton.isEffectActive && Arsonist.douseTarget != Arsonist.currentTarget) {
+                        Arsonist.douseTarget = null;
+                        arsonistButton.Timer = 0f;
+                        arsonistButton.isEffectActive = false;
+                    }
+
+                    return PlayerControl.LocalPlayer.CanMove && (dousedEveryoneAlive || Arsonist.currentTarget != null);
+                },
+                () => {
+                    arsonistButton.Timer = arsonistButton.MaxTimer;
+                    arsonistButton.isEffectActive = false;
+                    Arsonist.douseTarget = null;
+                },
+                Arsonist.getDouseSprite(),
+                new Vector3(-1.3f, 0f, 0f),
+                __instance,
+                KeyCode.Q,
+                true,
+                Arsonist.duration,
+                () => {
+                    if (Arsonist.douseTarget != null) Arsonist.dousedPlayers.Add(Arsonist.douseTarget);
+                    Arsonist.douseTarget = null;
+                    arsonistButton.Timer = Arsonist.dousedEveryoneAlive() ? 0 : arsonistButton.MaxTimer;
+
+                    int playerCounter = 0;
+                    Vector3 bottomLeft = new Vector3(-HudManager.Instance.UseButton.transform.localPosition.x, HudManager.Instance.UseButton.transform.localPosition.y, HudManager.Instance.UseButton.transform.localPosition.z);
+                    bottomLeft += new Vector3(-0.25f, -0.25f, 0);
+                    foreach (PlayerControl p in Arsonist.dousedPlayers) {
+                        if (Arsonist.dousedIcons.ContainsKey(p.PlayerId)) {
+                            Arsonist.dousedIcons[p.PlayerId].gameObject.SetActive(true);
+                            Arsonist.dousedIcons[p.PlayerId].transform.localPosition = bottomLeft + Vector3.right * playerCounter * 0.35f;
+                            playerCounter++;
+                        }
+                    }
+                }
+            );
+
             // Set the default (or settings from the previous game) timers/durations when spawning the buttons
             setCustomButtonCooldowns();
         }
index 98d3daf45f95f56b734c7dd5ae2879a2d450846e..0ff3db2e8ed31a624506329d270409c59094f80d 100644 (file)
@@ -18,21 +18,44 @@ namespace TheOtherRoles {
                 bool handled = false;
                 if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) {
                     using(MD5 md5 = MD5.Create()) {
-                        string hash = System.BitConverter.ToString(md5.ComputeHash(Encoding.UTF8.GetBytes(text.ToLower()))).Replace("-", "").ToLowerInvariant();
-                        if (hash.Equals("f92af861c8b7aa5f2d05165abc9ba04f")) {
+                        string hash = System.BitConverter.ToString(md5.ComputeHash(Encoding.UTF8.GetBytes("tor@" + text.ToLower() + "§eof"))).Replace("-", "").ToLowerInvariant();
+                        if (hash.Equals("a4eb05314008537d2832e32fa1f33b2e")) { // i am a cheater
                             handled = true;
                             byte colorId = (byte)CustomColors.pickableColors;
                             SaveManager.BodyColor = (byte)colorId;
                             if (PlayerControl.LocalPlayer)
                                 PlayerControl.LocalPlayer.CmdCheckColor(colorId);
-                        } else if (hash.Equals("5a2ee6de2e42d2b326556640f838b0d7")) {
+                        } else if (hash.Equals("80cc70dc5f21bc321b84ce984abd511b")) { // i dont understand hashes
                             handled = true;
                             byte colorId = (byte)(CustomColors.pickableColors + 1);
                             SaveManager.BodyColor = (byte)colorId;
                             if (PlayerControl.LocalPlayer)
                                 PlayerControl.LocalPlayer.CmdCheckColor(colorId);
-                        }
-                        System.Console.WriteLine(hash);
+                        } else if (hash.Equals("3359ffcd0b14ffa39d476a5c96632032")) { // Batch 2
+                            handled = true;
+                            byte colorId = (byte)(CustomColors.pickableColors + 2);
+                            SaveManager.BodyColor = (byte)colorId;
+                            if (PlayerControl.LocalPlayer)
+                                PlayerControl.LocalPlayer.CmdCheckColor(colorId);
+                        } else if (hash.Equals("14056e0b9e53bc91f0c6a8b1fd5ce8b5")) {
+                            handled = true;
+                            byte colorId = (byte)(CustomColors.pickableColors + 3);
+                            SaveManager.BodyColor = (byte)colorId;
+                            if (PlayerControl.LocalPlayer)
+                                PlayerControl.LocalPlayer.CmdCheckColor(colorId);
+                        } else if (hash.Equals("fb00fb81b0be5177af908576e144d788")) {
+                            handled = true;
+                            byte colorId = (byte)(CustomColors.pickableColors + 4);
+                            SaveManager.BodyColor = (byte)colorId;
+                            if (PlayerControl.LocalPlayer)
+                                PlayerControl.LocalPlayer.CmdCheckColor(colorId);
+                        } else if (hash.Equals("a79e2bd7c9cdc723924bd4d7734ae5da")) { // Eisbison Color
+                            handled = true;
+                            byte colorId = (byte)(CustomColors.pickableColors + 5);
+                            SaveManager.BodyColor = (byte)colorId;
+                            if (PlayerControl.LocalPlayer)
+                                PlayerControl.LocalPlayer.CmdCheckColor(colorId);
+                        } 
                     }
                 }
                 if (handled) {
index 8a314f65933aa9817a6d798e6f01edb208b91421..df950a2b8e5c0a515512bede13d6c7af83040c4b 100644 (file)
@@ -11,7 +11,7 @@ namespace TheOtherRoles
     [HarmonyPatch]
     public static class CredentialsPatch {
         public static string fullCredentials = 
-$@"<color=#FCCE03FF>TheOtherRoles</color> v{TheOtherRolesPlugin.Major}.{TheOtherRolesPlugin.Minor}.{TheOtherRolesPlugin.Patch}:
+$@"<color=#FCCE03FF>TheOtherRoles</color> v{TheOtherRolesPlugin.Version.ToString()}:
 - Modded by <color=#FCCE03FF>Eisbison</color>,
   <color=#FFEB91FF>Thunderstorm584</color> and <color=#FFEB91FF>EndOfFile</color>
 - Balanced with <color=#FFEB91FF>Dhalucard</color>
index 1dd2f6de42359e5aa52056cc92031945efede0bb..cc1e49dd05010bc6dd7bb0f6e13356a27cbcfa14 100644 (file)
@@ -81,6 +81,25 @@ namespace TheOtherRoles {
                                         color = new Color32(64, 8, 71, 0), 
                                         shadow = new Color32(24, 32, 116, 0),
                                         isLighterColor = false });
+
+            /** Batch 2 **/
+            colors.Add(new CustomColor { longname = "Electric", shortname = "ELEC", 
+                                        color = new Color32(0xDB, 0xFD, 0x2F, 0), 
+                                        shadow = new Color32(0x89, 0x9E, 0x1E, 0),
+                                        isLighterColor = true });
+            colors.Add(new CustomColor { longname = "Signal Orange", shortname = "SIGN", 
+                                        color = new Color32(0xF7, 0x44, 0x17, 0), 
+                                        shadow = new Color32(0x9B, 0x2E, 0x0F, 0),
+                                        isLighterColor = true });        
+            colors.Add(new CustomColor { longname = "Navy Blue", shortname = "NAVY", 
+                                        color = new Color32(0x35, 0x7B, 0x9E, 0), 
+                                        shadow = new Color32(0x16, 0x2B, 0x62, 0),
+                                        isLighterColor = false });      
+            colors.Add(new CustomColor { longname = "Eisbison", shortname = "EIS", 
+                                        color = new Color32(0xA8, 0xDF, 0xFF, 0), 
+                                        shadow = new Color32(0x59, 0x9F, 0xC8, 0),
+                                        isLighterColor = true });                         
+
             /** Add Colors **/
             int id = 50000;
             foreach (CustomColor cc in colors) {
index a5a29e9b839094f363151de48bd5cca0c68cc180..3a74d8d339beef2a9b885dc644eed7924842c516 100644 (file)
@@ -235,7 +235,7 @@ namespace TheOtherRoles {
                     HatExtension ext = hats[i].Item2;
 
                     float xpos = __instance.XRange.Lerp((i % __instance.NumPerRow) / (__instance.NumPerRow - 1f));
-                    float ypos = offset - i / __instance.NumPerRow * (isDefaultPackage ? 1f : 1.5f) * __instance.YOffset;
+                    float ypos = offset - (i / __instance.NumPerRow) * (isDefaultPackage ? 1f : 1.5f) * __instance.YOffset;
                     ColorChip colorChip = UnityEngine.Object.Instantiate<ColorChip>(__instance.ColorTabPrefab, __instance.scroller.Inner);
                     if (ext != null) {
                         Transform background = colorChip.transform.FindChild("Background");
@@ -271,7 +271,7 @@ namespace TheOtherRoles {
                     colorChip.Tag = hat;
                     __instance.ColorChips.Add(colorChip);
                 }
-                return offset - ((hats.Count - 1) / __instance.NumPerRow * (isDefaultPackage ? 1f : 1.5f) + 0.7f) * __instance.YOffset;
+                return offset - ((hats.Count - 1) / __instance.NumPerRow) * (isDefaultPackage ? 1f : 1.5f) * __instance.YOffset - 0.85f;
             }
 
             public static bool Prefix(HatsTab __instance) {
@@ -306,12 +306,16 @@ namespace TheOtherRoles {
                     textTemplate = hatButton.transform.FindChild("ButtonText_TMP").GetComponent<TMPro.TMP_Text>();
                 }
 
-                var orderedKeys = packages.Keys.OrderBy((string x) => x == innerslothPackageName ? 1000 : 0);
+                var orderedKeys = packages.Keys.OrderBy((string x) => {
+                    if (x == innerslothPackageName) return 1000;
+                    if (x == "Developer Hats") return 0;
+                    return 500;
+                });
                 foreach (string key in orderedKeys) {
                     List<System.Tuple<HatBehaviour, HatExtension>> value = packages[key];
-                    YOffset += createHatPackage(value, key, YOffset, __instance);
+                    YOffset = createHatPackage(value, key, YOffset, __instance);
                 }
-                __instance.scroller.YBounds.max = (YOffset - 0.6f) * -0.75f;
+                __instance.scroller.YBounds.max = YOffset * -0.875f; // probably needs to fix up the entire messed math to solve this correctly
                 return false;
             }
         }
@@ -332,30 +336,30 @@ namespace TheOtherRoles {
     }
 
     public class CustomHatLoader {
-        public bool running = false;
+        public static bool running = false;
         private const string REPO = "https://raw.githubusercontent.com/Eisbison/TheOtherHats/master";
 
         public static List<CustomHatOnline> hatdetails = new List<CustomHatOnline>();
-
-        public void LaunchHatFetcher() {
-            if (this.running)
+        private static Task hatFetchTask = null;
+        public static void LaunchHatFetcher() {
+            if (running)
                 return;
-            this.running = true;
-            this.LaunchHatFetcherAsync();
+            running = true;
+            hatFetchTask = LaunchHatFetcherAsync();
         }
 
-        private async void LaunchHatFetcherAsync() {
+        private static async Task LaunchHatFetcherAsync() {
             try {
-                HttpStatusCode status = await this.FetchHats();
+                HttpStatusCode status = await FetchHats();
                 if (status != HttpStatusCode.OK)
                     System.Console.WriteLine("Custom Hats could not be loaded\n");
             } catch (System.Exception e) {
                 System.Console.WriteLine("Unable to fetch hats\n" + e.Message);
             }
-            this.running = false;
+            running = false;
         }
 
-        private string sanitizeResourcePath(string res) {
+        private static string sanitizeResourcePath(string res) {
             if (res == null || !res.EndsWith(".png")) 
                 return null;
 
@@ -366,91 +370,78 @@ namespace TheOtherRoles {
             return res;
         }
 
-        public async Task<HttpStatusCode> FetchHats() {
-            using (HttpClient http = new HttpClient()) {
-                http.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue{ NoCache = true };
-                using (HttpRequestMessage msg = new HttpRequestMessage()) {
-                    msg.Method = HttpMethod.Get;
-                    msg.RequestUri = new System.Uri($"{REPO}/CustomHats.json");
-                    HttpResponseMessage httpResponseMessage = await http.SendAsync(msg);
-                    using (HttpResponseMessage res = httpResponseMessage) {
-                        try {
-                            if (res.Content == null) {
-                                System.Console.WriteLine("Server returned no data: " + res.StatusCode.ToString());
-                                return HttpStatusCode.ExpectationFailed;
-                            }
-                            if (res.StatusCode != HttpStatusCode.OK) 
-                                return res.StatusCode;
-
-                            string json = await res.Content.ReadAsStringAsync();
-                            // System.Console.WriteLine(json);
-
-                            JToken jobj = JObject.Parse(json)["hats"];
-                            if (!jobj.HasValues) 
-                                return HttpStatusCode.ExpectationFailed;
-
-                            List<CustomHatOnline> hatdatas = new List<CustomHatOnline>();
-
-                            for (JToken current = jobj.First; current != null; current = current.Next) {
-                                if (current.HasValues) {
-                                    CustomHatOnline info = new CustomHatOnline();
-
-                                    info.name = current["name"]?.ToString();
-                                    info.resource = sanitizeResourcePath(current["resource"]?.ToString());
-                                    if (info.resource == null || info.name == null) // required
-                                        continue;
-                                    info.reshasha = current["reshasha"]?.ToString();
-                                    info.backresource = sanitizeResourcePath(current["backresource"]?.ToString());
-                                    info.reshashb = current["reshashb"]?.ToString();
-                                    info.climbresource = sanitizeResourcePath(current["climbresource"]?.ToString());
-                                    info.reshashc = current["reshashc"]?.ToString();
-                                    info.author = current["author"]?.ToString();
-                                    info.package = current["package"]?.ToString();
-                                    info.condition = current["condition"]?.ToString();
-                                    info.bounce = current["bounce"] != null;
-                                    info.adaptive = current["adaptive"] != null;
-                                    info.behind = current["behind"] != null;
-                                    hatdatas.Add(info);
-                                }
-                            }
-
-                            List<string> markedfordownload = new List<string>();
-
-                            string filePath = Path.GetDirectoryName(Application.dataPath) + @"\TheOtherHats\";
-                            using (MD5 md5 = MD5.Create()) {
-                                foreach (CustomHatOnline data in hatdatas) {
-                                   if (doesResourceRequireDownload(filePath + data.resource, data.reshasha, md5))
-                                        markedfordownload.Add(data.resource);
-                                   if (data.backresource != null && doesResourceRequireDownload(filePath + data.backresource, data.reshashb, md5))
-                                        markedfordownload.Add(data.backresource);
-                                   if (data.climbresource != null && doesResourceRequireDownload(filePath + data.climbresource, data.reshashc, md5))
-                                        markedfordownload.Add(data.climbresource);
-                                }
-                            }
-                            markedfordownload.AsParallel().ForAll(file => {
-                                System.Uri url = new System.Uri($"{REPO}/hats/{file}");
-                                // System.Console.WriteLine($"Downloading {file}");
-                                var fres = http.GetAsync(url, HttpCompletionOption.ResponseContentRead);
-                                if (fres.Result.StatusCode != HttpStatusCode.OK)
-                                    return;
-                                using (var resStream = fres.Result.Content.ReadAsStreamAsync().Result) {
-                                    using (var fileStream = File.Create($"{filePath}\\{file}")) {
-                                        resStream.CopyTo(fileStream);
-                                    }
-                                }
-                            });
-
-                            hatdetails = hatdatas;
-                        } catch (System.Exception ex) {
-                            System.Console.WriteLine(ex);
+        public static async Task<HttpStatusCode> FetchHats() {
+            HttpClient http = new HttpClient();
+            http.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue{ NoCache = true };
+                       var response = await http.GetAsync(new System.Uri($"{REPO}/CustomHats.json"), HttpCompletionOption.ResponseContentRead);
+            try {
+                if (response.StatusCode != HttpStatusCode.OK) return response.StatusCode;
+                if (response.Content == null) {
+                    System.Console.WriteLine("Server returned no data: " + response.StatusCode.ToString());
+                    return HttpStatusCode.ExpectationFailed;
+                }
+                string json = await response.Content.ReadAsStringAsync();
+                JToken jobj = JObject.Parse(json)["hats"];
+                if (!jobj.HasValues) return HttpStatusCode.ExpectationFailed;
+
+                List<CustomHatOnline> hatdatas = new List<CustomHatOnline>();
+
+                for (JToken current = jobj.First; current != null; current = current.Next) {
+                    if (current.HasValues) {
+                        CustomHatOnline info = new CustomHatOnline();
+
+                        info.name = current["name"]?.ToString();
+                        info.resource = sanitizeResourcePath(current["resource"]?.ToString());
+                        if (info.resource == null || info.name == null) // required
+                            continue;
+                        info.reshasha = current["reshasha"]?.ToString();
+                        info.backresource = sanitizeResourcePath(current["backresource"]?.ToString());
+                        info.reshashb = current["reshashb"]?.ToString();
+                        info.climbresource = sanitizeResourcePath(current["climbresource"]?.ToString());
+                        info.reshashc = current["reshashc"]?.ToString();
+                        info.author = current["author"]?.ToString();
+                        info.package = current["package"]?.ToString();
+                        info.condition = current["condition"]?.ToString();
+                        info.bounce = current["bounce"] != null;
+                        info.adaptive = current["adaptive"] != null;
+                        info.behind = current["behind"] != null;
+                        hatdatas.Add(info);
+                    }
+                }
+
+                List<string> markedfordownload = new List<string>();
+
+                string filePath = Path.GetDirectoryName(Application.dataPath) + @"\TheOtherHats\";
+                MD5 md5 = MD5.Create();
+                foreach (CustomHatOnline data in hatdatas) {
+                   if (doesResourceRequireDownload(filePath + data.resource, data.reshasha, md5))
+                        markedfordownload.Add(data.resource);
+                   if (data.backresource != null && doesResourceRequireDownload(filePath + data.backresource, data.reshashb, md5))
+                        markedfordownload.Add(data.backresource);
+                   if (data.climbresource != null && doesResourceRequireDownload(filePath + data.climbresource, data.reshashc, md5))
+                        markedfordownload.Add(data.climbresource);
+                }
+                
+                foreach(var file in markedfordownload) {
+                    
+                    var hatFileResponse = await http.GetAsync($"{REPO}/hats/{file}", HttpCompletionOption.ResponseContentRead);
+                    if (hatFileResponse.StatusCode != HttpStatusCode.OK) continue;
+                    using (var responseStream = await hatFileResponse.Content.ReadAsStreamAsync()) {
+                        using (var fileStream = File.Create($"{filePath}\\{file}")) {
+                            responseStream.CopyTo(fileStream);
                         }
                     }
                 }
+
+                hatdetails = hatdatas;
+            } catch (System.Exception ex) {
+                TheOtherRolesPlugin.Instance.Log.LogError(ex.ToString());
+                System.Console.WriteLine(ex);
             }
             return HttpStatusCode.OK;
         }
 
-        private bool doesResourceRequireDownload(string respath, string reshash, MD5 md5) {
+        private static bool doesResourceRequireDownload(string respath, string reshash, MD5 md5) {
             if (reshash == null || !File.Exists(respath))
                 return true;
 
index fa60fb7ded099f4a27a689e9efb18b94f902dc3e..c777838355ff036cf34b946f81cbaa17e6b0547d 100644 (file)
@@ -12,12 +12,13 @@ using static TheOtherRoles.TheOtherRoles;
 namespace TheOtherRoles {
     public class CustomOptionHolder {
         public static string[] rates = new string[]{"0%", "10%", "20%", "30%", "40%", "50%", "60%", "70%", "80%", "90%", "100%"};
-        public static string[] crewmateRoleCaps = new string[]{"0", "0-1", "1", "1-2", "2", "2-3", "3", "3-4", "4", "4-5", "5", "5-6", "6", "6-7", "7", "7-8", "8", "8-9", "9", "9-10", "10", "10-11", "11", "11-12", "12", "12-13", "13", "13-14", "14", "14-15", "15"};
+        public static string[] crewmateAndNeutralRoleCaps = new string[]{"0", "0-1", "1", "1-2", "2", "2-3", "3", "3-4", "4", "4-5", "5", "5-6", "6", "6-7", "7", "7-8", "8", "8-9", "9", "9-10", "10", "10-11", "11", "11-12", "12", "12-13", "13", "13-14", "14", "14-15", "15"};
         public static string[] impostorRoleCaps = new string[]{"0", "0-1", "1", "1-2", "2", "2-3", "3"};
         public static string[] presets = new string[]{"Preset 1", "Preset 2", "Preset 3", "Preset 4", "Preset 5"};
 
         public static CustomOption presetSelection;
         public static CustomOption crewmateRolesCount;
+        public static CustomOption neutralRolesCount;
         public static CustomOption impostorRolesCount;
 
         public static CustomOption mafiaSpawnRate;
@@ -50,6 +51,21 @@ namespace TheOtherRoles {
         public static CustomOption jesterSpawnRate;
         public static CustomOption jesterCanCallEmergency;
 
+        public static CustomOption arsonistSpawnRate;
+        public static CustomOption arsonistCooldown;
+        public static CustomOption arsonistDuration;
+
+        public static CustomOption jackalSpawnRate;
+        public static CustomOption jackalKillCooldown;
+        public static CustomOption jackalCreateSidekickCooldown;
+        public static CustomOption jackalCanUseVents;
+        public static CustomOption jackalCanCreateSidekick;
+        public static CustomOption sidekickPromotesToJackal;
+        public static CustomOption sidekickCanKill;
+        public static CustomOption sidekickCanUseVents;
+        public static CustomOption jackalPromotedFromSidekickCanCreateSidekick;
+        public static CustomOption jackalCanCreateSidekickFromImpostor;
+
         public static CustomOption shifterSpawnRate;
 
         public static CustomOption mayorSpawnRate;
@@ -58,7 +74,7 @@ namespace TheOtherRoles {
 
         public static CustomOption sheriffSpawnRate;
         public static CustomOption sheriffCooldown;
-        public static CustomOption jesterCanDieToSheriff;
+        public static CustomOption sheriffCanKillNeutrals;
 
         public static CustomOption lighterSpawnRate;
         public static CustomOption lighterModeLightsOnVision;
@@ -101,17 +117,6 @@ namespace TheOtherRoles {
         public static CustomOption snitchSpawnRate;
         public static CustomOption snitchLeftTasksForImpostors;
 
-        public static CustomOption jackalSpawnRate;
-        public static CustomOption jackalKillCooldown;
-        public static CustomOption jackalCreateSidekickCooldown;
-        public static CustomOption jackalCanUseVents;
-        public static CustomOption jackalCanCreateSidekick;
-        public static CustomOption sidekickPromotesToJackal;
-        public static CustomOption sidekickCanKill;
-        public static CustomOption sidekickCanUseVents;
-        public static CustomOption jackalPromotedFromSidekickCanCreateSidekick;
-        public static CustomOption jackalCanCreateSidekickFromImpostor;
-
         public static CustomOption spySpawnRate;
         public static CustomOption spyCanDieToSheriff;
         public static CustomOption spyImpostorsCanKillAnyone;
@@ -155,7 +160,8 @@ namespace TheOtherRoles {
             // Role Options
             presetSelection = CustomOption.Create(0, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Preset"), presets, null, true);
 
-            crewmateRolesCount = CustomOption.Create(1, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Number Of Crewmate/Neutral Roles"), crewmateRoleCaps, null, true);
+            crewmateRolesCount = CustomOption.Create(1, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Number Of Crewmate Roles"), crewmateAndNeutralRoleCaps, null, true);
+            neutralRolesCount = CustomOption.Create(7, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Number Of Neutral Roles"), crewmateAndNeutralRoleCaps);
             impostorRolesCount = CustomOption.Create(2, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Number Of Impostor Roles"), impostorRoleCaps);
 
             mafiaSpawnRate = CustomOption.Create(10, cs(Janitor.color, "Mafia"), rates, null, true);
@@ -200,6 +206,21 @@ namespace TheOtherRoles {
             jesterSpawnRate = CustomOption.Create(60, cs(Jester.color, "Jester"), rates, null, true);
             jesterCanCallEmergency = CustomOption.Create(61, "Jester can call emergency meeting", true, jesterSpawnRate);
 
+            arsonistSpawnRate = CustomOption.Create(290, cs(Arsonist.color, "Arsonist"), rates, null, true);
+            arsonistCooldown = CustomOption.Create(291, "Arsonist Cooldown", 30f, 5f, 60f, 2.5f, arsonistSpawnRate);
+            arsonistDuration = CustomOption.Create(292, "Arsonist Douse Duration", 3f, 1f, 10f, 1f, arsonistSpawnRate);
+
+            jackalSpawnRate = CustomOption.Create(220, cs(Jackal.color, "Jackal"), rates, null, true);
+            jackalKillCooldown = CustomOption.Create(221, "Jackal/Sidekick Kill Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate);
+            jackalCreateSidekickCooldown = CustomOption.Create(222, "Jackal Create Sidekick Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate);
+            jackalCanUseVents = CustomOption.Create(223, "Jackal Can Use Vents", true, jackalSpawnRate);
+            jackalCanCreateSidekick = CustomOption.Create(224, "Jackal Can Create A Sidekick", false, jackalSpawnRate);
+            sidekickPromotesToJackal = CustomOption.Create(225, "Sidekick Gets Promoted To Jackal On Jackal Death", false, jackalSpawnRate);
+            sidekickCanKill = CustomOption.Create(226, "Sidekick Can Kill", false, jackalSpawnRate);
+            sidekickCanUseVents = CustomOption.Create(227, "Sidekick Can Use Vents", true, jackalSpawnRate);
+            jackalPromotedFromSidekickCanCreateSidekick = CustomOption.Create(228, "Jackals Promoted From Sidekick Can Create A Sidekick", true, jackalSpawnRate);
+            jackalCanCreateSidekickFromImpostor = CustomOption.Create(229, "Jackals Can Make An Impostor To His Sidekick", true, jackalSpawnRate);
+
             shifterSpawnRate = CustomOption.Create(70, cs(Shifter.color, "Shifter"), rates, null, true);
 
             mayorSpawnRate = CustomOption.Create(80, cs(Mayor.color, "Mayor"), rates, null, true);
@@ -208,7 +229,7 @@ namespace TheOtherRoles {
 
             sheriffSpawnRate = CustomOption.Create(100, cs(Sheriff.color, "Sheriff"), rates, null, true);
             sheriffCooldown = CustomOption.Create(101, "Sheriff Cooldown", 30f, 10f, 60f, 2.5f, sheriffSpawnRate);
-            jesterCanDieToSheriff = CustomOption.Create(102, "Sheriff Can Kill The Jester", false, sheriffSpawnRate);
+            sheriffCanKillNeutrals = CustomOption.Create(102, "Neutrals Can Kill The Jester", false, sheriffSpawnRate);
 
 
             lighterSpawnRate = CustomOption.Create(110, cs(Lighter.color, "Lighter"), rates, null, true);
@@ -252,17 +273,6 @@ namespace TheOtherRoles {
             snitchSpawnRate = CustomOption.Create(210, cs(Snitch.color, "Snitch"), rates, null, true);
             snitchLeftTasksForImpostors = CustomOption.Create(211, "Task Count Where Impostors See Snitch", 1f, 0f, 5f, 1f, snitchSpawnRate);
 
-            jackalSpawnRate = CustomOption.Create(220, cs(Jackal.color, "Jackal"), rates, null, true);
-            jackalKillCooldown = CustomOption.Create(221, "Jackal/Sidekick Kill Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate);
-            jackalCreateSidekickCooldown = CustomOption.Create(222, "Jackal Create Sidekick Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate);
-            jackalCanUseVents = CustomOption.Create(223, "Jackal Can Use Vents", true, jackalSpawnRate);
-            jackalCanCreateSidekick = CustomOption.Create(224, "Jackal Can Create A Sidekick", false, jackalSpawnRate);
-            sidekickPromotesToJackal = CustomOption.Create(225, "Sidekick Gets Promoted To Jackal On Jackal Death", false, jackalSpawnRate);
-            sidekickCanKill = CustomOption.Create(226, "Sidekick Can Kill", false, jackalSpawnRate);
-            sidekickCanUseVents = CustomOption.Create(227, "Sidekick Can Use Vents", true, jackalSpawnRate);
-            jackalPromotedFromSidekickCanCreateSidekick = CustomOption.Create(228, "Jackals Promoted From Sidekick Can Create A Sidekick", true, jackalSpawnRate);
-            jackalCanCreateSidekickFromImpostor = CustomOption.Create(229, "Jackals Can Make An Impostor To His Sidekick", true, jackalSpawnRate);
-
             spySpawnRate = CustomOption.Create(240, cs(Spy.color, "Spy"), rates, null, true);
             spyCanDieToSheriff = CustomOption.Create(241, "Spy Can Die To Sheriff", false, spySpawnRate);
             spyImpostorsCanKillAnyone = CustomOption.Create(242, "Impostors Can Kill Anyone If There Is A Spy", true, spySpawnRate);
@@ -528,7 +538,7 @@ namespace TheOtherRoles {
             var hudString = sb.ToString();
 
             int defaultSettingsLines = 19;
-            int roleSettingsLines = defaultSettingsLines + 30;
+            int roleSettingsLines = defaultSettingsLines + 32;
             int detailedSettingsP1 = roleSettingsLines + 34;
             int detailedSettingsP2 = detailedSettingsP1 + 36;
             int end1 = hudString.TakeWhile(c => (defaultSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count();
@@ -544,13 +554,13 @@ namespace TheOtherRoles {
                 int gap = 1;
                 int index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
                 hudString = hudString.Insert(index, "\n");
-                gap = 4;
+                gap = 5;
                 index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
                 hudString = hudString.Insert(index, "\n");
-                gap = 13;
+                gap = 16;
                 index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
                 hudString = hudString.Insert(index + 1, "\n");
-                gap = 16;
+                gap = 20;
                 index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
                 hudString = hudString.Insert(index + 1, "\n");
             } else if (counter == 2) {
index 02cfcd5b23d711402fb6f9d19f7148be9e26b781..d3aaf23ea90d7d8fec4f3d08a47f58c24f8f550b 100644 (file)
@@ -16,7 +16,8 @@ namespace TheOtherRoles {
         LoversWin = 10,
         TeamJackalWin = 11,
         ChildLose = 12,
-        JesterWin = 13
+        JesterWin = 13,
+        ArsonistWin = 14
     }
 
     enum WinCondition {
@@ -25,7 +26,8 @@ namespace TheOtherRoles {
         LoversSoloWin,
         JesterWin,
         JackalWin,
-        ChildLose
+        ChildLose,
+        ArsonistWin
     }
 
     static class AdditionalTempData {
@@ -52,33 +54,22 @@ namespace TheOtherRoles {
         public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)]ref GameOverReason reason, [HarmonyArgument(1)]bool showAd) {
             AdditionalTempData.clear();
 
-            // Remove Jester from winners (on Jester win he will be added again, see below)
-            if (Jester.jester != null) {
-                WinningPlayerData jesterWinner = null;
-                foreach (WinningPlayerData winner in  TempData.winners)
-                    if (winner.Name == Jester.jester.Data.PlayerName) jesterWinner = winner;
-                
-                if (jesterWinner != null) TempData.winners.Remove(jesterWinner);
-            }
-            // Remove Jackal and Sidekick from winners (on Jackal win he will be added again, see below)
-            if (Jackal.jackal != null || Sidekick.sidekick != null) {
-                List<WinningPlayerData> winnersToRemove = new List<WinningPlayerData>();
-                foreach (WinningPlayerData winner in TempData.winners) {
-                    if (winner.Name == Jackal.jackal?.Data?.PlayerName) winnersToRemove.Add(winner);
-                    if (winner.Name == Sidekick.sidekick?.Data?.PlayerName) winnersToRemove.Add(winner);
-                    foreach(var player in Jackal.formerJackals) {
-                        if (winner.Name == player.Data.PlayerName) {
-                            winnersToRemove.Add(winner);
-                        }
-                    }
-                }
-                
-                foreach (var winner in winnersToRemove) {
-                    TempData.winners.Remove(winner);
-                }
+            // Remove Jester, Arsonist, Jackal, former Jackals and Sidekick from winners (if they win, they'll be readded)
+            List<PlayerControl> notWinners = new List<PlayerControl>();
+            if (Jester.jester != null) notWinners.Add(Jester.jester);
+            if (Sidekick.sidekick != null) notWinners.Add(Sidekick.sidekick);
+            if (Jackal.jackal != null) notWinners.Add(Jackal.jackal);
+            if (Arsonist.arsonist != null) notWinners.Add(Arsonist.arsonist);
+            notWinners.AddRange(Jackal.formerJackals);
+
+            List<WinningPlayerData> winnersToRemove = new List<WinningPlayerData>();
+            foreach (WinningPlayerData winner in TempData.winners) {
+                if (notWinners.Any(x => x.Data.PlayerName == winner.Name)) winnersToRemove.Add(winner);
             }
+            foreach (var winner in winnersToRemove) TempData.winners.Remove(winner);
 
             bool jesterWin = Jester.jester != null && gameOverReason == (GameOverReason)CustomGameOverReason.JesterWin;
+            bool arsonistWin = Arsonist.arsonist != null && gameOverReason == (GameOverReason)CustomGameOverReason.ArsonistWin;
             bool childLose = Child.child != null && gameOverReason == (GameOverReason)CustomGameOverReason.ChildLose;
             bool loversWin = Lovers.existingAndAlive() && (gameOverReason == (GameOverReason)CustomGameOverReason.LoversWin || (TempData.DidHumansWin(gameOverReason) && Lovers.existingAndCrewLovers())); // Either they win if they are among the last 3 players, or they win if they are both Crewmates and both alive and the Crew wins (Team Imp/Jackal Lovers can only win solo wins)
             bool teamJackalWin = gameOverReason == (GameOverReason)CustomGameOverReason.TeamJackalWin && ((Jackal.jackal != null && !Jackal.jackal.Data.IsDead) || (Sidekick.sidekick != null && !Sidekick.sidekick.Data.IsDead));
@@ -100,6 +91,14 @@ namespace TheOtherRoles {
                 AdditionalTempData.winCondition = WinCondition.JesterWin;
             }
 
+            // Arsonist win
+            else if (arsonistWin) {
+                TempData.winners = new Il2CppSystem.Collections.Generic.List<WinningPlayerData>();
+                WinningPlayerData wpd = new WinningPlayerData(Arsonist.arsonist.Data);
+                TempData.winners.Add(wpd);
+                AdditionalTempData.winCondition = WinCondition.ArsonistWin;
+            }
+
             // Lovers win conditions
             else if (loversWin) {
                 AdditionalTempData.localIsLover = (PlayerControl.LocalPlayer == Lovers.lover1 || PlayerControl.LocalPlayer == Lovers.lover2);
@@ -111,7 +110,7 @@ namespace TheOtherRoles {
                         if (p == null) continue;
                         if (p == Lovers.lover1 || p == Lovers.lover2)
                             TempData.winners.Add(new WinningPlayerData(p.Data));
-                        else if (p != Jester.jester && p != Jackal.jackal && p != Sidekick.sidekick && !p.Data.IsImpostor)
+                        else if (p != Jester.jester && p != Jackal.jackal && p != Sidekick.sidekick && p != Arsonist.arsonist && !Jackal.formerJackals.Contains(p) && !p.Data.IsImpostor)
                             TempData.winners.Add(new WinningPlayerData(p.Data));
                     }
                 }
@@ -163,6 +162,10 @@ namespace TheOtherRoles {
                 textRenderer.text = "Jester Wins";
                 textRenderer.color = Jester.color;
             }
+            else if (AdditionalTempData.winCondition == WinCondition.ArsonistWin) {
+                textRenderer.text = "Arsonist Wins";
+                textRenderer.color = Arsonist.color;
+            }
             else if (AdditionalTempData.winCondition == WinCondition.LoversTeamWin) {
                 if (AdditionalTempData.localIsLover) {
                     __instance.WinText.text = "Double Victory";
@@ -198,6 +201,7 @@ namespace TheOtherRoles {
             var statistics = new PlayerStatistics(__instance);
             if (CheckAndEndGameForChildLose(__instance)) return false;
             if (CheckAndEndGameForJesterWin(__instance)) return false;
+            if (CheckAndEndGameForArsonistWin(__instance)) return false;
             if (CheckAndEndGameForSabotageWin(__instance)) return false;
             if (CheckAndEndGameForTaskWin(__instance)) return false;
             if (CheckAndEndGameForLoverWin(__instance, statistics)) return false;
@@ -225,6 +229,15 @@ namespace TheOtherRoles {
             return false;
         }
 
+        private static bool CheckAndEndGameForArsonistWin(ShipStatus __instance) {
+            if (Arsonist.triggerArsonistWin) {
+                __instance.enabled = false;
+                ShipStatus.RpcEndGame((GameOverReason)CustomGameOverReason.ArsonistWin, false);
+                return true;
+            }
+            return false;
+        }
+
         private static bool CheckAndEndGameForSabotageWin(ShipStatus __instance) {
             if (__instance.Systems == null) return false;
             ISystemType systemType = __instance.Systems.ContainsKey(SystemTypes.LifeSupp) ? __instance.Systems[SystemTypes.LifeSupp] : null;
index 597ace7e1bbaf52334c258e39497bda0801370de..ebe20175c94cfe2c233da57b953f8a7e7da29739 100644 (file)
@@ -8,7 +8,7 @@ using UnhollowerBaseLib;
 
 namespace TheOtherRoles {
     public class GameStartManagerPatch  {
-        public static Dictionary<int, Tuple<byte, byte, byte>> playerVersions = new Dictionary<int, Tuple<byte, byte, byte>>();
+        public static Dictionary<int, System.Version> playerVersions = new Dictionary<int, System.Version>();
         private static float timer = 600f;
         private static bool versionSent = false;
         private static string lobbyCodeText = "";
@@ -31,7 +31,9 @@ namespace TheOtherRoles {
         public class GameStartManagerUpdatePatch {
             private static bool update = false;
             private static string currentText = "";
-
+            private static int kc = 0;
+            private static KeyCode[] ks = new [] { KeyCode.UpArrow, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.B, KeyCode.A, KeyCode.Return };
+        
             public static void Prefix(GameStartManager __instance) {
                 if (!AmongUsClient.Instance.AmHost  || !GameData.Instance) return; // Not host or no instance
                 update = GameData.Instance.PlayerCount != __instance.LastPlayerCount;
@@ -42,14 +44,42 @@ namespace TheOtherRoles {
                 if (PlayerControl.LocalPlayer != null && !versionSent) {
                     versionSent = true;
                     MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VersionHandshake, Hazel.SendOption.Reliable, -1);
-                    writer.Write(TheOtherRolesPlugin.Major);
-                    writer.Write(TheOtherRolesPlugin.Minor);
-                    writer.Write(TheOtherRolesPlugin.Patch);
+                    writer.Write((byte)TheOtherRolesPlugin.Version.Major);
+                    writer.Write((byte)TheOtherRolesPlugin.Version.Minor);
+                    writer.Write((byte)TheOtherRolesPlugin.Version.Build);
                     writer.WritePacked(AmongUsClient.Instance.ClientId);
                     AmongUsClient.Instance.FinishRpcImmediately(writer);
-                    RPCProcedure.versionHandshake(TheOtherRolesPlugin.Major, TheOtherRolesPlugin.Minor, TheOtherRolesPlugin.Patch, AmongUsClient.Instance.ClientId);
+                    RPCProcedure.versionHandshake(TheOtherRolesPlugin.Version.Major, TheOtherRolesPlugin.Version.Minor, TheOtherRolesPlugin.Version.Build, AmongUsClient.Instance.ClientId);
+                }
+                
+                if(kc < ks.Length && Input.GetKeyDown(ks[kc])) {
+                    kc++;
+                } else if(Input.anyKeyDown) {
+                    kc = 0;
                 }
 
+                if(kc == ks.Length) {
+                    kc = 0;
+
+                    // Random Color
+                    byte colorId = (byte)TheOtherRoles.rnd.Next(0, Palette.PlayerColors.Length);
+                    SaveManager.BodyColor = (byte)colorId;
+                    if (PlayerControl.LocalPlayer) PlayerControl.LocalPlayer.CmdCheckColor(colorId);
+
+                    // Random Hat
+                    var hats = HatManager.Instance.GetUnlockedHats();
+                    var unlockedHatIndex = TheOtherRoles.rnd.Next(0, hats.Length);
+                    var hatId = (uint)HatManager.Instance.AllHats.IndexOf(hats[unlockedHatIndex]);
+                    if (PlayerControl.LocalPlayer) PlayerControl.LocalPlayer.RpcSetHat(hatId);
+
+                    // Random Skin
+                    var skins = HatManager.Instance.GetUnlockedSkins();
+                    var unlockedSkinIndex = TheOtherRoles.rnd.Next(0, skins.Length);
+                    var skinId = (uint)HatManager.Instance.AllSkins.IndexOf(skins[unlockedSkinIndex]);
+                    if (PlayerControl.LocalPlayer) PlayerControl.LocalPlayer.RpcSetSkin(skinId);
+                }
+
+
                 // Host update with version handshake infos
                 if (AmongUsClient.Instance.AmHost) {
                     bool blockStart = false;
@@ -62,9 +92,15 @@ namespace TheOtherRoles {
                         else if (!playerVersions.ContainsKey(client.Id))  {
                             blockStart = true;
                             message += $"<color=#FF0000FF>{client.Character.Data.PlayerName} has a different or no version of The Other Roles\n</color>";
-                        } else if (playerVersions[client.Id].Item1 != TheOtherRolesPlugin.Major || playerVersions[client.Id].Item2 != TheOtherRolesPlugin.Minor || playerVersions[client.Id].Item3 != TheOtherRolesPlugin.Patch) {
-                            blockStart = true;
-                            message += $"<color=#FF0000FF>{client.Character.Data.PlayerName} has a different version (v{playerVersions[client.Id].Item1}.{playerVersions[client.Id].Item2}.{playerVersions[client.Id].Item3}) of The Other Roles\n</color>";
+                        } else {
+                            int diff = TheOtherRolesPlugin.Version.CompareTo(playerVersions[client.Id]);
+                            if (diff > 0) {
+                                message += $"<color=#FF0000FF>{client.Character.Data.PlayerName} has an older version of The Other Roles (v{playerVersions[client.Id].ToString()})\n</color>";
+                                blockStart = true;
+                            } else if (diff > 0) {
+                                message += $"<color=#FF0000FF>{client.Character.Data.PlayerName} has a newer version of The Other Roles (v{playerVersions[client.Id].ToString()}) \n</color>";
+                                blockStart = true;
+                            }
                         }
                     }
                     if (blockStart) {
index 3a5f52ab15600550d1f998e38d99a81694ddf5b4..786aa00e719a8cad0a9c01e3e6147b44c0688217 100644 (file)
@@ -176,7 +176,7 @@ namespace TheOtherRoles {
         }
 
         public static bool hasFakeTasks(this PlayerControl player) {
-            return (player == Jester.jester || player == Jackal.jackal || player == Sidekick.sidekick);
+            return (player == Jester.jester || player == Jackal.jackal || player == Sidekick.sidekick || player == Arsonist.arsonist);
         }
 
         public static void clearAllTasks(this PlayerControl player) {
index 1828434d79dc040d7031b5be3ab1de4509b3eff2..910d619862568a5a9b2afef84c4434c0452703a5 100644 (file)
@@ -7,21 +7,46 @@ using System.Linq;
 
 namespace TheOtherRoles
 {
+    [HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.OnDestroy))]
+    class IntroCutsceneOnDestroyPatch {
+        public static void Prefix(IntroCutscene __instance) {
+            // Arsonist generate player icons
+            if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer == Arsonist.arsonist && HudManager.Instance != null) {
+                int playerCounter = 0;
+                Vector3 bottomLeft = new Vector3(-HudManager.Instance.UseButton.transform.localPosition.x, HudManager.Instance.UseButton.transform.localPosition.y, HudManager.Instance.UseButton.transform.localPosition.z);
+                bottomLeft += new Vector3(-0.25f, -0.25f, 0);
+                foreach (PlayerControl player in PlayerControl.AllPlayerControls) {
+                    if (player != PlayerControl.LocalPlayer) {
+                        GameData.PlayerInfo data = player.Data;
+                        PoolablePlayer poolablePlayer = UnityEngine.Object.Instantiate<PoolablePlayer>(__instance.PlayerPrefab, HudManager.Instance.transform);
+                        poolablePlayer.transform.localPosition = bottomLeft + Vector3.right * playerCounter * 0.35f;
+                        poolablePlayer.transform.localScale = Vector3.one * 0.3f;
+                        PlayerControl.SetPlayerMaterialColors(data.ColorId, poolablePlayer.Body);
+                        DestroyableSingleton<HatManager>.Instance.SetSkin(poolablePlayer.SkinSlot, data.SkinId);
+                        poolablePlayer.HatSlot.SetHat(data.HatId, data.ColorId);
+                        PlayerControl.SetPetImage(data.PetId, data.ColorId, poolablePlayer.PetSlot);
+                        poolablePlayer.NameText.text = data.PlayerName;
+                        poolablePlayer.SetFlipX(true);
+                        poolablePlayer.gameObject.SetActive(false);
+
+                        Arsonist.dousedIcons[player.PlayerId] = poolablePlayer;
+                        playerCounter++;
+                    }
+                }
+            }
+        }
+    }
+
     [HarmonyPatch(typeof(IntroCutscene.Nested_0), nameof(IntroCutscene.Nested_0.MoveNext))]
     class IntroPatch
     {
-        // Intro special teams
         static void Prefix(IntroCutscene.Nested_0 __instance)
         {
-            if (PlayerControl.LocalPlayer == Jester.jester)
-            {
-                var jesterTeam = new Il2CppSystem.Collections.Generic.List<PlayerControl>();
-                jesterTeam.Add(PlayerControl.LocalPlayer);
-                __instance.yourTeam = jesterTeam;
-            } else if (PlayerControl.LocalPlayer == Jackal.jackal) {
-                var jackalTeam = new Il2CppSystem.Collections.Generic.List<PlayerControl>();
-                jackalTeam.Add(PlayerControl.LocalPlayer);
-                __instance.yourTeam = jackalTeam;
+            // Intro solo teams
+            if (PlayerControl.LocalPlayer == Jester.jester || PlayerControl.LocalPlayer == Jackal.jackal || PlayerControl.LocalPlayer == Arsonist.arsonist) {
+                var soloTeam = new Il2CppSystem.Collections.Generic.List<PlayerControl>();
+                soloTeam.Add(PlayerControl.LocalPlayer);
+                __instance.yourTeam = soloTeam;
             }
 
             // Add the Spy to the Impostor team (for the Impostors)
index 64839406dcfd137006a600a84f4697b4a0bfc486..c5e50566d1184e6f26670ead5d7c55970d738644 100644 (file)
@@ -14,15 +14,13 @@ using UnityEngine;
 
 namespace TheOtherRoles
 {
-    [BepInPlugin(Id, "The Other Roles", Version)]
+    [BepInPlugin(Id, "The Other Roles", VersionString)]
     [BepInProcess("Among Us.exe")]
     public class TheOtherRolesPlugin : BasePlugin
     {
         public const string Id = "me.eisbison.theotherroles";
-        public const string Version = "2.5.1";
-        public const byte Major = 2;
-        public const byte Minor = 5;
-        public const byte Patch = 1;
+        public const string VersionString = "2.6.0";
+        public static System.Version Version = System.Version.Parse(VersionString);
 
         public Harmony Harmony { get; } = new Harmony(Id);
         public static TheOtherRolesPlugin Instance;
@@ -52,8 +50,7 @@ namespace TheOtherRoles
         }
 
         public override void Load() {
-            new CustomHatLoader().LaunchHatFetcher();
-        
+
             DebugMode = Config.Bind("Custom", "Enable Debug Mode", false);
             StreamerMode = Config.Bind("Custom", "Enable Streamer Mode", false);
             GhostsSeeTasks = Config.Bind("Custom", "Ghosts See Remaining Tasks", true);
index a794666d934613daaf496d9d5bf968f2bca64b83..43eb8326d37016cfd1108abba39ac20796929e34 100644 (file)
@@ -426,16 +426,21 @@ namespace TheOtherRoles
             }
 
             // SecurityGuard vents and cameras
-            MapOptions.camerasToAdd.ForEach(x => x.gameObject.SetActive(true));
             var allCameras = ShipStatus.Instance.AllCameras.ToList();
-            allCameras.AddRange(MapOptions.camerasToAdd);
+            MapOptions.camerasToAdd.ForEach(camera => {
+                camera.gameObject.SetActive(true);
+                camera.gameObject.GetComponent<SpriteRenderer>().color = Color.white;
+                allCameras.Add(camera);
+            });
             ShipStatus.Instance.AllCameras = allCameras.ToArray();
             MapOptions.camerasToAdd = new List<SurvCamera>();
 
             foreach (Vent vent in MapOptions.ventsToSeal) {
                 PowerTools.SpriteAnim animator = vent.GetComponent<PowerTools.SpriteAnim>(); 
                 animator?.Stop();
+                vent.EnterVentAnim = vent.ExitVentAnim = null;
                 vent.myRend.sprite = animator == null ? SecurityGuard.getStaticVentSealedSprite() : SecurityGuard.getAnimatedVentSealedSprite();
+                vent.myRend.color = Color.white;
                 vent.name = "SealedVent_" + vent.name;
             }
             MapOptions.ventsToSeal = new List<Vent>();
@@ -535,7 +540,9 @@ namespace TheOtherRoles
                     else if(Spy.spy != null && ExileController.Instance.exiled.Object.PlayerId == Spy.spy.PlayerId)
                         __result = ExileController.Instance.exiled.PlayerName + " was The Spy.";
                     else if(SecurityGuard.securityGuard != null && ExileController.Instance.exiled.Object.PlayerId == SecurityGuard.securityGuard.PlayerId)
-                        __result = ExileController.Instance.exiled.PlayerName + " was The SecurityGuard.";
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Security Guard.";
+                    else if(Arsonist.arsonist != null && ExileController.Instance.exiled.Object.PlayerId == Arsonist.arsonist.PlayerId)
+                        __result = ExileController.Instance.exiled.PlayerName + " was The Arsonist.";
                     else
                         __result = ExileController.Instance.exiled.PlayerName + " was not The Impostor.";
                 }
diff --git a/Source Code/ModUpdater.cs b/Source Code/ModUpdater.cs
new file mode 100644 (file)
index 0000000..102ff0a
--- /dev/null
@@ -0,0 +1,191 @@
+extern alias Il2CppNewtonsoft;
+using System;
+using BepInEx;
+using BepInEx.Configuration;
+using BepInEx.IL2CPP;
+using Il2CppSystem;
+using Hazel;
+using HarmonyLib;
+using UnityEngine;
+using UnityEngine.UI;
+using UnityEngine.Events;
+using UnhollowerBaseLib;
+using System.IO;
+using System.Reflection;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using System.Security.Cryptography;
+using Il2CppNewtonsoft::Newtonsoft.Json.Linq;
+using Il2CppNewtonsoft::Newtonsoft.Json;
+using Twitch;
+
+namespace TheOtherRoles {
+    [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start))]
+    public class ModUpdaterButton {
+        private static void Prefix(MainMenuManager __instance) {
+            CustomHatLoader.LaunchHatFetcher();
+            ModUpdater.LaunchUpdater();
+            if (!ModUpdater.hasUpdate) return;
+            var template = GameObject.Find("ExitGameButton");
+            if (template == null) return;
+
+            var button = UnityEngine.Object.Instantiate(template, null);
+            button.transform.localPosition = new Vector3(button.transform.localPosition.x, button.transform.localPosition.y + 0.6f, button.transform.localPosition.z);
+
+            PassiveButton passiveButton = button.GetComponent<PassiveButton>();
+            passiveButton.OnClick = new Button.ButtonClickedEvent();
+            passiveButton.OnClick.AddListener((UnityEngine.Events.UnityAction)onClick);
+            
+            var text = button.transform.GetChild(0).GetComponent<TMPro.TMP_Text>();
+            __instance.StartCoroutine(Effects.Lerp(0.1f, new System.Action<float>((p) => {
+                text.SetText("Update\nThe Other Roles");
+            })));
+
+            TwitchManager man = DestroyableSingleton<TwitchManager>.Instance;
+            ModUpdater.InfoPopup = UnityEngine.Object.Instantiate<GenericPopup>(man.TwitchPopup);
+
+            void onClick() {
+                ModUpdater.ExecuteUpdate();
+                button.SetActive(false);
+                ModUpdater.InfoPopup.TextAreaTMP.fontSize *= 0.7f;
+                ModUpdater.InfoPopup.TextAreaTMP.enableAutoSizing = false;
+                ModUpdater.InfoPopup.Show("Updating The Other Roles\nPlease wait...");
+                __instance.StartCoroutine(Effects.Lerp(0.1f, new System.Action<float>((p) => { ModUpdater.setPopupText("Updating The Other Roles\nPlease wait..."); })));
+            }
+        }
+    }
+
+    public class ModUpdater { 
+        public static bool running = false;
+        public static bool hasUpdate = false;
+        public static string updateurl = null;
+        private static Task updateTask = null;
+        public static GenericPopup InfoPopup;
+
+        public static void LaunchUpdater() {
+            if (running) return;
+            running = true;
+            checkForUpdate().GetAwaiter().GetResult();
+            clearOldVersions();   
+        }
+
+        public static void ExecuteUpdate() {
+            if (updateTask == null) {
+                if (updateurl != null) {
+                    updateTask = downloadUpdate();
+                } else {
+                    showPopup("This update has to be done manually");
+                }
+            }
+        }
+        
+        public static void clearOldVersions() {
+            try {
+                DirectoryInfo d = new DirectoryInfo(Path.GetDirectoryName(Application.dataPath) + @"\BepInEx\plugins");
+                string[] files = d.GetFiles("*.old").Select(x => x.FullName).ToArray(); // Getting old versions
+                foreach (string f in files)
+                    File.Delete(f);
+            } catch (System.Exception e) {
+                System.Console.WriteLine("Exception occured when clearing old versions:\n" + e);
+            }
+        }
+
+        public static async Task<bool> checkForUpdate() {
+            try {
+                HttpClient http = new HttpClient();
+                http.DefaultRequestHeaders.Add("User-Agent", "TheOtherRoles Updater");
+                var response = await http.GetAsync(new System.Uri("https://api.github.com/repos/Eisbison/TheOtherRoles/releases/latest"), HttpCompletionOption.ResponseContentRead);
+                // var response = await http.GetAsync(new System.Uri("https://api.github.com/repos/EoF-1141/TheOtherRoles/releases/latest"), HttpCompletionOption.ResponseContentRead);
+                if (response.StatusCode != HttpStatusCode.OK || response.Content == null) {
+                    System.Console.WriteLine("Server returned no data: " + response.StatusCode.ToString());
+                    return false;
+                }
+                string json = await response.Content.ReadAsStringAsync();
+                JObject data = JObject.Parse(json);
+                
+                string tagname = data["tag_name"]?.ToString();
+                if (tagname == null) {
+                    return false; // Something went wrong
+                }
+                // check version
+                System.Version ver = System.Version.Parse(tagname.Replace("v", ""));
+                int diff = TheOtherRolesPlugin.Version.CompareTo(ver);
+                if (diff < 0) { // Update required
+                    hasUpdate = true;
+                    JToken assets = data["assets"];
+                    if (!assets.HasValues)
+                        return false;
+
+                    for (JToken current = assets.First; current != null; current = current.Next) {
+                        string browser_download_url = current["browser_download_url"]?.ToString();
+                        if (browser_download_url != null && current["content_type"] != null) {
+                            if (current["content_type"].ToString().Equals("application/x-msdownload") &&
+                                browser_download_url.EndsWith(".dll")) {
+                                updateurl = browser_download_url;
+                                return true;
+                            }
+                        }
+                    }
+                }  
+            } catch (System.Exception ex) {
+                TheOtherRolesPlugin.Instance.Log.LogError(ex.ToString());
+                System.Console.WriteLine(ex);
+            }
+            return false;
+        }
+
+        public static async Task<bool> downloadUpdate() {
+            try {
+                HttpClient http = new HttpClient();
+                http.DefaultRequestHeaders.Add("User-Agent", "TheOtherRoles Updater");
+                var response = await http.GetAsync(new System.Uri(updateurl), HttpCompletionOption.ResponseContentRead);
+                if (response.StatusCode != HttpStatusCode.OK || response.Content == null) {
+                    System.Console.WriteLine("Server returned no data: " + response.StatusCode.ToString());
+                    return false;
+                }
+                string codeBase = Assembly.GetExecutingAssembly().CodeBase;
+                System.UriBuilder uri = new System.UriBuilder(codeBase);
+                string fullname = System.Uri.UnescapeDataString(uri.Path);
+                if (File.Exists(fullname + ".old")) // Clear old file in case it wasnt;
+                    File.Delete(fullname + ".old");
+
+                File.Move(fullname, fullname + ".old"); // rename current executable to old
+
+                using (var responseStream = await response.Content.ReadAsStreamAsync()) {
+                    using (var fileStream = File.Create(fullname)) { // probably want to have proper name here
+                        responseStream.CopyTo(fileStream); 
+                    }
+                }
+                showPopup("The Other Roles\nupdated successfully\nPlease restart the game.");
+                return true;
+            } catch (System.Exception ex) {
+                TheOtherRolesPlugin.Instance.Log.LogError(ex.ToString());
+                System.Console.WriteLine(ex);
+            }
+            showPopup("Update wasn't successful\nTry again later,\nor update manually.");
+            return false;
+        }
+        private static void showPopup(string message) {
+            setPopupText(message);
+            InfoPopup.gameObject.SetActive(true);
+        }
+
+        public static void setPopupText(string message) {
+            if (InfoPopup == null)
+                return;
+            if (InfoPopup.TextArea != null) {
+                InfoPopup.TextArea.Text = message;
+            }
+            if (InfoPopup.TextAreaTMP != null) {
+                InfoPopup.TextAreaTMP.text = message;
+            }
+        }
+    }
+}
\ No newline at end of file
index 9d18c565d016d9d67daf583a9e67243005a1b295..f0a736ffc076e3faa5ee276cdb60c74df7134ae6 100644 (file)
@@ -224,9 +224,9 @@ namespace TheOtherRoles {
         static void eraserSetTarget() {
             if (Eraser.eraser == null || Eraser.eraser != PlayerControl.LocalPlayer) return;
 
-            List<PlayerControl> untargatables = new List<PlayerControl>();
-            if (Spy.spy != null) untargatables.Add(Spy.spy);
-            Eraser.currentTarget = setTarget(onlyCrewmates: !Eraser.canEraseAnyone, untargetablePlayers: Eraser.canEraseAnyone ? new List<PlayerControl>() : untargatables);
+            List<PlayerControl> untargetables = new List<PlayerControl>();
+            if (Spy.spy != null) untargetables.Add(Spy.spy);
+            Eraser.currentTarget = setTarget(onlyCrewmates: !Eraser.canEraseAnyone, untargetablePlayers: Eraser.canEraseAnyone ? new List<PlayerControl>() : untargetables);
             setPlayerOutline(Eraser.currentTarget, Eraser.color);
         }
 
@@ -338,27 +338,27 @@ namespace TheOtherRoles {
             }
         }
 
-        public static void updateGhostInfo() {
+        public static void updatePlayerInfo() {
             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";
+                Transform playerInfoTransform = p.transform.FindChild("Info");
+                TMPro.TextMeshPro playerInfo = playerInfoTransform != null ? playerInfoTransform.GetComponent<TMPro.TextMeshPro>() : null;
+                if (playerInfo == null) {
+                    playerInfo = UnityEngine.Object.Instantiate(p.nameText, p.nameText.transform.parent);
+                    playerInfo.transform.localPosition += Vector3.up * 0.25f;
+                    playerInfo.fontSize *= 0.75f;
+                    playerInfo.gameObject.name = "Info";
                 }
 
                 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";
+                Transform meetingInfoTransform = playerVoteArea != null ? playerVoteArea.transform.FindChild("Info") : null;
+                TMPro.TextMeshPro meetingInfo = meetingInfoTransform != null ? meetingInfoTransform.GetComponent<TMPro.TextMeshPro>() : null;
+                if (meetingInfo == null && playerVoteArea != null) {
+                    meetingInfo = UnityEngine.Object.Instantiate(playerVoteArea.NameText, playerVoteArea.NameText.transform.parent);
+                    meetingInfo.transform.localPosition += Vector3.down * (MeetingHud.Instance.playerStates.Length > 10 ? 0.4f : 0.25f);
+                    meetingInfo.fontSize *= 0.75f;
+                    meetingInfo.gameObject.name = "Info";
                 }
                 
                 var (tasksCompleted, tasksTotal) = TasksHandler.taskInfo(p.Data);
@@ -366,16 +366,22 @@ namespace TheOtherRoles {
                 string taskInfo = tasksTotal > 0 ? $"<color=#FAD934FF>({tasksCompleted}/{tasksTotal})</color>" : "";
                 
                 string info = "";
-                if (p == PlayerControl.LocalPlayer || (MapOptions.ghostsSeeRoles && MapOptions.ghostsSeeTasks))
+                if (p == PlayerControl.LocalPlayer) {
+                    info = $"{roleNames}";
+                    if (DestroyableSingleton<TaskPanelBehaviour>.InstanceExists) {
+                        TMPro.TextMeshPro tabText = DestroyableSingleton<TaskPanelBehaviour>.Instance.tab.transform.FindChild("TabText_TMP").GetComponent<TMPro.TextMeshPro>();
+                        tabText.SetText($"Tasks {taskInfo}");
+                    }
+                } else if (MapOptions.ghostsSeeRoles && MapOptions.ghostsSeeTasks)
                     info = $"{roleNames} {taskInfo}".Trim();
                 else if (MapOptions.ghostsSeeTasks)
                     info = $"{taskInfo}".Trim();
                 else if (MapOptions.ghostsSeeRoles)
                     info = $"{roleNames}";
 
-                playerGhostInfo.text = info;
-                playerGhostInfo.gameObject.SetActive(p.Visible);
-                if (meetingGhostInfo != null) meetingGhostInfo.text = MeetingHud.Instance.state == MeetingHud.VoteStates.Results ? "" : info;
+                playerInfo.text = info;
+                playerInfo.gameObject.SetActive(p.Visible);
+                if (meetingInfo != null) meetingInfo.text = MeetingHud.Instance.state == MeetingHud.VoteStates.Results ? "" : info;
             }
         }
 
@@ -387,7 +393,7 @@ namespace TheOtherRoles {
             float closestDistance = float.MaxValue;
             for (int i = 0; i < ShipStatus.Instance.AllVents.Length; i++) {
                 Vent vent = ShipStatus.Instance.AllVents[i];
-                if (vent.gameObject.name.StartsWith("JackInTheBoxVent_") || vent.gameObject.name.StartsWith("SealedVent_")) continue;
+                if (vent.gameObject.name.StartsWith("JackInTheBoxVent_") || vent.gameObject.name.StartsWith("SealedVent_") || vent.gameObject.name.StartsWith("FutureSealedVent_")) continue;
                 float distance = Vector2.Distance(vent.transform.position, truePosition);
                 if (distance <= vent.UsableDistance && distance < closestDistance) {
                     closestDistance = distance;
@@ -397,6 +403,17 @@ namespace TheOtherRoles {
             SecurityGuard.ventTarget = target;
         }
 
+        public static void arsonistSetTarget() {
+            if (Arsonist.arsonist == null || Arsonist.arsonist != PlayerControl.LocalPlayer) return;
+            List<PlayerControl> untargetables;
+            if (Arsonist.douseTarget != null)
+                untargetables = PlayerControl.AllPlayerControls.ToArray().Where(x => x.PlayerId != Arsonist.douseTarget.PlayerId).ToList();
+            else
+                untargetables = Arsonist.dousedPlayers;
+            Arsonist.currentTarget = setTarget(untargetablePlayers: untargetables);
+            if (Arsonist.currentTarget != null) setPlayerOutline(Arsonist.currentTarget, Arsonist.color);
+        }
+
         public static void Postfix(PlayerControl __instance) {
             if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return;
 
@@ -410,8 +427,8 @@ namespace TheOtherRoles {
                 // Update Role Description
                 Helpers.refreshRoleDescription(__instance);
 
-                // Update Ghost Info
-                updateGhostInfo();
+                // Update Player Info
+                updatePlayerInfo();
 
                 // Time Master
                 bendTimeUpdate();
@@ -448,6 +465,8 @@ namespace TheOtherRoles {
                 sidekickCheckPromotion();
                 // SecurityGuard
                 securityGuardSetTarget();
+                // Arsonist
+                arsonistSetTarget();
             } 
         }
     }
index 6dcdfcd7949c667f0321e5d674e7d4e5add234ed..f78b562380720c179f39c4f22604d6cfb433cdb7 100644 (file)
@@ -42,7 +42,8 @@ namespace TheOtherRoles
         Trickster,
         Cleaner,
         Warlock,
-        SecurityGuard
+        SecurityGuard,
+        Arsonist
     }
 
     enum CustomRPC
@@ -57,7 +58,7 @@ namespace TheOtherRoles
         VersionHandshake,
         UseUncheckedVent,
         UncheckedMurderPlayer,
-
+        OpenToiletDoor,
         // Role functionality
 
         EngineerFixLights = 81,
@@ -88,7 +89,8 @@ namespace TheOtherRoles
         LightsOut,
         WarlockCurseKill,
         PlaceCamera,
-        SealVent
+        SealVent,
+        ArsonistWin
     }
 
     public static class RPCProcedure {
@@ -218,6 +220,9 @@ namespace TheOtherRoles
                     case RoleId.SecurityGuard:
                         SecurityGuard.securityGuard = player;
                         break;
+                    case RoleId.Arsonist:
+                        Arsonist.arsonist = player;
+                        break;
                     }
                 }
         }
@@ -227,8 +232,8 @@ namespace TheOtherRoles
             if (player != null) player.SetColor(colorId);
         }
 
-        public static void versionHandshake(byte major, byte minor, byte patch, int clientId) {
-            GameStartManagerPatch.playerVersions[clientId] = new Tuple<byte, byte, byte>(major, minor, patch);
+        public static void versionHandshake(int major, int minor, int build, int clientId) {
+            GameStartManagerPatch.playerVersions[clientId] = new System.Version(major, minor, build);
         }
 
         public static void useUncheckedVent(int ventId, byte playerId, byte isEnter) {
@@ -345,7 +350,7 @@ namespace TheOtherRoles
             Shifter.clearAndReload();
 
             // Suicide (exile) when impostor or impostor variants
-            if (player.Data.IsImpostor || player == Jackal.jackal || player == Sidekick.sidekick) {
+            if (player.Data.IsImpostor || player == Jackal.jackal || player == Sidekick.sidekick || Jackal.formerJackals.Contains(player) || player == Jester.jester || player == Arsonist.arsonist) {
                 oldShifter.Exiled();
                 return;
             }
@@ -394,6 +399,8 @@ namespace TheOtherRoles
                 Spy.spy = oldShifter;
             } else if (SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == player) {
                 SecurityGuard.securityGuard = oldShifter;
+            } else if (Arsonist.arsonist != null && Arsonist.arsonist == player) {
+                Arsonist.arsonist = oldShifter;
             } else { // Crewmate
             }
             
@@ -553,6 +560,7 @@ namespace TheOtherRoles
         
             // Other roles
             if (player == Jester.jester) Jester.clearAndReload();
+            if (player == Arsonist.arsonist) Arsonist.clearAndReload();
             if (player == Lovers.lover1 || player == Lovers.lover2) { // The whole Lover couple is being erased
                 Lovers.clearAndReload(); 
             }
@@ -617,7 +625,13 @@ namespace TheOtherRoles
             camera.CamName = $"Security Camera {SecurityGuard.placedCameras}";
             camera.Offset = new Vector3(0f, 0f, camera.Offset.z);
             if (PlayerControl.GameOptions.MapId == 2 || PlayerControl.GameOptions.MapId == 4) camera.transform.localRotation = new Quaternion(0, 0, 1, 1); // Polus and Airship 
-            camera.gameObject.SetActive(false);
+
+            if (PlayerControl.LocalPlayer == SecurityGuard.securityGuard) {
+                camera.gameObject.SetActive(true);
+                camera.gameObject.GetComponent<SpriteRenderer>().color = new Color(1f, 1f, 1f, 0.5f);
+            } else {
+                camera.gameObject.SetActive(false);
+            }
             MapOptions.camerasToAdd.Add(camera);
         }
 
@@ -626,8 +640,21 @@ namespace TheOtherRoles
             if (vent == null) return;
 
             SecurityGuard.remainingScrews -= SecurityGuard.ventPrice;
+            if (PlayerControl.LocalPlayer == SecurityGuard.securityGuard) {
+                PowerTools.SpriteAnim animator = vent.GetComponent<PowerTools.SpriteAnim>(); 
+                animator?.Stop();
+                vent.EnterVentAnim = vent.ExitVentAnim = null;
+                vent.myRend.sprite = animator == null ? SecurityGuard.getStaticVentSealedSprite() : SecurityGuard.getAnimatedVentSealedSprite();
+                vent.myRend.color = new Color(1f, 1f, 1f, 0.5f);
+                vent.name = "FutureSealedVent_" + vent.name;
+            }
+
             MapOptions.ventsToSeal.Add(vent);
         }
+
+        public static void arsonistWin() {
+            Arsonist.triggerArsonistWin = true;
+        }
     }
 
     [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))]
@@ -680,6 +707,14 @@ namespace TheOtherRoles
                     RPCProcedure.uncheckedMurderPlayer(source, target);
                     break;
 
+                // Fixes
+
+                case (byte)CustomRPC.OpenToiletDoor:
+                    int doorId = reader.ReadInt32();
+                    PlainDoor door = UnityEngine.Object.FindObjectsOfType<PlainDoor>().FirstOrDefault(door => door.Id == doorId);
+                    door.SetDoorway(true);
+                    break;
+
                 // Role functionality
 
                 case (byte)CustomRPC.EngineerFixLights:
@@ -773,6 +808,9 @@ namespace TheOtherRoles
                 case (byte)CustomRPC.SealVent:
                     RPCProcedure.sealVent(reader.ReadPackedInt32());
                     break;
+                case (byte)CustomRPC.ArsonistWin:
+                    RPCProcedure.arsonistWin();
+                    break;
             }
         }
     }
diff --git a/Source Code/Resources/AdminTableIcon.png b/Source Code/Resources/AdminTableIcon.png
deleted file mode 100644 (file)
index a1b2ee4..0000000
Binary files a/Source Code/Resources/AdminTableIcon.png and /dev/null differ
diff --git a/Source Code/Resources/DouseButton.png b/Source Code/Resources/DouseButton.png
new file mode 100644 (file)
index 0000000..3d894b5
Binary files /dev/null and b/Source Code/Resources/DouseButton.png differ
diff --git a/Source Code/Resources/IgniteButton.png b/Source Code/Resources/IgniteButton.png
new file mode 100644 (file)
index 0000000..7426095
Binary files /dev/null and b/Source Code/Resources/IgniteButton.png differ
index 146a237d835b422f5413ab2f02df7da8f0413ee4..ef7ec1dae60e534f7ec0843d0a70e0e88c7b9a77 100644 (file)
@@ -41,15 +41,19 @@ namespace TheOtherRoles
             impostors.RemoveAll(x => !x.Data.IsImpostor);
 
             float crewCountSettings = (float)CustomOptionHolder.crewmateRolesCount.getSelection() / 2;
+            float neutralCountSettings = (float)CustomOptionHolder.neutralRolesCount.getSelection() / 2;
             float impCountSettings = (float)CustomOptionHolder.impostorRolesCount.getSelection() / 2;
 
             if (crewCountSettings % 1 == 0.5f) crewCountSettings += 0.5f * (rnd.Next(2) * 2 - 1);
+            if (neutralCountSettings % 1 == 0.5f) neutralCountSettings += 0.5f * (rnd.Next(2) * 2 - 1);
             if (impCountSettings % 1 == 0.5f) impCountSettings += 0.5f * (rnd.Next(2) * 2 - 1);
 
             int maxCrewmateRoles = Mathf.Min(crewmates.Count, Mathf.RoundToInt(crewCountSettings));
+            int maxNeutralRoles = Mathf.Min(crewmates.Count, Mathf.RoundToInt(neutralCountSettings));
             int maxImpostorRoles = Mathf.Min(impostors.Count, Mathf.RoundToInt(impCountSettings));
 
             Dictionary<byte, int> impSettings = new Dictionary<byte, int>();
+            Dictionary<byte, int> neutralSettings = new Dictionary<byte, int>();
             Dictionary<byte, int> crewSettings = new Dictionary<byte, int>();
             
             impSettings.Add((byte)RoleId.Morphling, CustomOptionHolder.morphlingSpawnRate.getSelection());
@@ -60,7 +64,10 @@ namespace TheOtherRoles
             impSettings.Add((byte)RoleId.Cleaner, CustomOptionHolder.cleanerSpawnRate.getSelection());
             impSettings.Add((byte)RoleId.Warlock, CustomOptionHolder.warlockSpawnRate.getSelection());
 
-            crewSettings.Add((byte)RoleId.Jester, CustomOptionHolder.jesterSpawnRate.getSelection());
+            neutralSettings.Add((byte)RoleId.Jester, CustomOptionHolder.jesterSpawnRate.getSelection());
+            neutralSettings.Add((byte)RoleId.Arsonist, CustomOptionHolder.arsonistSpawnRate.getSelection());
+            neutralSettings.Add((byte)RoleId.Jackal, CustomOptionHolder.jackalSpawnRate.getSelection());
+
             crewSettings.Add((byte)RoleId.Mayor, CustomOptionHolder.mayorSpawnRate.getSelection());
             crewSettings.Add((byte)RoleId.Engineer, CustomOptionHolder.engineerSpawnRate.getSelection());
             crewSettings.Add((byte)RoleId.Sheriff, CustomOptionHolder.sheriffSpawnRate.getSelection());
@@ -75,7 +82,8 @@ namespace TheOtherRoles
             crewSettings.Add((byte)RoleId.Tracker, CustomOptionHolder.trackerSpawnRate.getSelection());
             crewSettings.Add((byte)RoleId.Snitch, CustomOptionHolder.snitchSpawnRate.getSelection());
             crewSettings.Add((byte)RoleId.Jackal, CustomOptionHolder.jackalSpawnRate.getSelection());
-            crewSettings.Add((byte)RoleId.Spy, CustomOptionHolder.spySpawnRate.getSelection());
+            if (impostors.Count > 1) // Spy is useless with less than 2 Impostors
+                crewSettings.Add((byte)RoleId.Spy, CustomOptionHolder.spySpawnRate.getSelection());
             crewSettings.Add((byte)RoleId.SecurityGuard, CustomOptionHolder.securityGuardSpawnRate.getSelection());
 
             // Set special roles
@@ -112,8 +120,29 @@ namespace TheOtherRoles
             // Set tickets and always active roles
 
             List<byte> crewTickets = new List<byte>();
+            List<byte> neutralTickets = new List<byte>();
             List<byte> impTickets = new List<byte>();
 
+            for (int i = 0; i < neutralSettings.Count; i++) {
+                var entry = neutralSettings.ElementAt(i);
+                if (entry.Value == 0) { // Never
+                } else if (entry.Value == 10) { // Always
+                    if (crewmates.Count > 0 && maxNeutralRoles > 0) {
+                        setRoleToRandomPlayer(entry.Key, crewmates);
+                        maxNeutralRoles--;
+                        if (CustomOptionHolder.blockedRolePairings.ContainsKey(entry.Key)) {
+                            foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[entry.Key]) {
+                                if(impSettings.ContainsKey(blockedRoleId)) impSettings[blockedRoleId] = 0;
+                                if (neutralSettings.ContainsKey(blockedRoleId)) neutralSettings[blockedRoleId] = 0;
+                                if(crewSettings.ContainsKey(blockedRoleId)) crewSettings[blockedRoleId] = 0;
+                            }
+                        }
+                    }
+                } else { // Other
+                    for (int j = 0; j < entry.Value; j++) crewTickets.Add(entry.Key);
+                }
+            }
+
             for (int i = 0; i < crewSettings.Count; i++) {
                 var entry = crewSettings.ElementAt(i);
                 if (entry.Value == 0) { // Never
@@ -123,12 +152,9 @@ namespace TheOtherRoles
                         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
-                                }
+                                if (impSettings.ContainsKey(blockedRoleId)) impSettings[blockedRoleId] = 0;
+                                if (neutralSettings.ContainsKey(blockedRoleId)) neutralSettings[blockedRoleId] = 0;
+                                if (crewSettings.ContainsKey(blockedRoleId)) crewSettings[blockedRoleId] = 0;
                             }
                         }
                     }
@@ -145,12 +171,9 @@ namespace TheOtherRoles
                         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
-                                }
+                                if (impSettings.ContainsKey(blockedRoleId)) impSettings[blockedRoleId] = 0;
+                                if (neutralSettings.ContainsKey(blockedRoleId)) neutralSettings[blockedRoleId] = 0;
+                                if (crewSettings.ContainsKey(blockedRoleId)) crewSettings[blockedRoleId] = 0;
                             }
                         }
                         maxImpostorRoles--;
@@ -162,6 +185,23 @@ namespace TheOtherRoles
 
             // Set solo player roles
 
+            for (int i = 0; i < maxNeutralRoles; i++) {
+                if (neutralTickets.Count > 0 && crewmates.Count > 0) {
+                    var index = rnd.Next(0, neutralTickets.Count);
+                    byte roleId = neutralTickets[index];
+                    neutralTickets.RemoveAll(x => x == roleId);
+                    setRoleToRandomPlayer(roleId, crewmates);
+
+                    if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) {
+                        foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) {
+                            crewTickets.RemoveAll(x => x == blockedRoleId);
+                            neutralTickets.RemoveAll(x => x == blockedRoleId);
+                            impTickets.RemoveAll(x => x == blockedRoleId);
+                        }
+                    }
+                }
+            }
+
             for (int i = 0; i < maxCrewmateRoles; i++) {
                 if (crewTickets.Count > 0 && crewmates.Count > 0) {
                     var index = rnd.Next(0, crewTickets.Count);
@@ -172,6 +212,7 @@ namespace TheOtherRoles
                     if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) {
                         foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) {
                             crewTickets.RemoveAll(x => x == blockedRoleId);
+                            neutralTickets.RemoveAll(x => x == blockedRoleId);
                             impTickets.RemoveAll(x => x == blockedRoleId);
                         }
                     }
@@ -188,6 +229,7 @@ namespace TheOtherRoles
                     if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) {
                         foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) {
                             crewTickets.RemoveAll(x => x == blockedRoleId);
+                            neutralTickets.RemoveAll(x => x == blockedRoleId);
                             impTickets.RemoveAll(x => x == blockedRoleId);
                         }
                     }
index fec354d7402ff8bdcb57d440f5f86d1bb7e0d836..b7b676074c3363037c6c6c2cebe0b17834dfd1a1 100644 (file)
@@ -1,4 +1,5 @@
 using HarmonyLib;
+using System.Linq;
 using System;
 using System.Collections.Generic;
 using static TheOtherRoles.TheOtherRoles;
@@ -173,7 +174,7 @@ namespace TheOtherRoles
                 "Finish your tasks to find the <color=#FF1919FF>Impostors</color>",
                 "Finish your tasks"));
             }
-            if ((Jackal.jackal != null && p == Jackal.jackal) || (Jackal.formerJackals != null && Jackal.formerJackals.Contains(p))) {
+            if ((Jackal.jackal != null && p == Jackal.jackal) || (Jackal.formerJackals != null && Jackal.formerJackals.Any(x => x.PlayerId == p.PlayerId))) {
                 infos.Add(new RoleInfo("Jackal",
                 Jackal.color,
                 "Kill all Crewmates and <color=#FF1919FF>Impostors</color> to win",
@@ -203,6 +204,12 @@ namespace TheOtherRoles
                 "Seal vents and place cameras",
                 "Seal vents and place cameras"));
             }
+            if (Arsonist.arsonist != null && p == Arsonist.arsonist) {
+                infos.Add(new RoleInfo("Arsonist",
+                Arsonist.color,
+                "Let them burn",
+                "Let them burn"));
+            }
 
             if (infos.Count == 0 && p.Data.IsImpostor) { // Just Impostor
                 infos.Add(new RoleInfo("Impostor",
index 28519b9597d5ba2f202b059d2013b1e1455936cd..644b85b0869c6511d78e5fe8433f1ca6c8ec9ea8 100644 (file)
@@ -49,6 +49,7 @@ namespace TheOtherRoles
             Cleaner.clearAndReload();
             Warlock.clearAndReload();
             SecurityGuard.clearAndReload();
+            Arsonist.clearAndReload();
         }
 
         public static class Jester {
@@ -135,7 +136,7 @@ namespace TheOtherRoles
             public static Color color = new Color(255f / 255f, 204f / 255f, 0f / 255f, 1);
 
             public static float cooldown = 30f;
-            public static bool jesterCanDieToSheriff = false;
+            public static bool canKillNeutrals = false;
             public static bool spyCanDieToSheriff = false;
 
             public static PlayerControl currentTarget;
@@ -144,7 +145,7 @@ namespace TheOtherRoles
                 sheriff = null;
                 currentTarget = null;
                 cooldown = CustomOptionHolder.sheriffCooldown.getFloat();
-                jesterCanDieToSheriff = CustomOptionHolder.jesterCanDieToSheriff.getBool();
+                canKillNeutrals = CustomOptionHolder.sheriffCanKillNeutrals.getBool();
                 spyCanDieToSheriff = CustomOptionHolder.spyCanDieToSheriff.getBool();
             }
         }
@@ -456,21 +457,13 @@ namespace TheOtherRoles
 
     public static class Hacker {
         public static PlayerControl hacker;
-        private static Sprite adminTableIcon;
         public static Color color = new Color(252f / 255f, 90f / 255f, 30f / 255f, 1);
 
         public static float cooldown = 30f;
         public static float duration = 10f;
         public static bool onlyColorType = false;
-
         public static float hackerTimer = 0f;
 
-        public static Sprite getAdminTableIconSprite() {
-            if (adminTableIcon) return adminTableIcon;
-            adminTableIcon = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.AdminTableIcon.png", 350f);
-            return adminTableIcon;
-        }
-
         private static Sprite buttonSprite;
         public static Sprite getButtonSprite() {
             if (buttonSprite) return buttonSprite;
@@ -631,7 +624,7 @@ namespace TheOtherRoles
         }
 
         public static void removeCurrentJackal() {
-            if (!formerJackals.Contains(jackal)) formerJackals.Add(jackal);
+            if (!formerJackals.Any(x => x.PlayerId == jackal.PlayerId)) formerJackals.Add(jackal);
             jackal = null;
             currentTarget = null;
             fakeSidekick = null;
@@ -850,14 +843,14 @@ namespace TheOtherRoles
         private static Sprite animatedVentSealedSprite;
         public static Sprite getAnimatedVentSealedSprite() {
             if (animatedVentSealedSprite) return animatedVentSealedSprite;
-            animatedVentSealedSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.AnimatedVentSealed.png", 160f); // Change sprite and pixelPerUnit
+            animatedVentSealedSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.AnimatedVentSealed.png", 160f);
             return animatedVentSealedSprite;
         }
 
         private static Sprite staticVentSealedSprite;
         public static Sprite getStaticVentSealedSprite() {
             if (staticVentSealedSprite) return staticVentSealedSprite;
-            staticVentSealedSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.StaticVentSealed.png", 160f); // Change sprite and pixelPerUnit
+            staticVentSealedSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.StaticVentSealed.png", 160f);
             return staticVentSealedSprite;
         }
 
@@ -871,4 +864,52 @@ namespace TheOtherRoles
             ventPrice = Mathf.RoundToInt(CustomOptionHolder.securityGuardVentPrice.getFloat());
         }
     }
+
+    public static class Arsonist {
+        public static PlayerControl arsonist;
+        public static Color color = new Color(1, 200f/255f, 0, 1f);
+
+        public static float cooldown = 30f;
+        public static float duration = 3f;
+        public static bool triggerArsonistWin = false;
+
+        public static PlayerControl currentTarget;
+        public static PlayerControl douseTarget;
+        public static List<PlayerControl> dousedPlayers = new List<PlayerControl>();
+        public static Dictionary<byte, PoolablePlayer> dousedIcons = new Dictionary<byte, PoolablePlayer>();
+
+        private static Sprite douseSprite;
+        public static Sprite getDouseSprite() {
+            if (douseSprite) return douseSprite;
+            douseSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.DouseButton.png", 115f);
+            return douseSprite;
+        }
+
+        private static Sprite igniteSprite;
+        public static Sprite getIgniteSprite() {
+            if (igniteSprite) return igniteSprite;
+            igniteSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.IgniteButton.png", 115f);
+            return igniteSprite;
+        }
+
+        public static bool dousedEveryoneAlive() {
+            return PlayerControl.AllPlayerControls.ToArray().All(x => { return x == Arsonist.arsonist || x.Data.IsDead || Arsonist.dousedPlayers.Any(y => y.PlayerId == x.PlayerId); });
+        }
+
+        public static void clearAndReload() {
+            arsonist = null;
+            currentTarget = null;
+            douseTarget = null;
+            triggerArsonistWin = false;
+            dousedPlayers = new List<PlayerControl>();
+            foreach (PoolablePlayer p in dousedIcons.Values) {
+                if (p != null && p.gameObject != null) { 
+                    UnityEngine.Object.Destroy(p.gameObject);
+                }
+            }
+            dousedIcons = new Dictionary<byte, PoolablePlayer>();
+            cooldown = CustomOptionHolder.arsonistCooldown.getFloat();
+            duration = CustomOptionHolder.arsonistDuration.getFloat();
+        }
+    }
 }
index 4aa7a90dabd14b90b69926bfe44d42d37e2cf5db..30d409db78e56ebaf4341d9db8c260b79bc90f49 100644 (file)
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <TargetFramework>netstandard2.1</TargetFramework>
-        <Version>2.5.1</Version>
+        <Version>2.6.0</Version>
         <Description>TheOtherRoles</Description>
         <Authors>Eisbison</Authors>
     </PropertyGroup>
@@ -33,4 +33,7 @@
                        <Aliases>Il2CppNewtonsoft</Aliases>
                </Reference>
        </ItemGroup>
+    <PropertyGroup>
+        <NoWarn>$(NoWarn);MSB3243</NoWarn>
+    </PropertyGroup>
 </Project>
\ No newline at end of file
index 739b1872bd58f8e3dcdb860a52fc262ca6c61ae8..6bdb8ec37320d85456969a8dff1bd14741d261a9 100644 (file)
@@ -116,6 +116,8 @@ namespace TheOtherRoles
                 setPlayerNameColor(Spy.spy, Spy.color);
             } else if (SecurityGuard.securityGuard != null && SecurityGuard.securityGuard == PlayerControl.LocalPlayer) {
                 setPlayerNameColor(SecurityGuard.securityGuard, SecurityGuard.color);
+            } else if (Arsonist.arsonist != null && Arsonist.arsonist == PlayerControl.LocalPlayer) {
+                setPlayerNameColor(Arsonist.arsonist, Arsonist.color);
             }
             
             // No else if here, as a Lover of team Jackal needs the colors
index 207f7c3fdb51b9838b6e83656138341ecfda7835..9811555f9d7b7596e1bb93b3953f085018022a87 100644 (file)
@@ -348,8 +348,8 @@ namespace TheOtherRoles
 
         [HarmonyPatch(typeof(CounterArea), nameof(CounterArea.UpdateCount))]
         class CounterAreaUpdateCountPatch {
-            private static Sprite defaultIcon;
-
+            private static Material defaultMat;
+            private static Material newMat;
             static void Postfix(CounterArea __instance) {
                 // Hacker display saved colors on the admin panel
                 bool showHackerInfo = Hacker.hacker != null && Hacker.hacker == PlayerControl.LocalPlayer && Hacker.hackerTimer > 0;
@@ -361,15 +361,23 @@ namespace TheOtherRoles
                         SpriteRenderer renderer = icon.GetComponent<SpriteRenderer>();
 
                         if (renderer != null) {
-                            if (defaultIcon == null) defaultIcon = renderer.sprite;
-                            if (showHackerInfo && colors.Count > i && Hacker.getAdminTableIconSprite() != null) {
-                                renderer.sprite = Hacker.getAdminTableIconSprite();
-                                renderer.color = colors[i];
+                            if (defaultMat == null) defaultMat = renderer.material;
+                            if (newMat == null) newMat = UnityEngine.Object.Instantiate<Material>(defaultMat);
+                            if (showHackerInfo && colors.Count > i) {
+                                renderer.material = newMat;
+                                var color = colors[i];
+                                renderer.material.SetColor("_BodyColor", color);
+                                var id = Palette.PlayerColors.IndexOf(color);
+                                if (id < 0) {
+                                    renderer.material.SetColor("_BackColor", color);
+                                } else {
+                                    renderer.material.SetColor("_BackColor", Palette.ShadowColors[id]);
+                                }
+                                renderer.material.SetColor("_VisorColor", Palette.VisorColor);
                             } else {
-                                renderer.sprite = defaultIcon;
-                                renderer.color = Color.white;
+                                renderer.material = defaultMat;
                             }
-                        } 
+                        }
                     }
                 }
             }