]> git.deb.at Git - rhonda/theotherroles.git/commitdiff
Update to v4.8.0
authorgendelo3 <gendelo3@gmail.com>
Sun, 23 Feb 2025 14:29:07 +0000 (15:29 +0100)
committergendelo3 <gendelo3@gmail.com>
Sun, 23 Feb 2025 14:29:07 +0000 (15:29 +0100)
37 files changed:
README.md
TheOtherRoles/Buttons.cs
TheOtherRoles/CustomGameModes/PropHunt.cs
TheOtherRoles/CustomOptionHolder.cs
TheOtherRoles/Helpers.cs
TheOtherRoles/Main.cs
TheOtherRoles/Modules/CustomColors.cs
TheOtherRoles/Modules/CustomOptions.cs
TheOtherRoles/Modules/RoleDraft.cs [new file with mode: 0644]
TheOtherRoles/Objects/Bomb.cs
TheOtherRoles/Objects/CustomButton.cs
TheOtherRoles/Objects/Portal.cs
TheOtherRoles/Objects/Trap.cs
TheOtherRoles/Patches/ClientOptionsPatch.cs
TheOtherRoles/Patches/CredentialsPatch.cs
TheOtherRoles/Patches/EndGamePatch.cs
TheOtherRoles/Patches/ExileControllerPatch.cs
TheOtherRoles/Patches/GameStartManagerPatch.cs
TheOtherRoles/Patches/IntroPatch.cs
TheOtherRoles/Patches/MainMenuPatch.cs
TheOtherRoles/Patches/MapBehaviourPatch.cs
TheOtherRoles/Patches/MeetingPatch.cs
TheOtherRoles/Patches/PlayerControlPatch.cs
TheOtherRoles/Patches/RoleAssignmentPatch.cs
TheOtherRoles/Patches/UpdatePatch.cs
TheOtherRoles/Patches/UsablesPatch.cs
TheOtherRoles/RPC.cs
TheOtherRoles/Resources/Endscreen.png [new file with mode: 0644]
TheOtherRoles/Resources/EndscreenActive.png [new file with mode: 0644]
TheOtherRoles/Resources/EventKickButton.png [new file with mode: 0644]
TheOtherRoles/Resources/SoundEffects/SoundEffectSourcesAndLicenses.md
TheOtherRoles/Resources/SoundEffects/toraudio
TheOtherRoles/RoleInfo.cs
TheOtherRoles/SoundEffectsManager.cs
TheOtherRoles/TheOtherRoles.cs
TheOtherRoles/TheOtherRoles.csproj
TheOtherRoles/Utilities/EventUtility.cs

index 8a4671aee2b3234cbcd3c66cb82fe1d273abc358..440adc0ee82b037eeabb208d888f3c7a3963e8b6 100644 (file)
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ The [Role Assignment](#role-assignment) section explains how the roles are being
 # Releases
 | Among Us - Version| Mod Version | Link |
 |----------|-------------|-----------------|
+| 2024.11.26| v4.8.0| [Download](https://github.com/TheOtherRolesAU/TheOtherRoles/releases/download/v4.8.0/TheOtherRoles.zip)
 | 2024.11.26| v4.7.0| [Download](https://github.com/TheOtherRolesAU/TheOtherRoles/releases/download/v4.7.0/TheOtherRoles.zip)
 | 2024.6.18| v4.6.0| [Download](https://github.com/TheOtherRolesAU/TheOtherRoles/releases/download/v4.6.0/TheOtherRoles.zip)
 
@@ -135,6 +136,18 @@ The [Role Assignment](#role-assignment) section explains how the roles are being
 # Changelog
 <details>
   <summary>Click to show the Changelog</summary>
+  
+**Version 4.8.0**
+- Added an optional Role Draft mode, where players can select their role out of some roles that are shown to them.
+- Added a new option to allow the medic to shift the medic shield as well - no more invincible medics.
+- Added partial key rebinding - all kill buttons will now use the vanilla kill button shortcut, same for vent and each roles first ability.
+- Fixed the way options view panel to match vanilla changes
+- Fixed options not showing/hiding sub-options when switching Presets
+- Fixed a bug where voting the witch would not save the target
+- Fixed a some bugs with the trapper, bomber and portal
+- Fixed a bug in PropHunt where you players could not transform into props
+- Fixed the summary button for the last game appearing outside the lobby sometimes
+- Changed the positioning of the ping tracker in meetings
 
 **Version 4.7.0**
 - Updated to Among Us version 2024.11.26 (Vanilla Updates)
@@ -904,6 +917,7 @@ Thanks to miniduikboot & GD for hosting modded servers (and so much more)
 [TheEpicRoles](https://github.com/LaicosVK/TheEpicRoles) - Idea for the first kill shield (partly) and the tabbed option menu (fully + some code), by **LaicosVK** **DasMonschta** **Nova**\
 [Ninja](#ninja), [Thief](#thief), [Lawyer](#lawyer) / [Pursuer](#pursuer), [Deputy](#deputy), [Portalmaker](#portalmaker), [Guesser Modifier](#guesser-modifier) - Idea: [K3ndo](https://github.com/K3ndoo) ; Developed by [Gendelo](https://github.com/gendelo3) & [Mallöris](https://github.com/Mallaris) \
 [ugackMiner53](https://github.com/ugackMiner53/PropHunt) - Idea and core code for the Prop Hunt game mode
+Role Draft Music: [Unreal Superhero 3 by Kenët & Rez](https://www.youtube.com/watch?v=9STiQ8cCIo0)
 
 # Settings
 The mod adds a few new settings to Among Us (in addition to the role settings):
index 488418770ecf5f8d5b493b6ef44e68bca24cc5a9..ff290641e81965bff04f92f74b8ca6d780f46b1e 100644 (file)
@@ -72,6 +72,7 @@ namespace TheOtherRoles
         private static CustomButton propHuntSpeedboostButton;
         public static CustomButton propHuntAdminButton;
         public static CustomButton propHuntFindButton;
+        public static CustomButton eventKickButton;
 
         public static Dictionary<byte, List<CustomButton>> deputyHandcuffedButtons = null;
         public static PoolablePlayer targetDisplay;
@@ -658,7 +659,7 @@ namespace TheOtherRoles
                Hacker.getAdminSprite(),
                CustomButton.ButtonPositions.lowerRowRight,
                __instance,
-               KeyCode.Q,
+               KeyCode.G,
                true,
                0f,
                () => { 
@@ -719,7 +720,7 @@ namespace TheOtherRoles
                Hacker.getVitalsSprite(),
                CustomButton.ButtonPositions.lowerRowCenter,
                __instance,
-               KeyCode.Q,
+               KeyCode.H,
                true,
                0f,
                () => {
@@ -773,7 +774,7 @@ namespace TheOtherRoles
                 Tracker.getTrackCorpsesButtonSprite(),
                 CustomButton.ButtonPositions.lowerRowCenter,
                 __instance,
-                KeyCode.Q,
+                KeyCode.G,
                 true,
                 Tracker.corpsesTrackingDuration,
                 () => {
@@ -964,7 +965,7 @@ namespace TheOtherRoles
                 Portalmaker.getUsePortalButtonSprite(),
                 new Vector3(0.9f, -0.06f, 0),
                 __instance,
-                KeyCode.H,
+                KeyCode.J,
                 mirror: true
             );
 
@@ -1004,7 +1005,7 @@ namespace TheOtherRoles
                 Portalmaker.getUsePortalButtonSprite(),
                 new Vector3(0.9f, 1f, 0),
                 __instance,
-                KeyCode.J,
+                KeyCode.G,
                 mirror: true
             );
 
@@ -1360,7 +1361,7 @@ namespace TheOtherRoles
                 SecurityGuard.getCamSprite(),
                 CustomButton.ButtonPositions.lowerRowRight,
                 __instance,
-                KeyCode.Q,
+                KeyCode.G,
                 true,
                 0f,
                 () => {
@@ -2390,6 +2391,26 @@ namespace TheOtherRoles
                 buttonText: "FIND"
                 );
 
+            eventKickButton = new CustomButton(
+                () => {
+                    EventUtility.kickTarget();
+                },
+                () => { return EventUtility.isEnabled && Mini.mini != null && !Mini.mini.Data.IsDead && PlayerControl.LocalPlayer != Mini.mini; },
+                () => { return EventUtility.currentTarget != null; },
+                () => { },
+                EventUtility.getKickButtonSprite(),
+                CustomButton.ButtonPositions.highRowRight,
+                __instance,
+                KeyCode.K,
+                true,
+                3f,
+                () => {
+                    // onEffectEnds
+                    eventKickButton.Timer = 69;
+                },
+                buttonText: "KICK"
+                );
+
             // Set the default (or settings from the previous game) timers / durations when spawning the buttons
             initialized = true;
             setCustomButtonCooldowns();
index 346e9786f04dab32e034c3e9b8a450ed092a2404..5a54161f146b031372aaf58061e15d4d763b915f 100644 (file)
@@ -13,8 +13,6 @@ using TheOtherRoles.Utilities;
 using TMPro;
 using UnityEngine;
 using UnityEngine.Video;
-using static TheOtherRoles.Snitch;
-using static UnityEngine.GraphicsBuffer;
 
 namespace TheOtherRoles.CustomGameModes {
     [HarmonyPatch]
@@ -78,7 +76,6 @@ namespace TheOtherRoles.CustomGameModes {
         public static float dangerMeterActive = 0f;
 
         private static List<GameObject> duplicatedCollider = new();
-        private static GameObject introObject;
 
         public static void clearAndReload() {
             remainingShots.Clear();
@@ -368,7 +365,7 @@ namespace TheOtherRoles.CustomGameModes {
                     }
                     bool whiteListed = false;
                     foreach (var whiteListedWord in whitelistedObjects) {
-                        if (collider.gameObject.name.Contains(whiteListedWord)) whiteListed = true;
+                        if ((bool)(collider.gameObject?.name?.Contains(whiteListedWord))) whiteListed = true;
                     }
                     if (collider.GetComponent<Console>() != null || whiteListed) {
                         float dist = Vector2.Distance(origin.transform.position, collider.transform.position);
@@ -379,7 +376,9 @@ namespace TheOtherRoles.CustomGameModes {
                     }
                 }
                 return bestCollider.gameObject;
-            } catch { return null; }
+            } catch (Exception e) {
+                TheOtherRolesPlugin.Logger.LogError($"Error in find closest disguise object: {e}");
+                return null; }
         }
 
         public static GameObject FindPropByNameAndPos(string propName, float posX) {
@@ -450,7 +449,6 @@ namespace TheOtherRoles.CustomGameModes {
                     HudManager.Instance.FullScreen.enabled = false;
                     videoPlayer.Destroy();
                     assetBundle.Unload(false);
-                    introObject.Destroy();
                 } else {
                     HudManager.Instance.FullScreen.enabled = true;
                     HudManager.Instance.FullScreen.gameObject.SetActive(true);
@@ -523,14 +521,15 @@ namespace TheOtherRoles.CustomGameModes {
         [HarmonyPostfix]
         public static void MapSetPostfix() {  // Make sure the map in the settings is in sync with the map from li
             if (TORMapOptions.gameMode != CustomGamemodes.PropHunt && TORMapOptions.gameMode != CustomGamemodes.HideNSeek || AmongUsClient.Instance.IsGameStarted) return;
-            int map = GameOptionsManager.Instance.currentGameOptions.MapId;
+            int? map = GameOptionsManager.Instance?.currentGameOptions?.MapId;
+            if (map == null) return;
             if (map > 3) map--;
             if (TORMapOptions.gameMode == CustomGamemodes.HideNSeek)
                 if (CustomOptionHolder.hideNSeekMap.selection != map)
-                    CustomOptionHolder.hideNSeekMap.updateSelection(map);
+                    CustomOptionHolder.hideNSeekMap.updateSelection((int)map);
             if (TORMapOptions.gameMode == CustomGamemodes.PropHunt)
                 if (CustomOptionHolder.propHuntMap.selection != map)
-                    CustomOptionHolder.propHuntMap.updateSelection(map);
+                    CustomOptionHolder.propHuntMap.updateSelection((int)map);
         }
 
 
index 2b1389937f43a01e9a74a06aad66f58170845112..66c3436db9699dbb5ee4db98612f4cc865a7ead2 100644 (file)
@@ -21,8 +21,18 @@ namespace TheOtherRoles {
         public static CustomOption modifiersCountMin;
         public static CustomOption modifiersCountMax;
 
+        public static CustomOption isDraftMode;
+        public static CustomOption draftModeAmountOfChoices;
+        public static CustomOption draftModeTimeToChoose;
+        public static CustomOption draftModeShowRoles;
+        public static CustomOption draftModeHideImpRoles;
+        public static CustomOption draftModeHideNeutralRoles;
+
         public static CustomOption anyPlayerCanStopStart;
         public static CustomOption enableEventMode;
+        public static CustomOption eventReallyNoMini;
+        public static CustomOption eventKicksPerRound;
+        public static CustomOption eventHeavyAge;
         public static CustomOption deadImpsBlockSabotage;
 
         public static CustomOption mafiaSpawnRate;
@@ -315,6 +325,7 @@ namespace TheOtherRoles {
         public static CustomOption modifierArmored;
 
         public static CustomOption modifierShifter;
+        public static CustomOption modifierShifterShiftsMedicShield;
 
         public static CustomOption maxNumberOfMeetings;
         public static CustomOption blockSkippingInEmergencyMeetings;
@@ -433,6 +444,13 @@ namespace TheOtherRoles {
             
             if (Utilities.EventUtility.canBeEnabled) enableEventMode = CustomOption.Create(10423, Types.General, cs(Color.green, "Enable Special Mode"), true, null, true);
 
+            isDraftMode = CustomOption.Create(600, Types.General, cs(Color.yellow, "Enable Role Draft"), false, null, true, null, "Role Draft");
+            draftModeAmountOfChoices = CustomOption.Create(601, Types.General, cs(Color.yellow, "Max Amount Of Roles\nTo Choose From"), 5f, 2f, 15f, 1f, isDraftMode, false);
+            draftModeTimeToChoose = CustomOption.Create(602, Types.General, cs(Color.yellow, "Time For Selection"), 5f, 3f, 20f, 1f, isDraftMode, false);
+            draftModeShowRoles = CustomOption.Create(603, Types.General, cs(Color.yellow, "Show Picked Roles"), false, isDraftMode, false);
+            draftModeHideImpRoles = CustomOption.Create(604, Types.General, cs(Color.yellow, "Hide Impostor Roles"), false, draftModeShowRoles, false);
+            draftModeHideNeutralRoles = CustomOption.Create(605, Types.General, cs(Color.yellow, "Hide Neutral Roles"), false, draftModeShowRoles, false);
+
             // Using new id's for the options to not break compatibilty with older versions
             crewmateRolesCountMin = CustomOption.Create(300, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Crewmate Roles"), 15f, 0f, 15f, 1f, null, true, heading: "Min/Max Roles");
             crewmateRolesCountMax = CustomOption.Create(301, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Crewmate Roles"), 15f, 0f, 15f, 1f);            
@@ -718,6 +736,11 @@ namespace TheOtherRoles {
             modifierMini = CustomOption.Create(1061, Types.Modifier, cs(Color.yellow, "Mini"), rates, null, true);
             modifierMiniGrowingUpDuration = CustomOption.Create(1062, Types.Modifier, "Mini Growing Up Duration", 400f, 100f, 1500f, 100f, modifierMini);
             modifierMiniGrowingUpInMeeting = CustomOption.Create(1063, Types.Modifier, "Mini Grows Up In Meeting", true, modifierMini);
+            if (Utilities.EventUtility.canBeEnabled || Utilities.EventUtility.isEnabled) {
+                eventKicksPerRound = CustomOption.Create(10424, Types.Modifier, cs(Color.green, "Maximum Kicks Mini Suffers"), 4f, 0f, 14f, 1f, modifierMini);
+                eventHeavyAge = CustomOption.Create(10425, Types.Modifier, cs(Color.green, "Age At Which Mini Is Heavy"), 12f, 6f, 18f, 0.5f, modifierMini);
+                eventReallyNoMini = CustomOption.Create(10426, Types.Modifier, cs(Color.green, "Really No Mini :("), false, modifierMini, invertedParent: true);
+            }
 
             modifierVip = CustomOption.Create(1070, Types.Modifier, cs(Color.yellow, "VIP"), rates, null, true);
             modifierVipQuantity = CustomOption.Create(1071, Types.Modifier, cs(Color.yellow, "VIP Quantity"), ratesModifier, modifierVip);
@@ -736,6 +759,7 @@ namespace TheOtherRoles {
             modifierArmored = CustomOption.Create(1101, Types.Modifier, cs(Color.yellow, "Armored"), rates, null, true);
 
             modifierShifter = CustomOption.Create(1100, Types.Modifier, cs(Color.yellow, "Shifter"), rates, null, true);
+            modifierShifterShiftsMedicShield = CustomOption.Create(1102, Types.Modifier, "Can Shift Medic Shield", false, modifierShifter);
 
             // Guesser Gamemode (2000 - 2999)
             guesserGamemodeCrewNumber = CustomOption.Create(2001, Types.Guesser, cs(Guesser.color, "Number of Crew Guessers"), 15f, 0f, 15f, 1f, null, true, heading: "Amount of Guessers");
index 2e51993d7b2a368b63667a574b92e88a813bd871..202a48f3c0878d39f0a966433252aacaa6f6dba9 100644 (file)
@@ -33,7 +33,7 @@ namespace TheOtherRoles {
     }
     public static class Helpers
     {
-
+        public static string previousEndGameSummary = "";
         public static Dictionary<string, Sprite> CachedSprites = new();
 
         public static Sprite loadSpriteFromResources(string path, float pixelsPerUnit, bool cache=true) {
index a3a42d4ffce9007b2453c03e3745496cd80a0b0d..1d38bfed4e3c33960328d50525364a7469000d4c 100644 (file)
@@ -32,7 +32,7 @@ namespace TheOtherRoles
     public class TheOtherRolesPlugin : BasePlugin
     {
         public const string Id = "me.eisbison.theotherroles";
-        public const string VersionString = "4.7.0";
+        public const string VersionString = "4.8.0";
         public static uint betaDays = 0;  // amount of days for the build to be usable (0 for infinite!)
 
         public static Version Version = Version.Parse(VersionString);
index 980055350afa949d1ae641de13a78d835952c006..50fc75dfafa46d9d228f67164daebd3282148685 100644 (file)
@@ -204,7 +204,6 @@ namespace TheOtherRoles.Modules {
                         str = ColorUtility.ToHtmlStringRGB(c);
                         
                         color = c.r + c.g + c.b  > 180 ? Palette.Black : Palette.White;
-                        TheOtherRolesPlugin.Logger.LogMessage($"{c.r}, {c.g}, {c.b}");
                     }
                     __instance.playerColorText.text = __instance.player.ColorBlindName;
                     __instance.playerNameText.text = "<color=#" + str + ">" + (string.IsNullOrEmpty(sender.Data.PlayerName) ? "..." : sender.Data.PlayerName);
index cdbf309a549c10f1d49549f9e9e4e47d496626a4..f1af1fd788d91cd3c03863a4f9c648b9ffbc67e8 100644 (file)
@@ -52,10 +52,11 @@ namespace TheOtherRoles {
         public CustomOptionType type;
         public Action onChange = null;
         public string heading = "";
+        public bool invertedParent;
 
         // Option creation
 
-        public CustomOption(int id, CustomOptionType type, string name,  System.Object[] selections, System.Object defaultValue, CustomOption parent, bool isHeader, Action onChange = null, string heading = "") {
+        public CustomOption(int id, CustomOptionType type, string name,  System.Object[] selections, System.Object defaultValue, CustomOption parent, bool isHeader, Action onChange = null, string heading = "", bool invertedParent = false) {
             this.id = id;
             this.name = parent == null ? name : "- " + name;
             this.selections = selections;
@@ -66,6 +67,7 @@ namespace TheOtherRoles {
             this.type = type;
             this.onChange = onChange;
             this.heading = heading;
+            this.invertedParent = invertedParent;
             selection = 0;
             if (id != 0) {
                 entry = TheOtherRolesPlugin.Instance.Config.Bind($"Preset{preset}", id.ToString(), defaultSelection);
@@ -74,19 +76,19 @@ namespace TheOtherRoles {
             options.Add(this);
         }
 
-        public static CustomOption Create(int id, CustomOptionType type, string name, string[] selections, CustomOption parent = null, bool isHeader = false, Action onChange = null, string heading = "") {
-            return new CustomOption(id, type, name, selections, "", parent, isHeader, onChange, heading);
+        public static CustomOption Create(int id, CustomOptionType type, string name, string[] selections, CustomOption parent = null, bool isHeader = false, Action onChange = null, string heading = "", bool invertedParent = false) {
+            return new CustomOption(id, type, name, selections, "", parent, isHeader, onChange, heading, invertedParent);
         }
 
-        public static CustomOption Create(int id, CustomOptionType type, string name, float defaultValue, float min, float max, float step, CustomOption parent = null, bool isHeader = false, Action onChange = null, string heading = "") {
+        public static CustomOption Create(int id, CustomOptionType type, string name, float defaultValue, float min, float max, float step, CustomOption parent = null, bool isHeader = false, Action onChange = null, string heading = "", bool invertedParent = false) {
             List<object> selections = new();
             for (float s = min; s <= max; s += step)
                 selections.Add(s);
-            return new CustomOption(id, type, name, selections.ToArray(), defaultValue, parent, isHeader, onChange, heading);
+            return new CustomOption(id, type, name, selections.ToArray(), defaultValue, parent, isHeader, onChange, heading, invertedParent);
         }
 
-        public static CustomOption Create(int id, CustomOptionType type, string name, bool defaultValue, CustomOption parent = null, bool isHeader = false, Action onChange = null, string heading = "") {
-            return new CustomOption(id, type, name, new string[]{"Off", "On"}, defaultValue ? "On" : "Off", parent, isHeader, onChange, heading);
+        public static CustomOption Create(int id, CustomOptionType type, string name, bool defaultValue, CustomOption parent = null, bool isHeader = false, Action onChange = null, string heading = "", bool invertedParent = false) {
+            return new CustomOption(id, type, name, new string[]{"Off", "On"}, defaultValue ? "On" : "Off", parent, isHeader, onChange, heading, invertedParent);
         }
 
         // Static behaviour
@@ -106,6 +108,17 @@ namespace TheOtherRoles {
                     stringOption.ValueText.text = option.selections[option.selection].ToString();
                 }
             }
+
+            // make sure to reload all tabs, even the ones in the background, because they might have changed when the preset was switched!
+            if (AmongUsClient.Instance?.AmHost == true) {
+                foreach (var entry in GameOptionsMenuStartPatch.currentGOMs) {
+                    CustomOptionType optionType = (CustomOptionType)entry.Key;
+                    GameOptionsMenu gom = entry.Value;
+                    if (gom != null) {
+                        GameOptionsMenuStartPatch.updateGameOptionsMenu(optionType, gom);
+                    }
+                }
+            }
         }
 
         public static void saveVanillaOptions() {
@@ -190,13 +203,7 @@ namespace TheOtherRoles {
             try {
                 if (onChange != null) onChange();
             } catch { }
-            if (AmongUsClient.Instance?.AmHost == true) {
-                var currentTab = GameOptionsMenuStartPatch.currentTabs.FirstOrDefault(x => x.active).GetComponent<GameOptionsMenu>();
-                if (currentTab != null) {
-                    var optionType = options.First(x => x.optionBehaviour == currentTab.Children[0]).type;
-                    GameOptionsMenuStartPatch.updateGameOptionsMenu(optionType, currentTab);
-                }
-            }
+
 
             if (optionBehaviour != null && optionBehaviour is StringOption stringOption) {
                 stringOption.oldValue = stringOption.Value = selection;
@@ -215,6 +222,15 @@ namespace TheOtherRoles {
                 ShareOptionSelections();// Share all selections
             }
 
+            if (AmongUsClient.Instance?.AmHost == true) {
+                var currentTab = GameOptionsMenuStartPatch.currentTabs.FirstOrDefault(x => x.active).GetComponent<GameOptionsMenu>();
+                if (currentTab != null) {
+                    var optionType = options.First(x => x.optionBehaviour == currentTab.Children[0]).type;
+                    GameOptionsMenuStartPatch.updateGameOptionsMenu(optionType, currentTab);
+                }
+                
+            }
+
         }
 
         public static byte[] serializeOptions() {
@@ -455,15 +471,19 @@ namespace TheOtherRoles {
 
             float num = 1.44f;
             int i = 0;
-            int singles = 0;
+            int singles = 1;
             int headers = 0;
             int lines = 0;
             var curType = CustomOptionType.Modifier;
+            int numBonus = 0;
 
             foreach (var option in relevantOptions) {
                 if (option.isHeader && (int)optionType != 99 || (int)optionType == 99 && curType != option.type) {
                     curType = option.type;
-                    if (i != 0) num -= 0.59f;
+                    if (i != 0) {
+                        num -= 0.85f;
+                        numBonus++;
+                    }
                     if (i % 2 != 0) singles++;
                     headers++; // for header
                     CategoryHeaderMasked categoryHeaderMasked = UnityEngine.Object.Instantiate<CategoryHeaderMasked>(__instance.categoryHeaderOrigin);
@@ -478,7 +498,7 @@ namespace TheOtherRoles {
                     categoryHeaderMasked.transform.localScale = Vector3.one;
                     categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, num, -2f);
                     __instance.settingsInfo.Add(categoryHeaderMasked.gameObject);
-                    num -= 0.85f;
+                    num -= 1.05f;
                     i = 0;
                 } else if (option.parent != null && (option.parent.selection == 0 || option.parent.parent != null && option.parent.parent.selection == 0)) continue;  // Hides options, for which the parent is disabled!
                 if (option == CustomOptionHolder.crewmateRolesCountMax || option == CustomOptionHolder.neutralRolesCountMax || option == CustomOptionHolder.impostorRolesCountMax || option == CustomOptionHolder.modifiersCountMax || option == CustomOptionHolder.crewmateRolesFill)
@@ -492,7 +512,7 @@ namespace TheOtherRoles {
                     lines++;
                     num2 = -8.95f;
                     if (i > 0) {
-                        num -= 0.59f;
+                        num -= 0.85f;
                     }
                 } else {
                     num2 = -3f;
@@ -515,8 +535,8 @@ namespace TheOtherRoles {
 
                 i++;
             }
-            float actual_spacing = (headers * 0.85f + lines * 0.59f) / (headers + lines);
-            __instance.scrollBar.CalculateAndSetYBounds((float)(__instance.settingsInfo.Count + singles * 2 + headers), 2f, 6f, actual_spacing);
+            float actual_spacing = (headers * 1.05f + lines * 0.85f) / (headers + lines) * 1.01f;
+            __instance.scrollBar.CalculateAndSetYBounds((float)(__instance.settingsInfo.Count + singles * 2 + headers), 2f, 5f, actual_spacing);
 
         }
 
@@ -620,12 +640,13 @@ namespace TheOtherRoles {
     class GameOptionsMenuStartPatch {
         public static List<GameObject> currentTabs = new();
         public static List<PassiveButton> currentButtons = new();
-
+        public static Dictionary<byte, GameOptionsMenu> currentGOMs = new();
         public static void Postfix(GameSettingMenu __instance) {
             currentTabs.ForEach(x => x?.Destroy());
             currentButtons.ForEach(x => x?.Destroy());
             currentTabs = new();
             currentButtons = new();
+            currentGOMs.Clear();
 
             if (GameOptionsManager.Instance.currentGameOptions.GameMode == GameModes.HideNSeek) return;
 
@@ -701,7 +722,8 @@ namespace TheOtherRoles {
                     categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f;
                     categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f);
                     num -= 0.63f;
-                } else if (option.parent != null && (option.parent.selection == 0 || option.parent.parent != null && option.parent.parent.selection == 0)) continue;  // Hides options, for which the parent is disabled!
+                } else if (option.parent != null && (option.parent.selection == 0 && !option.invertedParent || option.parent.parent != null && option.parent.parent.selection == 0 && !option.parent.invertedParent)) continue;  // Hides options, for which the parent is disabled!
+                else if (option.parent != null && option.parent.selection != 0 && option.invertedParent) continue;
                 OptionBehaviour optionBehaviour = UnityEngine.Object.Instantiate<StringOption>(menu.stringOptionOrigin, Vector3.zero, Quaternion.identity, menu.settingsContainer);
                 optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f);
                 optionBehaviour.SetClickMask(menu.ButtonClickMask);
@@ -788,6 +810,7 @@ namespace TheOtherRoles {
 
             currentTabs.Add(torSettingsTab);
             torSettingsTab.SetActive(false);
+            currentGOMs.Add((byte)optionType, torSettingsGOM);
         }
 
         public static void updateGameOptionsMenu(CustomOptionType optionType, GameOptionsMenu torSettingsGOM) {
@@ -976,7 +999,7 @@ namespace TheOtherRoles {
                     if (type == CustomOption.CustomOptionType.Modifier) line += buildModifierExtras(option);
                     sb.AppendLine(line);
                 }
-                else if (option.parent.getSelection() > 0) {
+                else if (option.parent.getSelection() > 0 || option.invertedParent && option.parent.getSelection() == 0) {
                     if (option.id == 103) //Deputy
                         sb.AppendLine($"- {Helpers.cs(Deputy.color, "Deputy")}: {option.selections[option.selection].ToString()}");
                     else if (option.id == 224) //Sidekick
@@ -992,7 +1015,7 @@ namespace TheOtherRoles {
                 if (TORMapOptions.gameMode == CustomGamemodes.HideNSeek && option.type != CustomOptionType.HideNSeekMain && option.type != CustomOptionType.HideNSeekRoles) continue;
                 if (TORMapOptions.gameMode == CustomGamemodes.PropHunt && option.type != CustomOptionType.PropHunt) continue;
                 if (option.parent != null) {
-                    bool isIrrelevant = option.parent.getSelection() == 0 || (option.parent.parent != null && option.parent.parent.getSelection() == 0);
+                    bool isIrrelevant = (option.parent.getSelection() == 0 && !option.invertedParent) || (option.parent.parent != null && option.parent.parent.getSelection() == 0 && !option.parent.invertedParent);
 
                     Color c = isIrrelevant ? Color.grey : Color.white;  // No use for now
                     if (isIrrelevant) continue;
@@ -1244,6 +1267,8 @@ namespace TheOtherRoles {
             }
             if (Input.GetKeyDown(KeyCode.F1))
                 HudManagerUpdate.ToggleSettings(HudManager.Instance);
+            if (Input.GetKeyDown(KeyCode.F2) && LobbyBehaviour.Instance)
+                HudManagerUpdate.ToggleSummary(HudManager.Instance);
             if (TheOtherRolesPlugin.optionsPage >= GameOptionsDataPatch.maxPage) TheOtherRolesPlugin.optionsPage = 0;
         }
     }
@@ -1363,6 +1388,9 @@ namespace TheOtherRoles {
         private static GameObject settingsBackground;
         public static void OpenSettings(HudManager __instance) {
             if (__instance.FullScreen == null || MapBehaviour.Instance && MapBehaviour.Instance.IsOpen) return;
+            if (summaryTMP) {
+                CloseSummary();
+            }
             settingsBackground = GameObject.Instantiate(__instance.FullScreen.gameObject, __instance.transform);
             settingsBackground.SetActive(true);
             var renderer = settingsBackground.GetComponent<SpriteRenderer>();
@@ -1390,9 +1418,54 @@ namespace TheOtherRoles {
             else OpenSettings(__instance);
         }
 
+        [HarmonyPrefix]
+        public static void Prefix3(HudManager __instance) {
+            if (!summaryTMP) return;
+            summaryTMP.text = Helpers.previousEndGameSummary;
+
+            summaryTMP.transform.localPosition = new Vector3(- 3 * 1.2f, 2.2f, -500f);
+
+        }
+
+        private static TMPro.TextMeshPro summaryTMP = null;
+        private static GameObject summaryBackground;
+        public static void OpenSummary(HudManager __instance) {
+            if (__instance.FullScreen == null || MapBehaviour.Instance && MapBehaviour.Instance.IsOpen || Helpers.previousEndGameSummary.IsNullOrWhiteSpace()) return;
+            if (settingsTMPs[0]) {
+                CloseSettings();
+            }
+            summaryBackground = GameObject.Instantiate(__instance.FullScreen.gameObject, __instance.transform);
+            summaryBackground.SetActive(true);
+            var renderer = summaryBackground.GetComponent<SpriteRenderer>();
+            renderer.color = new Color(0.2f, 0.2f, 0.2f, 0.9f);
+            renderer.enabled = true;
+
+
+            summaryTMP = GameObject.Instantiate(__instance.KillButton.cooldownTimerText, __instance.transform);
+            summaryTMP.alignment = TMPro.TextAlignmentOptions.TopLeft;
+            summaryTMP.enableWordWrapping = false;
+            summaryTMP.transform.localScale = Vector3.one * 0.3f; 
+            summaryTMP.gameObject.SetActive(true);
+
+        }
+
+        public static void CloseSummary() {
+            summaryTMP?.gameObject.Destroy();
+            summaryTMP = null;
+            if (summaryBackground) summaryBackground.Destroy();
+        }
+
+        public static void ToggleSummary(HudManager __instance) {
+            if (summaryTMP) CloseSummary();
+            else OpenSummary(__instance);
+        }
+
         static PassiveButton toggleSettingsButton;
         static GameObject toggleSettingsButtonObject;
 
+        static PassiveButton toggleSummaryButton;
+        static GameObject toggleSummaryButtonObject;
+
         static GameObject toggleZoomButtonObject;
         static PassiveButton toggleZoomButton;
 
@@ -1416,7 +1489,6 @@ namespace TheOtherRoles {
             toggleSettingsButtonObject.SetActive(__instance.MapButton.gameObject.active && !(MapBehaviour.Instance && MapBehaviour.Instance.IsOpen) && GameOptionsManager.Instance.currentGameOptions.GameMode != GameModes.HideNSeek);
             toggleSettingsButtonObject.transform.localPosition = __instance.MapButton.transform.localPosition + new Vector3(0, -0.8f, -500f);
 
-
             if (!toggleZoomButton || !toggleZoomButtonObject) {
                 // add a special button for settings viewing:
                 toggleZoomButtonObject = GameObject.Instantiate(__instance.MapButton.gameObject, __instance.MapButton.transform.parent);
@@ -1439,5 +1511,34 @@ namespace TheOtherRoles {
             var posOffset = Helpers.zoomOutStatus ? new Vector3(-1.27f, -7.92f, -52f) : new Vector3(0, -1.6f, -52f);
             toggleZoomButtonObject.transform.localPosition = HudManager.Instance.MapButton.transform.localPosition + posOffset;
         }
+
+        [HarmonyPostfix]
+        public static void Postfix2(HudManager __instance) {
+            if (AmongUsClient.Instance.GameState == InnerNet.InnerNetClient.GameStates.Started) {
+                if (toggleSummaryButtonObject != null) {
+                    toggleSummaryButtonObject.SetActive(false);
+                    toggleSummaryButtonObject.Destroy();
+                    toggleSummaryButton.Destroy();
+                }
+                return;
+            }
+            if (!toggleSummaryButton || !toggleSummaryButtonObject) {
+                // add a special button for settings viewing:
+                toggleSummaryButtonObject = GameObject.Instantiate(__instance.MapButton.gameObject, __instance.MapButton.transform.parent);
+                toggleSummaryButtonObject.transform.localPosition = __instance.MapButton.transform.localPosition + new Vector3(0, -1.25f, -500f);
+                toggleSummaryButtonObject.name = "TOGGLESUMMARYSBUTTON";
+                SpriteRenderer renderer = toggleSummaryButtonObject.transform.Find("Inactive").GetComponent<SpriteRenderer>();
+                SpriteRenderer rendererActive = toggleSummaryButtonObject.transform.Find("Active").GetComponent<SpriteRenderer>();
+                toggleSummaryButtonObject.transform.Find("Background").localPosition = Vector3.zero;
+                renderer.sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.Endscreen.png", 100f);
+                rendererActive.sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.EndscreenActive.png", 100f);
+                toggleSummaryButton = toggleSummaryButtonObject.GetComponent<PassiveButton>();
+                toggleSummaryButton.OnClick.RemoveAllListeners();
+                toggleSummaryButton.OnClick.AddListener((Action)(() => ToggleSummary(__instance)));
+            }
+            toggleSummaryButtonObject.SetActive(__instance.SettingsButton.gameObject.active && LobbyBehaviour.Instance && !Helpers.previousEndGameSummary.IsNullOrWhiteSpace() && GameOptionsManager.Instance.currentGameOptions.GameMode != GameModes.HideNSeek 
+                && AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started);
+            toggleSummaryButtonObject.transform.localPosition = __instance.SettingsButton.transform.localPosition + new Vector3(-1.45f, 0.03f, -500f);
+        }
     }
 }
diff --git a/TheOtherRoles/Modules/RoleDraft.cs b/TheOtherRoles/Modules/RoleDraft.cs
new file mode 100644 (file)
index 0000000..97bd2fa
--- /dev/null
@@ -0,0 +1,434 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+using HarmonyLib;
+using Hazel;
+using BepInEx.Unity.IL2CPP.Utils.Collections;
+using System.Collections;
+using TheOtherRoles.Patches;
+using static TheOtherRoles.TheOtherRoles;
+using UnityEngine.UI;
+using Reactor.Utilities.Extensions;
+
+namespace TheOtherRoles.Modules
+{
+    [HarmonyPatch]
+    class RoleDraft
+    {
+        public static bool isEnabled => CustomOptionHolder.isDraftMode.getBool() && (TORMapOptions.gameMode == CustomGamemodes.Classic || TORMapOptions.gameMode == CustomGamemodes.Guesser);
+        public static bool isRunning = false;
+
+        public static List<byte> pickOrder = new();
+        private static bool picked = false;
+        private static float timer = 0f;
+        private static List<ActionButton> buttons = new List<ActionButton>();
+        private static TMPro.TextMeshPro feedText;
+        public static List<byte> alreadyPicked = new();
+        public static IEnumerator CoSelectRoles(IntroCutscene __instance)
+        {
+            isRunning = true;
+            SoundEffectsManager.play("draft", volume: 1f, true, true);
+            alreadyPicked.Clear();
+            bool playedAlert = false;
+            feedText = UnityEngine.Object.Instantiate(__instance.TeamTitle, __instance.transform);
+            var aspectPosition = feedText.gameObject.AddComponent<AspectPosition>();
+            aspectPosition.Alignment = AspectPosition.EdgeAlignments.LeftTop;
+            aspectPosition.DistanceFromEdge = new Vector2(1.62f, 1.2f);
+            aspectPosition.AdjustPosition();
+            feedText.transform.localScale = new Vector3(0.6f, 0.6f, 1);
+            feedText.text = "<size=200%>Player's Picks:</size>\n\n";
+            feedText.alignment = TMPro.TextAlignmentOptions.TopLeft;
+            feedText.autoSizeTextContainer = true;
+            feedText.fontSize = 3f;
+            feedText.enableAutoSizing = false;
+            __instance.TeamTitle.transform.localPosition = __instance.TeamTitle.transform.localPosition + new Vector3(1f, 0f);
+            __instance.TeamTitle.text = "Currently Picking:";
+            __instance.BackgroundBar.enabled = false;
+            __instance.TeamTitle.transform.localScale = new Vector3(0.25f, 0.25f, 1f);
+            __instance.TeamTitle.autoSizeTextContainer = true;
+            __instance.TeamTitle.enableAutoSizing = false;
+            __instance.TeamTitle.fontSize = 5;
+            __instance.TeamTitle.alignment = TMPro.TextAlignmentOptions.Top;
+            __instance.ImpostorText.gameObject.SetActive(false);
+            GameObject.Find("BackgroundLayer")?.SetActive(false);
+            foreach (var player in UnityEngine.Object.FindObjectsOfType<PoolablePlayer>())
+            {
+                if (player.name.Contains("Dummy"))
+                {
+                    player.gameObject.SetActive(false);
+                }
+            }
+            __instance.FrontMost.gameObject.SetActive(false);
+
+            if (AmongUsClient.Instance.AmHost)
+            {
+                sendPickOrder();
+            }
+
+            while (pickOrder.Count == 0)
+            {
+                yield return null;
+            }
+
+            while (pickOrder.Count > 0) {
+                picked = false;
+                timer = 0;
+                float maxTimer = CustomOptionHolder.draftModeTimeToChoose.getFloat();
+                string playerText = "";
+                while (timer < maxTimer || !picked) {
+                    if (pickOrder.Count == 0)
+                        break;
+                    // wait for pick
+                    timer += Time.deltaTime;
+                    if (PlayerControl.LocalPlayer.PlayerId == pickOrder[0]) {
+                        if (!playedAlert) {
+                            playedAlert = true;
+                            SoundManager.Instance.PlaySound(ShipStatus.Instance.SabotageSound, false, 1f, null);
+                        }
+                        // Animate beginning of choice, by changing background color
+                        float min = 50 / 255f;
+                        Color backGroundColor = new Color(min, min, min, 1);
+                        if (timer < 1) {
+                            float max = 230 / 255f;
+                            if (timer < 0.5f) { // White flash                              
+                                float p = timer / 0.5f;
+                                float value = (float)Math.Pow(p, 2f) * max;
+                                backGroundColor = new Color(value, value, value, 1);
+                            } else {
+                                float p = (1 - timer) / 0.5f;
+                                float value = (float)Math.Pow(p, 2f) * max + (1 - (float)Math.Pow(p, 2f)) * min;
+                                backGroundColor = new Color(value, value, value, 1);
+                            }
+
+                        }
+                        HudManager.Instance.FullScreen.color = backGroundColor;
+                        GameObject.Find("BackgroundLayer")?.SetActive(false);
+
+                        // enable pick, wait for pick
+                        Color youColor = timer - (int)timer > 0.5 ? Color.red : Color.yellow;
+                        playerText = Helpers.cs(youColor, "You!");
+                        // Available Roles:
+                        List<RoleInfo> availableRoles = new();
+                        foreach (RoleInfo roleInfo in RoleInfo.allRoleInfos) {
+                            int impostorCount = PlayerControl.AllPlayerControls.ToArray().ToList().Where(x => x.Data.Role.IsImpostor).Count();
+                            if (roleInfo.isModifier) continue;
+                            // Remove Impostor Roles
+                            if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && !roleInfo.isImpostor) continue;
+                            if (!PlayerControl.LocalPlayer.Data.Role.IsImpostor && roleInfo.isImpostor) continue;
+
+                            RoleManagerSelectRolesPatch.RoleAssignmentData roleData = RoleManagerSelectRolesPatch.getRoleAssignmentData();
+                            roleData.crewSettings.Add((byte)RoleId.Sheriff, CustomOptionHolder.sheriffSpawnRate.getSelection());
+                            if (CustomOptionHolder.sheriffSpawnRate.getSelection() > 0)
+                                roleData.crewSettings.Add((byte)RoleId.Deputy, CustomOptionHolder.deputySpawnRate.getSelection());
+                            if (roleData.neutralSettings.ContainsKey((byte)roleInfo.roleId) && roleData.neutralSettings[(byte)roleInfo.roleId] == 0) continue;
+                            else if (roleData.impSettings.ContainsKey((byte)roleInfo.roleId) && roleData.impSettings[(byte)roleInfo.roleId] == 0) continue;
+                            else if (roleData.crewSettings.ContainsKey((byte)roleInfo.roleId) && roleData.crewSettings[(byte)roleInfo.roleId] == 0) continue;
+                            else if (new List<RoleId>() { RoleId.Janitor, RoleId.Godfather, RoleId.Mafioso }.Contains(roleInfo.roleId) && (CustomOptionHolder.mafiaSpawnRate.getSelection() == 0 || GameOptionsManager.Instance.currentGameOptions.NumImpostors < 3)) continue;
+                            else if (roleInfo.roleId == RoleId.Sidekick) continue;
+                            if (roleInfo.roleId == RoleId.Deputy && Sheriff.sheriff == null) continue;
+                            if (roleInfo.roleId == RoleId.Pursuer) continue;
+                            if (roleInfo.roleId == RoleId.Spy && impostorCount < 2) continue;
+                            if (roleInfo.roleId == RoleId.Prosecutor && (CustomOptionHolder.lawyerIsProsecutorChance.getSelection() == 0 || CustomOptionHolder.lawyerSpawnRate.getSelection() == 0)) continue;
+                            if (roleInfo.roleId == RoleId.Lawyer && (CustomOptionHolder.lawyerIsProsecutorChance.getSelection() == 10 || CustomOptionHolder.lawyerSpawnRate.getSelection() == 0)) continue;
+                            if (TORMapOptions.gameMode == CustomGamemodes.Guesser && (roleInfo.roleId == RoleId.EvilGuesser || roleInfo.roleId == RoleId.NiceGuesser)) continue;
+                            if (alreadyPicked.Contains((byte)roleInfo.roleId) && roleInfo.roleId != RoleId.Crewmate) continue;
+                            if (CustomOptionHolder.crewmateRolesFill.getBool() && roleInfo.roleId == RoleId.Crewmate) continue;
+
+                            int impsPicked = alreadyPicked.Where(x => RoleInfo.roleInfoById[(RoleId)x].isImpostor).Count();
+
+                            // Hanlde forcing of 100% roles for impostors
+                            if (PlayerControl.LocalPlayer.Data.Role.IsImpostor) {
+                                int impsMax = CustomOptionHolder.impostorRolesCountMax.getSelection();
+                                int impsMin = CustomOptionHolder.impostorRolesCountMin.getSelection();
+                                if (impsMin > impsMax) impsMin = impsMax;
+                                int impsLeft = pickOrder.Where(x => Helpers.playerById(x).Data.Role.IsImpostor).Count();
+                                int imps100 = roleData.impSettings.Where(x => x.Value == 10).Count();
+                                if (imps100 > impsMax) imps100 = impsMax;
+                                int imps100Picked = alreadyPicked.Where(x => roleData.impSettings.GetValueSafe(x) == 10).Count();
+                                if (imps100 - imps100Picked >= impsLeft && !(roleData.impSettings.Where(x => x.Value == 10 && x.Key == (byte)roleInfo.roleId).Count() > 0)) continue;
+                                if (impsMin - impsPicked >= impsLeft && roleInfo.roleId == RoleId.Impostor) continue;
+                                if (impsPicked >= impsMax && roleInfo.roleId != RoleId.Impostor) continue;
+                            }
+
+                            // Player is no impostor! Handle forcing of 100% roles for crew and neutral
+                            else {
+                                // No more neutrals possible!
+                                int neutralsPicked = alreadyPicked.Where(x => RoleInfo.roleInfoById[(RoleId)x].isNeutral).Count();
+                                int crewPicked = alreadyPicked.Count - impsPicked - neutralsPicked;
+                                int neutralsMax = CustomOptionHolder.neutralRolesCountMax.getSelection();
+                                int neutralsMin = CustomOptionHolder.neutralRolesCountMin.getSelection();
+                                int neutrals100 = roleData.neutralSettings.Where(x => x.Value == 10).Count();
+                                if (neutrals100 > neutralsMin) neutralsMin = neutrals100;
+                                if (neutralsMin > neutralsMax) neutralsMin = neutralsMax;
+
+                                // If crewmate fill disabled and crew picked the amount of allowed crewmates alreay: no more crewmate except vanilla crewmate allowed!
+                                int crewLimit = PlayerControl.AllPlayerControls.Count - impostorCount - (neutralsMin > neutrals100 ? neutralsMin : neutrals100 > neutralsMax ? neutralsMax : neutrals100);
+                                int maxCrew = CustomOptionHolder.crewmateRolesFill.getBool() ? CustomOptionHolder.crewmateRolesCountMax.getSelection() : crewLimit;
+                                if (maxCrew > crewLimit)
+                                    maxCrew = crewLimit;
+                                if (crewPicked >= crewLimit && !roleInfo.isNeutral && roleInfo.roleId != RoleId.Crewmate) continue;
+                                // Fill roles means no crewmates allowed!
+                                if (CustomOptionHolder.crewmateRolesFill.getBool() && roleInfo.roleId == RoleId.Crewmate) continue;
+
+                                bool allowAnyNeutral = false;
+                                if (neutralsPicked >= neutralsMax && roleInfo.isNeutral) continue;
+                                // More neutrals needed? Then no more crewmates! This takes precedence over crew roles set to 100%!
+                                var crewmatesLeft = pickOrder.Count - pickOrder.Where(x => Helpers.playerById(x).Data.Role.IsImpostor).Count();
+
+                                if (crewmatesLeft <= neutralsMin - neutralsPicked && !roleInfo.isNeutral) {
+                                    continue;
+                                } else if (neutralsMin - neutrals100 > neutralsPicked)
+                                    allowAnyNeutral = true;
+                                // Handle 100% Roles PER Faction.
+
+                                int neutrals100Picked = alreadyPicked.Where(x => roleData.neutralSettings.GetValueSafe(x) == 10).Count();
+                                if (neutrals100 > neutralsMax) neutrals100 = neutralsMax;
+
+                                int crew100 = roleData.crewSettings.Where(x => x.Value == 10).Count();
+                                int crew100Picked = alreadyPicked.Where(x => roleData.crewSettings.GetValueSafe(x) == 10).Count();
+                                if (neutrals100 > neutralsMax) neutrals100 = neutralsMax;
+
+                                if (crew100 > maxCrew) crew100 = maxCrew;
+                                if ((neutrals100 - neutrals100Picked >= crewmatesLeft || roleInfo.isNeutral && neutrals100 - neutrals100Picked >= neutralsMax - neutralsPicked) && !(neutrals100Picked >= neutralsMax) && !(roleData.neutralSettings.Where(x => x.Value == 10 && x.Key == (byte)roleInfo.roleId).Count() > 0)) continue;
+                                if (!(allowAnyNeutral && roleInfo.isNeutral) && crew100 - crew100Picked >= crewmatesLeft && !(roleData.crewSettings.Where(x => x.Value == 10 && x.Key == (byte)roleInfo.roleId).Count() > 0)) continue;
+
+                                if (!(allowAnyNeutral && roleInfo.isNeutral) && neutrals100 + crew100 - neutrals100Picked - crew100Picked >= crewmatesLeft && !(roleData.crewSettings.Where(x => x.Value == 10 && x.Key == (byte)roleInfo.roleId).Count() > 0 || roleData.neutralSettings.Where(x => x.Value == 10 && x.Key == (byte)roleInfo.roleId).Count() > 0)) continue;
+
+                            }
+                            // Handle role pairings that are blocked, e.g. Vampire Warlock, Cleaner Vulture etc.
+                            bool blocked = false;
+                            foreach (var blockedRoleId in CustomOptionHolder.blockedRolePairings) {
+                                if (alreadyPicked.Contains(blockedRoleId.Key) && blockedRoleId.Value.ToList().Contains((byte)roleInfo.roleId)) {
+                                    blocked = true;
+                                    break;
+                                }
+                            }
+                            if (blocked) continue;
+
+
+                            availableRoles.Add(roleInfo);
+                        }
+
+                        // Fallback for if all roles are somehow removed. (This is only the case if there is a bug, hence print a warning
+                        if (availableRoles.Count == 0) {
+                            if (PlayerControl.LocalPlayer.Data.Role.IsImpostor)
+                                availableRoles.Add(RoleInfo.impostor);
+                            else
+                                availableRoles.Add(RoleInfo.crewmate);
+                            TheOtherRolesPlugin.Logger.LogWarning("Draft Mode: Fallback triggered, because no roles were left. Forced addition of basegame Imp/Crewmate");
+                        }
+
+                        List<RoleInfo> originalAvailable = new(availableRoles);
+
+                        // remove some roles, so that you can't always get the same roles:
+                        if (availableRoles.Count > CustomOptionHolder.draftModeAmountOfChoices.getFloat()) {
+                            int countToRemove = availableRoles.Count - (int)CustomOptionHolder.draftModeAmountOfChoices.getFloat();
+                            while (countToRemove-- > 0) {
+                                var toRemove = availableRoles.OrderBy(_ => Guid.NewGuid()).First();
+                                availableRoles.Remove(toRemove);
+                            }
+                        }
+
+                        if (timer >= maxTimer) {
+                            sendPick((byte)originalAvailable.OrderBy(_ => Guid.NewGuid()).First().roleId);
+                        }
+
+
+                        if (GameObject.Find("RoleButton") == null) {
+                            SoundEffectsManager.play("timemasterShield");
+                            int i = 0;
+                            int buttonsPerRow = 4;
+                            int lastRow = availableRoles.Count / buttonsPerRow;
+                            int buttonsInLastRow = availableRoles.Count % buttonsPerRow;
+
+                            foreach (RoleInfo roleInfo in availableRoles) {
+                                float row = i / buttonsPerRow;
+                                float col = i % buttonsPerRow;
+                                if (buttonsInLastRow != 0 && row == lastRow) {
+                                    col += (buttonsPerRow - buttonsInLastRow) / 2f;
+                                }
+                                // planned rows: maximum of 4, hence the following calculation for rows as well:
+                                row += (4 - lastRow - 1) / 2f;
+
+                                ActionButton actionButton = UnityEngine.Object.Instantiate(HudManager.Instance.KillButton, __instance.TeamTitle.transform);
+                                actionButton.gameObject.SetActive(true);
+                                actionButton.gameObject.name = "RoleButton";
+                                actionButton.transform.localPosition = new Vector3(-8.4f + col * 5.5f, -10 - row * 3f);
+                                actionButton.transform.localScale = new Vector3(2f, 2f);
+                                actionButton.SetCoolDown(0, 0);
+                                GameObject textHolder = new GameObject("textHolder");
+                                var text = textHolder.AddComponent<TMPro.TextMeshPro>();
+                                text.text = roleInfo.name.Replace(" ", "\n");
+                                text.horizontalAlignment = TMPro.HorizontalAlignmentOptions.Center;
+                                text.fontSize = 5;
+                                textHolder.layer = actionButton.gameObject.layer;
+                                text.outlineWidth = 0.1f;
+                                text.outlineColor = Color.white;
+                                text.color = roleInfo.color;
+                                textHolder.transform.SetParent(actionButton.transform, false);
+                                textHolder.transform.localPosition = new Vector3(0, text.text.Contains("\n") ? -1.975f : -2.2f, -1);
+                                GameObject actionButtonGameObject = actionButton.gameObject;
+                                SpriteRenderer actionButtonRenderer = actionButton.graphic;
+                                Material actionButtonMat = actionButtonRenderer.material;
+
+                                PassiveButton button = actionButton.GetComponent<PassiveButton>();
+                                button.OnClick = new Button.ButtonClickedEvent();
+                                button.OnClick.AddListener((Action)(() => {
+                                    sendPick((byte)roleInfo.roleId);
+                                }));
+                                HudManager.Instance.StartCoroutine(Effects.Lerp(0.5f, new Action<float>((p) => {
+                                    actionButton.OverrideText("");
+                                })));
+                                buttons.Add(actionButton);
+                                i++;
+                            }
+                        }
+
+                    } else {
+                        int currentPick = PlayerControl.AllPlayerControls.Count - pickOrder.Count + 1;
+                        playerText = $"Anonymous Player {currentPick}";
+                        HudManager.Instance.FullScreen.color = Color.black;
+                    }
+                    __instance.TeamTitle.text = $"{Helpers.cs(Color.white, "<size=280%>Welcome to the Role Draft!</size>")}\n\n\n<size=200%> Currently Picking:</size>\n\n\n<size=250%>{playerText}</size>";
+                    int waitMore = pickOrder.IndexOf(PlayerControl.LocalPlayer.PlayerId);
+                    string waitMoreText = "";
+                    if (waitMore > 0) {
+                        waitMoreText = $" ({waitMore} rounds until your turn)";
+                    }
+                    __instance.TeamTitle.text += $"\n\n{waitMoreText}\nRandom Selection In... {(int)(maxTimer + 1 - timer)}\n {(SoundManager.MusicVolume > -80 ? "♫ Music: Ultimate Superhero 3 - Kenët & Rez ♫" : "")}";
+                    yield return null;
+                }
+            }
+            HudManager.Instance.FullScreen.color = Color.black;
+            __instance.FrontMost.gameObject.SetActive(true);
+            GameObject.Find("BackgroundLayer")?.SetActive(true);
+            if (AmongUsClient.Instance.AmHost)
+            {
+                RoleManagerSelectRolesPatch.assignRoleTargets(null); // Assign targets for Lawyer & Prosecutor
+                if (RoleManagerSelectRolesPatch.isGuesserGamemode) RoleManagerSelectRolesPatch.assignGuesserGamemode();
+                RoleManagerSelectRolesPatch.assignModifiers(); // Assign modifier
+            }
+
+            float myTimer = 0f;
+            while (myTimer < 3f)
+            {
+                myTimer += Time.deltaTime;
+                Color c = new Color(0, 0, 0, myTimer / 3.0f);
+                __instance.FrontMost.color = c;
+                yield return null;
+            }
+
+            SoundEffectsManager.stop("draft");
+            isRunning = false;
+            yield break;
+        }
+
+        public static void receivePick(byte playerId, byte roleId)
+        {
+            if (!isEnabled) return;
+            RPCProcedure.setRole(roleId, playerId);
+            alreadyPicked.Add(roleId);
+            try
+            {
+                pickOrder.Remove(playerId);
+                timer = 0;
+                picked = true;                
+                RoleInfo roleInfo = RoleInfo.allRoleInfos.First(x => (byte)x.roleId == roleId);
+                string roleString = Helpers.cs(roleInfo.color, roleInfo.name);
+                int roleLength = roleInfo.name.Length;  // Not used for now, but stores the amount of charactes of the roleString.
+                if (!CustomOptionHolder.draftModeShowRoles.getBool() && !(playerId == PlayerControl.LocalPlayer.PlayerId)) {
+                    roleString = "Unknown Role";
+                    roleLength = roleString.Length;
+                }                   
+                else if (CustomOptionHolder.draftModeHideImpRoles.getBool() && roleInfo.isImpostor && !(playerId == PlayerControl.LocalPlayer.PlayerId)) {
+                    roleString = Helpers.cs(Palette.ImpostorRed, "Impostor Role");
+                    roleLength = "Impostor Role".Length;
+                }                    
+                else if (CustomOptionHolder.draftModeHideNeutralRoles.getBool() && roleInfo.isNeutral && !(playerId == PlayerControl.LocalPlayer.PlayerId)) {
+                    roleString = Helpers.cs(Palette.Blue, "Neutral Role");
+                    roleLength = "Neutral Role".Length;
+                }                    
+                string line = $"{(playerId == PlayerControl.LocalPlayer.PlayerId ? "You" : alreadyPicked.Count)}:";
+                line = line + string.Concat(Enumerable.Repeat(" ", 6 - line.Length)) + roleString;
+                feedText.text += line + "\n";
+                SoundEffectsManager.play("select");
+            }
+            catch (Exception e) { TheOtherRolesPlugin.Logger.LogError(e); }
+        }
+
+        public static void sendPick(byte RoleId)
+        {
+            SoundEffectsManager.stop("timeMasterShield");
+            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.DraftModePick, SendOption.Reliable, -1);
+            writer.Write(PlayerControl.LocalPlayer.PlayerId);
+            writer.Write(RoleId);
+            AmongUsClient.Instance.FinishRpcImmediately(writer);
+            receivePick(PlayerControl.LocalPlayer.PlayerId, RoleId);
+
+            // destroy all the buttons:
+            foreach (var button in buttons)
+            {
+                button?.gameObject?.Destroy();
+            }
+            buttons.Clear();
+        }
+
+
+        public static void sendPickOrder()
+        {
+            pickOrder = PlayerControl.AllPlayerControls.ToArray().Select(x => x.PlayerId).OrderBy(_ => Guid.NewGuid()).ToList().ToList();
+            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.DraftModePickOrder, SendOption.Reliable, -1);
+            writer.Write((byte)pickOrder.Count);
+            foreach (var item in pickOrder)
+            {
+                writer.Write(item);
+            }
+            AmongUsClient.Instance.FinishRpcImmediately(writer);
+        }
+
+
+        public static void receivePickOrder(int amount, MessageReader reader)
+        {
+            pickOrder.Clear();
+            for (int i = 0; i < amount; i++)
+            {
+                pickOrder.Add(reader.ReadByte());
+            }
+        }
+
+        class PatchedEnumerator() : IEnumerable
+        {
+            public IEnumerator enumerator;
+            public IEnumerator Postfix;
+            public IEnumerator GetEnumerator()
+            {
+                while (enumerator.MoveNext())
+                {
+                    yield return enumerator.Current;
+                }
+                while (Postfix.MoveNext())
+                    yield return Postfix.Current;
+            }
+        }
+
+
+        [HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.ShowTeam))]
+
+        class ShowRolePatch
+        {
+            [HarmonyPostfix]
+            public static void Postfix(IntroCutscene __instance, ref Il2CppSystem.Collections.IEnumerator __result)
+            {
+                if (!isEnabled) return;
+                var newEnumerator = new PatchedEnumerator()
+                {
+                    enumerator = __result.WrapToManaged(),
+                    Postfix = CoSelectRoles(__instance)
+                };
+                __result = newEnumerator.GetEnumerator().WrapToIl2Cpp();
+            }
+
+        }
+    }
+}
index f881162fbb72c25ae8a4cfe1b3c4acc2a4303bcc..51cc0a078b1d31f54d97623333a894266916cb08 100644 (file)
@@ -94,7 +94,11 @@ namespace TheOtherRoles.Objects {
                     AmongUsClient.Instance.FinishRpcImmediately(writer);
                     GameHistory.overrideDeathReasonAndKiller(PlayerControl.LocalPlayer, DeadPlayer.CustomDeathReason.Bomb, killer: Bomber.bomber);
                 }
-                SoundEffectsManager.playAtPosition("bombExplosion", position, range: Bomber.hearRange) ;
+                try {
+                    SoundEffectsManager.playAtPosition("bombExplosion", position, maxDuration: 1.6f, range: Bomber.hearRange);
+                } catch (Exception e) {
+                    TheOtherRolesPlugin.Logger.LogWarning($"Exception in Sound Effect for Bomb explosion: {e}");
+                }
             }
             Bomber.clearBomb();
             canDefuse = false;
index ce4d85ab32b0ce5868b51e4026c29f89b0cc82a9..967a0a9871970a90d8ee99dc8ca67235c289e0d5 100644 (file)
@@ -1,6 +1,8 @@
 using Il2CppSystem.Runtime.ExceptionServices;
+using Rewired;
 using System;
 using System.Collections.Generic;
+using TheOtherRoles.Modules;
 using TMPro;
 using UnityEngine;
 using UnityEngine.UI;
@@ -9,6 +11,8 @@ using static TheOtherRoles.TheOtherRoles;
 namespace TheOtherRoles.Objects {
     public class CustomButton {
         public static List<CustomButton> buttons = new List<CustomButton>();
+        public static KeyCode Action2Keycode = KeyCode.G; //TheOtherRolesPlugin.Instance.Config.Bind("Buttons", "Action2Keycode", KeyCode.G, "Second Ability Button Key").Value;
+        public static KeyCode Action3Keycode = KeyCode.H; // TheOtherRolesPlugin.Instance.Config.Bind("Buttons", "Action3Keycode", KeyCode.H, "Third Ability Button Key").Value;
         public ActionButton actionButton;
         public GameObject actionButtonGameObject;
         public SpriteRenderer actionButtonRenderer;
@@ -32,6 +36,7 @@ namespace TheOtherRoles.Objects {
         public HudManager hudManager;
         public bool mirror;
         public KeyCode? hotkey;
+        public KeyCode? originalHotkey;
         public string buttonText;
         public bool isHandcuffed = false;
         private static readonly int Desat = Shader.PropertyToID("_Desat");
@@ -44,6 +49,7 @@ namespace TheOtherRoles.Objects {
             public static readonly Vector3 upperRowCenter = new Vector3(-1f, 1f, 0f);  // Not usable for imps beacuse of new button positions!
             public static readonly Vector3 upperRowLeft = new Vector3(-2f, 1f, 0f);
             public static readonly Vector3 upperRowFarLeft = new Vector3(-3f, 1f, 0f);
+            public static readonly Vector3 highRowRight = new Vector3(0f, 2.06f, 0f);
         }
 
         public CustomButton(Action OnClick, Func<bool> HasButton, Func<bool> CouldUse, Action OnMeetingEnds, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, KeyCode? hotkey, bool HasEffect, float EffectDuration, Action OnEffectEnds, bool mirror = false, string buttonText = "")
@@ -62,6 +68,7 @@ namespace TheOtherRoles.Objects {
             this.mirror = mirror;
             this.hotkey = hotkey;
             this.buttonText = buttonText;
+            originalHotkey = hotkey;
             Timer = 16.2f;
             buttons.Add(this);
             actionButton = UnityEngine.Object.Instantiate(hudManager.KillButton, hudManager.KillButton.transform.parent);
@@ -73,7 +80,6 @@ namespace TheOtherRoles.Objects {
             this.showButtonText = (actionButtonRenderer.sprite == Sprite || buttonText != "");
             button.OnClick = new Button.ButtonClickedEvent();
             button.OnClick.AddListener((UnityEngine.Events.UnityAction)onClickEvent);
-
             setActive(false);
         }
 
@@ -148,6 +154,33 @@ namespace TheOtherRoles.Objects {
             }
         }
 
+
+        // Reload the rebound hotkeys from the among us settings.
+        public static void ReloadHotkeys() {
+            foreach (var button in buttons) {
+                // Q button is used only for killing! This rebinds every button that would use Q to use the currently set killing button in among us.
+                if (button.originalHotkey == KeyCode.Q) {
+                    Player player = Rewired.ReInput.players.GetPlayer(0);
+                    string keycode = player.controllers.maps.GetFirstButtonMapWithAction(8, true).elementIdentifierName;
+                    button.hotkey = (KeyCode)Enum.Parse(typeof(KeyCode), keycode);
+                }
+                // F is the default ability button. All buttons that would use F now use the ability button.
+                if (button.originalHotkey == KeyCode.F) {
+                    Player player = Rewired.ReInput.players.GetPlayer(0);
+                    string keycode = player.controllers.maps.GetFirstButtonMapWithAction(49, true).elementIdentifierName;
+                    button.hotkey = (KeyCode)Enum.Parse(typeof(KeyCode), keycode);
+                }
+
+                if (button.originalHotkey == KeyCode.G) {
+                    button.hotkey = Action2Keycode;
+                }
+                if (button.originalHotkey == KeyCode.H) {
+                    button.hotkey = Action3Keycode;
+                }
+            }
+
+        }
+
         public void setActive(bool isActive) {
             if (isActive) {
                 actionButtonGameObject.SetActive(true);
@@ -210,7 +243,7 @@ namespace TheOtherRoles.Objects {
                 actionButtonMat.SetFloat(Desat, 1f);
             }
         
-            if (Timer >= 0) {
+            if (Timer >= 0 && !RoleDraft.isRunning) {  // Make sure role draft has finished or isnt running
                 if (HasEffect && isEffectActive)
                     Timer -= Time.deltaTime;
                 else if (!localPlayer.inVent && moveable)
index 11fa4b43350c811cd243b252d9580b043bc77ace..a6e4af3874b8b45f87ea8c7da56cc740ae966637 100644 (file)
@@ -108,7 +108,7 @@ namespace TheOtherRoles.Objects {
             else if (secondPortal == null) {
                 secondPortal = this;
             }
-            var lastRoom = FastDestroyableSingleton<HudManager>.Instance?.roomTracker.LastRoom.RoomId;
+            var lastRoom = FastDestroyableSingleton<HudManager>.Instance?.roomTracker?.LastRoom?.RoomId;
             this.room = lastRoom != null ? DestroyableSingleton<TranslationController>.Instance.GetString((SystemTypes)lastRoom) : "Open Field";
         }
 
index d6133881dfb40b411c2bf47ad347de03ccd59034..3232ae1b3a96c3185862c7941fc9bee09d16d044 100644 (file)
@@ -17,7 +17,7 @@ namespace TheOtherRoles.Objects {
         public bool triggerable = false;
         private int usedCount = 0;
         private int neededCount = Trapper.trapCountToReveal;
-        public List<PlayerControl> trappedPlayer = new List<PlayerControl>();
+        public List<byte> trappedPlayer = new List<byte>();
         private Arrow arrow = new Arrow(Color.blue);
 
         private static Sprite trapSprite;
@@ -84,13 +84,13 @@ namespace TheOtherRoles.Objects {
             }
             player.moveable = false;
             player.NetTransform.Halt();
-            Trapper.playersOnMap.Add(player); 
+            Trapper.playersOnMap.Add(player.PlayerId); 
             if (localIsTrapper) t.arrow.arrow.SetActive(true);
 
             FastDestroyableSingleton<HudManager>.Instance.StartCoroutine(Effects.Lerp(Trapper.trapDuration, new Action<float>((p) => { 
                 if (p == 1f) {
                     player.moveable = true;
-                    Trapper.playersOnMap.RemoveAll(x => x == player);
+                    Trapper.playersOnMap.RemoveAll(x => x == player.PlayerId);
                     if (trapPlayerIdMap.ContainsKey(playerId)) trapPlayerIdMap.Remove(playerId);
                     t.arrow.arrow.SetActive(false);
                 }
@@ -100,9 +100,8 @@ namespace TheOtherRoles.Objects {
                 t.revealed = true;
             }
 
-            t.trappedPlayer.Add(player);
+            t.trappedPlayer.Add(player.PlayerId);
             t.triggerable = true;
-
         }
 
         public static void Update() {
@@ -116,7 +115,7 @@ namespace TheOtherRoles.Objects {
             Trap target = null;
             foreach (Trap trap in traps) {
                 if (trap.arrow.arrow.active) trap.arrow.Update();
-                if (trap.revealed || !trap.triggerable || trap.trappedPlayer.Contains(player)) continue;
+                if (trap.revealed || !trap.triggerable || trap.trappedPlayer.Contains(player.PlayerId)) continue;
                 if (player.inVent || !player.CanMove) continue;
                 float distance = Vector2.Distance(trap.trap.transform.position, player.GetTruePosition());
                 if (distance <= ud && distance < closestDistance) {
index 36b2b6189efc03718af602220524fd3fb638480f..e88dd838d01fd387c1402c3f17906be11ff148c5 100644 (file)
@@ -20,7 +20,11 @@ namespace TheOtherRoles.Patches
             new("Ghosts Can Additionally See Modifier", () => TORMapOptions.ghostsSeeModifier = TheOtherRolesPlugin.GhostsSeeModifier.Value = !TheOtherRolesPlugin.GhostsSeeModifier.Value, TheOtherRolesPlugin.GhostsSeeModifier.Value),
             new("Show Role Summary", () => TORMapOptions.showRoleSummary = TheOtherRolesPlugin.ShowRoleSummary.Value = !TheOtherRolesPlugin.ShowRoleSummary.Value, TheOtherRolesPlugin.ShowRoleSummary.Value),
             new("Show Lighter / Darker", () => TORMapOptions.showLighterDarker = TheOtherRolesPlugin.ShowLighterDarker.Value = !TheOtherRolesPlugin.ShowLighterDarker.Value, TheOtherRolesPlugin.ShowLighterDarker.Value),
-            new("Enable Sound Effects", () => TORMapOptions.enableSoundEffects = TheOtherRolesPlugin.EnableSoundEffects.Value = !TheOtherRolesPlugin.EnableSoundEffects.Value, TheOtherRolesPlugin.EnableSoundEffects.Value),
+            new("Enable Sound Effects", () =>  {
+                TORMapOptions.enableSoundEffects = TheOtherRolesPlugin.EnableSoundEffects.Value = !TheOtherRolesPlugin.EnableSoundEffects.Value;
+                if (!TORMapOptions.enableSoundEffects) SoundEffectsManager.stopAll();
+                 return TORMapOptions.enableSoundEffects;
+                }, TheOtherRolesPlugin.EnableSoundEffects.Value),
             new("Show Vents On Map", () => TORMapOptions.ShowVentsOnMap = TheOtherRolesPlugin.ShowVentsOnMap.Value = !TheOtherRolesPlugin.ShowVentsOnMap.Value, TheOtherRolesPlugin.ShowVentsOnMap.Value),
             new("Show Chat Notifications", () => TORMapOptions.ShowChatNotifications = TheOtherRolesPlugin.ShowChatNotifications.Value = !TheOtherRolesPlugin.ShowChatNotifications.Value, TheOtherRolesPlugin.ShowChatNotifications.Value),
         };
@@ -189,7 +193,7 @@ namespace TheOtherRoles.Patches
                     button.Background.color = button.onState ? Color.green : Palette.ImpostorRed;
                 }));
                 
-                passiveButton.OnMouseOver.AddListener((Action) (() => button.Background.color = new Color32(34 ,139, 34, byte.MaxValue)));
+                passiveButton.OnMouseOver.AddListener((Action) (() => button.Background.color = button.onState ? new Color32(34 ,139, 34, byte.MaxValue): new Color32(139, 34, 34, byte.MaxValue)));
                 passiveButton.OnMouseOut.AddListener((Action) (() => button.Background.color = button.onState ? Color.green : Palette.ImpostorRed));
 
                 foreach (var spr in button.gameObject.GetComponentsInChildren<SpriteRenderer>())
index 778e86ad90883f027f5f9067e7ca1258b57b8205..82aa761d7ab870a0301d9093013b2a46ec624a23 100644 (file)
@@ -40,9 +40,9 @@ $@"<size=60%> <color=#FCCE03FF>Special thanks to Smeggy</color></size>";
                     if (HideNSeek.isHideNSeekGM) gameModeText = $"Hide 'N Seek";
                     else if (HandleGuesser.isGuesserGm) gameModeText = $"Guesser";
                     else if (PropHunt.isPropHuntGM) gameModeText = "Prop Hunt";
-                    if (gameModeText != "") gameModeText = Helpers.cs(Color.yellow, gameModeText) + "\n";
+                    if (gameModeText != "") gameModeText = Helpers.cs(Color.yellow, gameModeText) + (MeetingHud.Instance ? " " : "\n");
                     __instance.text.text = $"<size=130%><color=#ff351f>TheOtherRoles</color></size> v{TheOtherRolesPlugin.Version.ToString() + (TheOtherRolesPlugin.betaDays > 0 ? "-BETA" : "")}\n{gameModeText}" + __instance.text.text;
-                    position.DistanceFromEdge = new Vector3(2.25f, 0.11f, 0);
+                    position.DistanceFromEdge = MeetingHud.Instance ? new Vector3(1.25f, 0.15f, 0) : new Vector3(1.55f, 0.15f, 0);
                 } else {
                     string gameModeText = $"";
                     if (TORMapOptions.gameMode == CustomGamemodes.HideNSeek) gameModeText = $"Hide 'N Seek";
index ac3c1315f3b18f8a9be0bc3463be18ab9642ac2d..0e75acb917de8da5e570e11c23b937bfd37730b7 100644 (file)
@@ -388,6 +388,7 @@ namespace TheOtherRoles.Patches {
                 var roleSummaryTextMeshRectTransform = roleSummaryTextMesh.GetComponent<RectTransform>();
                 roleSummaryTextMeshRectTransform.anchoredPosition = new Vector2(position.x + 3.5f, position.y - 0.1f);
                 roleSummaryTextMesh.text = roleSummaryText.ToString();
+                Helpers.previousEndGameSummary = $"<size=110%>{roleSummaryText.ToString()}</size>";
             }
             AdditionalTempData.clear();
         }
index 5ff9fb3f693a1dab0d120dfc1c7f1eb16ed4b3aa..adb4008cf0710376805c547eca4a90a2006e1438 100644 (file)
@@ -10,7 +10,7 @@ using TheOtherRoles.Utilities;
 using UnityEngine;
 
 namespace TheOtherRoles.Patches {
-    [HarmonyPatch(typeof(ExileController), nameof(ExileController.Begin))]
+    [HarmonyPatch(typeof(ExileController), nameof(ExileController.BeginForGameplay))]
     [HarmonyPriority(Priority.First)]
     class ExileControllerBeginPatch {
         public static void Prefix(ExileController __instance, [HarmonyArgument(0)]ref NetworkedPlayerInfo exiled) {
@@ -57,7 +57,7 @@ namespace TheOtherRoles.Patches {
             if (Witch.witch != null && Witch.futureSpelled != null && AmongUsClient.Instance.AmHost) {
                 bool exiledIsWitch = exiled != null && exiled.PlayerId == Witch.witch.PlayerId;
                 bool witchDiesWithExiledLover = exiled != null && Lovers.existing() && Lovers.bothDie && (Lovers.lover1.PlayerId == Witch.witch.PlayerId || Lovers.lover2.PlayerId == Witch.witch.PlayerId) && (exiled.PlayerId == Lovers.lover1.PlayerId || exiled.PlayerId == Lovers.lover2.PlayerId);
-
+                
                 if ((witchDiesWithExiledLover || exiledIsWitch) && Witch.witchVoteSavesTargets) Witch.futureSpelled = new List<PlayerControl>();
                 foreach (PlayerControl target in Witch.futureSpelled) {
                     if (target != null && !target.Data.IsDead && Helpers.checkMuderAttempt(Witch.witch, target, true) == MurderAttemptResult.PerformKill){
index 1e0b3c6188bf4f0fb015313a038437386e23f5e0..205aed80fd834562e2e74eadfd7cb626053561c9 100644 (file)
@@ -89,6 +89,8 @@ namespace TheOtherRoles.Patches {
                         }
                     }
                 }
+
+
                 // Display message to the host
                 if (AmongUsClient.Instance.AmHost) {
                     if (versionMismatch) {
@@ -124,8 +126,12 @@ namespace TheOtherRoles.Patches {
                         PassiveButton startButtonPassiveButton = copiedStartButton.GetComponent<PassiveButton>();
                         void StopStartFunc() {
                             __instance.ResetStartState();
+                            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.StopStart, Hazel.SendOption.Reliable, -1);
+                            writer.Write(PlayerControl.LocalPlayer.PlayerId);
+                            AmongUsClient.Instance.FinishRpcImmediately(writer);
                             copiedStartButton.Destroy();
                             startingTimer = 0;
+                            SoundManager.Instance.StopSound(GameStartManager.Instance.gameStartSound);
                         }
                         startButtonPassiveButton.OnClick.AddListener((Action)(() => StopStartFunc()));
                         __instance.StartCoroutine(Effects.Lerp(.1f, new System.Action<float>((p) => {
@@ -180,12 +186,13 @@ namespace TheOtherRoles.Patches {
                         PassiveButton startButtonPassiveButton = copiedStartButton.GetComponent<PassiveButton>();
 
                         void StopStartFunc() {
-                            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.StopStart, Hazel.SendOption.Reliable, AmongUsClient.Instance.HostId);
+                            MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.StopStart, Hazel.SendOption.Reliable, -1);
                             writer.Write(PlayerControl.LocalPlayer.PlayerId);
                             AmongUsClient.Instance.FinishRpcImmediately(writer);
                             copiedStartButton.Destroy();
                             __instance.GameStartText.text = String.Empty;
                             startingTimer = 0;
+                            SoundManager.Instance.StopSound(GameStartManager.Instance.gameStartSound);
                         }
                         startButtonPassiveButton.OnClick.AddListener((Action)(() => StopStartFunc()));
                         __instance.StartCoroutine(Effects.Lerp(.1f, new System.Action<float>((p) => {
index cbf12f4602086ba982a9f7ead0bab69a51b334b6..ceda0a8e15b0d277837fa6880137697e2e0beb09 100644 (file)
@@ -8,6 +8,7 @@ using Hazel;
 
 using TheOtherRoles.Utilities;
 using TheOtherRoles.CustomGameModes;
+using TheOtherRoles.Modules;
 
 namespace TheOtherRoles.Patches {
     [HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.OnDestroy))]
@@ -82,10 +83,7 @@ namespace TheOtherRoles.Patches {
                     BountyHunter.cooldownText.transform.localScale = Vector3.one * 0.4f;
                     BountyHunter.cooldownText.gameObject.SetActive(true);
                 }
-            }
-
-            // Force Reload of SoundEffectHolder
-            SoundEffectsManager.Load();
+            }           
 
             // First kill
             if (AmongUsClient.Instance.AmHost && TORMapOptions.shieldFirstKill && TORMapOptions.firstKillName != "" && !HideNSeek.isHideNSeekGM && !PropHunt.isPropHuntGM) {
@@ -169,14 +167,26 @@ namespace TheOtherRoles.Patches {
                 }
                 yourTeam = fakeImpostorTeam;
             }
+
+            // Role draft: If spy is enabled, don't show the team
+            if (CustomOptionHolder.spySpawnRate.getSelection() > 0 && PlayerControl.AllPlayerControls.ToArray().ToList().Where(x => x.Data.Role.IsImpostor).Count() > 1) {
+                var fakeImpostorTeam = new Il2CppSystem.Collections.Generic.List<PlayerControl>(); // The local player always has to be the first one in the list (to be displayed in the center)
+                fakeImpostorTeam.Add(PlayerControl.LocalPlayer);
+                yourTeam = fakeImpostorTeam;
+            }
         }
 
         public static void setupIntroTeam(IntroCutscene __instance, ref  Il2CppSystem.Collections.Generic.List<PlayerControl> yourTeam) {
             List<RoleInfo> infos = RoleInfo.getRoleInfoForPlayer(PlayerControl.LocalPlayer);
             RoleInfo roleInfo = infos.Where(info => !info.isModifier).FirstOrDefault();
-            if (roleInfo == null) return;
-            if (roleInfo.isNeutral) {
-                var neutralColor = new Color32(76, 84, 78, 255);
+            var neutralColor = new Color32(76, 84, 78, 255);
+            if (roleInfo == null || roleInfo == RoleInfo.crewmate) {
+                if (RoleDraft.isEnabled && CustomOptionHolder.neutralRolesCountMax.getSelection() > 0) {
+                    __instance.TeamTitle.text = "<size=60%>Crewmate" + Helpers.cs(Color.white, " / ") + Helpers.cs(neutralColor, "Neutral") + "</size>";
+                }
+                return;
+            }
+            if (roleInfo.isNeutral) {                
                 __instance.BackgroundBar.material.color = neutralColor;
                 __instance.TeamTitle.text = "Neutral";
                 __instance.TeamTitle.color = neutralColor;
index 8dbdc85bc6ccf3bb1d20f04136aa4d0fb411b3e3..6dfd161684774eb82f343e3ee48b497668179ab3 100644 (file)
@@ -21,6 +21,10 @@ namespace TheOtherRoles.Modules {
         private static AnnouncementPopUp popUp;
 
         private static void Prefix(MainMenuManager __instance) {
+
+            // Force Reload of SoundEffectHolder
+            SoundEffectsManager.Load();
+
             var template = GameObject.Find("ExitGameButton");
             var template2 = GameObject.Find("CreditsButton");
             if (template == null || template2 == null) return;
@@ -115,8 +119,12 @@ TownOfUs - Idea for the Swapper, Shifter, Arsonist and a similar Mayor role came
 Ottomated - Idea for the Morphling, Snitch and Camouflager role came from Ottomated
 Crowded-Mod - Our implementation for 10+ player lobbies was inspired by the one from the Crowded Mod Team
 Goose-Goose-Duck - Idea for the Vulture role came from Slushiegoose
-TheEpicRoles - Idea for the first kill shield (partly) and the tabbed option menu (fully + some code), by LaicosVK DasMonschta Nova
-ugackMiner53 - Idea and core code for the Prop Hunt game mode</size>";
+TheEpicRoles - Idea for the first kill shield (partly) and the (old) tabbed option menu (fully + some code), by LaicosVK DasMonschta Nova
+ugackMiner53 - Idea and core code for the Prop Hunt game mode
+Role Draft Music: [https://www.youtube.com/watch?v=9STiQ8cCIo0]Unreal Superhero 3 by Kenët & Rez[]
+
+License: TheOtherRoles is licensed under the [https://github.com/TheOtherRolesAU/TheOtherRoles?tab=GPL-3.0-1-ov-file#readme]GPLv3[]
+</size>";
                 creditsString += "</align>";
 
                 Assets.InnerNet.Announcement creditsAnnouncement = new() {
index 2830179851740bfaaf72886eaf6c0395b89bc98a..872a8179fa89885e05da8a0f2c6777755990eeda 100644 (file)
@@ -40,23 +40,25 @@ namespace TheOtherRoles.Patches {
 
                        __instance.HerePoint.transform.SetLocalZ(-2.1f);
                        if (Trapper.trapper != null && PlayerControl.LocalPlayer.PlayerId == Trapper.trapper.PlayerId) {
-                               foreach (PlayerControl player in Trapper.playersOnMap) {
-                                       if (herePoints.ContainsKey(player.PlayerId)) continue;
-                                       Vector3 v = Trap.trapPlayerIdMap[player.PlayerId].trap.transform.position;
+                               foreach (byte playerId in Trapper.playersOnMap) {
+                                       if (herePoints.ContainsKey(playerId)) continue;
+                                       Vector3 v = Trap.trapPlayerIdMap[playerId].trap.transform.position;
                                        v /= MapUtilities.CachedShipStatus.MapScale;
                                        v.x *= Mathf.Sign(MapUtilities.CachedShipStatus.transform.localScale.x);
                                        v.z = -2.1f;
                                        var herePoint = UnityEngine.Object.Instantiate(__instance.HerePoint, __instance.HerePoint.transform.parent, true);
                                        herePoint.transform.localPosition = v;
                                        herePoint.enabled = true;
-                                       int colorId = player.CurrentOutfit.ColorId;
+                                       PlayerControl player = Helpers.playerById(playerId);
+                                       if (player == null) continue;
+                    int colorId = player.CurrentOutfit.ColorId;
                                        if (Trapper.anonymousMap) player.CurrentOutfit.ColorId = 6;
                                        player.SetPlayerMaterialColors(herePoint);
                                        player.CurrentOutfit.ColorId = colorId;
-                                       herePoints.Add(player.PlayerId, herePoint);
+                                       herePoints.Add(playerId, herePoint);
                                }
-                               foreach (var s in herePoints.Where(x => !Trapper.playersOnMap.Contains(Helpers.playerById(x.Key))).ToList()) {
-                                       UnityEngine.Object.Destroy(s.Value);
+                               foreach (var s in herePoints.Where(x => !Trapper.playersOnMap.Contains(x.Key)).ToList()) {
+                                       UnityEngine.Object.Destroy(s.Value.gameObject);
                                        herePoints.Remove(s.Key);
                                }
                        } else if (Snitch.snitch != null && PlayerControl.LocalPlayer.PlayerId == Snitch.snitch.PlayerId && !Snitch.snitch.Data.IsDead && Snitch.mode != Snitch.Mode.Chat) {
@@ -88,7 +90,7 @@ namespace TheOtherRoles.Patches {
                                                }
                                        } else {
                                                foreach (var s in herePoints) {
-                                                       UnityEngine.Object.Destroy(s.Value);
+                                                       UnityEngine.Object.Destroy(s.Value.gameObject);
                                                        herePoints.Remove(s.Key);
                                                }
                                        }
index da0f91b5cba80397e1cc1882bf57faf946f7c0f4..f022048c4adfa0da481bf2e6dd1c00b1d46a9d02 100644 (file)
@@ -713,7 +713,8 @@ namespace TheOtherRoles.Patches {
                         if (!trap.revealed) continue;
                         string message = $"Trap {trap.instanceId}: \n";
                         trap.trappedPlayer = trap.trappedPlayer.OrderBy(x => rnd.Next()).ToList();
-                        foreach (PlayerControl p in trap.trappedPlayer) {
+                        foreach (byte playerId in trap.trappedPlayer) {
+                            PlayerControl p = Helpers.playerById(playerId);
                             if (Trapper.infoType == 0) message += RoleInfo.GetRolesString(p, false, false, true) + "\n";
                             else if (Trapper.infoType == 1) {
                                 if (Helpers.isNeutral(p) || p.Data.Role.IsImpostor) message += "Evil Role \n";
@@ -755,7 +756,7 @@ namespace TheOtherRoles.Patches {
 
                 if (PlayerControl.LocalPlayer.Data.IsDead && output != "") FastDestroyableSingleton<HudManager>.Instance.Chat.AddChat(PlayerControl.LocalPlayer, $"{output}");
 
-                Trapper.playersOnMap = new List<PlayerControl>();
+                Trapper.playersOnMap = new ();
                 Snitch.playerRoomMap = new Dictionary<byte, byte>();
 
                 // Remove revealed traps
index 790f27abc0af2509e6cca9a6d31a0018ecdc65d5..65989d49b40df00d181248e3cdb5739ec8f66c1e 100644 (file)
@@ -21,7 +21,7 @@ namespace TheOtherRoles.Patches {
     public static class PlayerControlFixedUpdatePatch {
         // Helpers
 
-        static PlayerControl setTarget(bool onlyCrewmates = false, bool targetPlayersInVents = false, List<PlayerControl> untargetablePlayers = null, PlayerControl targetingPlayer = null) {
+        public static PlayerControl setTarget(bool onlyCrewmates = false, bool targetPlayersInVents = false, List<PlayerControl> untargetablePlayers = null, PlayerControl targetingPlayer = null) {
             PlayerControl result = null;
             float num = AmongUs.GameOptions.GameOptionsData.KillDistances[Mathf.Clamp(GameOptionsManager.Instance.currentNormalGameOptions.KillDistance, 0, 2)];
             if (!MapUtilities.CachedShipStatus) return result;
@@ -51,7 +51,7 @@ namespace TheOtherRoles.Patches {
             return result;
         }
 
-        static void setPlayerOutline(PlayerControl target, Color color) {
+        public static void setPlayerOutline(PlayerControl target, Color color) {
             if (target == null || target.cosmetics?.currentBodySprite?.BodySprite == null) return;
 
             color = color.SetAlpha(Chameleon.visibility(target.PlayerId));
index 012903bc64adabedacc4495d47545eb5a8095aa9..ba60885b8bfc29f868279d597efbe6a0d4fb93b4 100644 (file)
@@ -9,8 +9,10 @@ using AmongUs.GameOptions;
 using TheOtherRoles.Utilities;
 using static TheOtherRoles.TheOtherRoles;
 using TheOtherRoles.CustomGameModes;
+using TheOtherRoles.Modules;
 
-namespace TheOtherRoles.Patches {
+namespace TheOtherRoles.Patches
+{
     [HarmonyPatch(typeof(RoleOptionsCollectionV08), nameof(RoleOptionsCollectionV08.GetNumPerGame))]
     class RoleOptionsDataGetNumPerGamePatch{
         public static void Postfix(ref int __result) {
@@ -51,7 +53,8 @@ namespace TheOtherRoles.Patches {
             MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ResetVaribles, Hazel.SendOption.Reliable, -1);
             AmongUsClient.Instance.FinishRpcImmediately(writer);
             RPCProcedure.resetVariables();
-            if (TORMapOptions.gameMode == CustomGamemodes.HideNSeek || TORMapOptions.gameMode == CustomGamemodes.PropHunt || GameOptionsManager.Instance.currentGameOptions.GameMode == GameModes.HideNSeek) return; // Don't assign Roles in Hide N Seek
+            if (TORMapOptions.gameMode == CustomGamemodes.HideNSeek || TORMapOptions.gameMode == CustomGamemodes.PropHunt || GameOptionsManager.Instance.currentGameOptions.GameMode == GameModes.HideNSeek
+                || RoleDraft.isEnabled) return; // Don't assign Roles in Hide N Seek
             assignRoles();
         }
 
@@ -383,7 +386,7 @@ namespace TheOtherRoles.Patches {
             }
         }
 
-        private static void assignRoleTargets(RoleAssignmentData data) {
+        public static void assignRoleTargets(RoleAssignmentData data) {
             // Set Lawyer or Prosecutor Target
             if (Lawyer.lawyer != null) {
                 var possibleTargets = new List<PlayerControl>();
@@ -413,7 +416,7 @@ namespace TheOtherRoles.Patches {
             }
         }
 
-        private static void assignModifiers() {
+        public static void assignModifiers() {
             var modifierMin = CustomOptionHolder.modifiersCountMin.getSelection();
             var modifierMax = CustomOptionHolder.modifiersCountMax.getSelection();
             if (modifierMin > modifierMax) modifierMin = modifierMax;
@@ -485,7 +488,7 @@ namespace TheOtherRoles.Patches {
             assignModifiersToPlayers(chanceModifierToAssign, players, modifierCount); // Assign chance modifier
         }
 
-        private static void assignGuesserGamemode() {
+        public static void assignGuesserGamemode() {
             List<PlayerControl> impPlayer = PlayerControl.AllPlayerControls.ToArray().ToList().OrderBy(x => Guid.NewGuid()).ToList();
             List<PlayerControl> neutralPlayer = PlayerControl.AllPlayerControls.ToArray().ToList().OrderBy(x => Guid.NewGuid()).ToList();
             List<PlayerControl> crewPlayer = PlayerControl.AllPlayerControls.ToArray().ToList().OrderBy(x => Guid.NewGuid()).ToList();
@@ -596,7 +599,13 @@ namespace TheOtherRoles.Patches {
                 case RoleId.Tiebreaker:
                     selection = CustomOptionHolder.modifierTieBreaker.getSelection(); break;
                 case RoleId.Mini:
-                    selection = CustomOptionHolder.modifierMini.getSelection(); break;
+                    selection = CustomOptionHolder.modifierMini.getSelection();
+                    if (EventUtility.isEnabled) {
+                        selection = 10;
+                        if (CustomOptionHolder.modifierMini.getSelection() == 0 && CustomOptionHolder.eventReallyNoMini.getBool())
+                            selection = 0;
+                    }
+                    break;
                 case RoleId.Bait:
                     selection = CustomOptionHolder.modifierBait.getSelection();
                     if (multiplyQuantity) selection *= CustomOptionHolder.modifierBaitQuantity.getQuantity();
index 9079436897595c3ddad0ff5d8e936ea636a3a77d..0c1d717705a07fd31a4c8e3fdedf477b8eb84ad2 100644 (file)
@@ -314,7 +314,13 @@ namespace TheOtherRoles.Patches {
         {
             if (GameOptionsManager.Instance.currentGameOptions.GameMode == GameModes.HideNSeek) return;
             if (Deputy.handcuffedKnows.ContainsKey(PlayerControl.LocalPlayer.PlayerId) && Deputy.handcuffedKnows[PlayerControl.LocalPlayer.PlayerId] > 0 || MeetingHud.Instance) __instance.ImpostorVentButton.Hide();
-            else if (PlayerControl.LocalPlayer.roleCanUseVents() && !__instance.ImpostorVentButton.isActiveAndEnabled) __instance.ImpostorVentButton.Show();
+            else if (PlayerControl.LocalPlayer.roleCanUseVents() && !__instance.ImpostorVentButton.isActiveAndEnabled) {
+                __instance.ImpostorVentButton.Show();
+                
+            }
+            if (Rewired.ReInput.players.GetPlayer(0).GetButtonDown(RewiredConsts.Action.UseVent) && !PlayerControl.LocalPlayer.Data.Role.IsImpostor && PlayerControl.LocalPlayer.roleCanUseVents()) {
+                __instance.ImpostorVentButton.DoClick();
+            }
 
         }
 
index 1d054212ba10b0ecf25dd25501adacba21026812..19cc39ee7fff7f1f7921c68df161c540d3ecd58f 100644 (file)
@@ -104,12 +104,12 @@ namespace TheOtherRoles.Patches {
                 Deputy.setHandcuffedKnows();
                 return false;
             }
-            if (Trapper.playersOnMap.Contains(PlayerControl.LocalPlayer)) return false;
+            if (Trapper.playersOnMap.Contains(PlayerControl.LocalPlayer.PlayerId)) return false;
 
             bool canUse;
             bool couldUse;
             __instance.CanUse(PlayerControl.LocalPlayer.Data, out canUse, out couldUse);
-            bool canMoveInVents = PlayerControl.LocalPlayer != Spy.spy && !Trapper.playersOnMap.Contains(PlayerControl.LocalPlayer);
+            bool canMoveInVents = PlayerControl.LocalPlayer != Spy.spy && !Trapper.playersOnMap.Contains(PlayerControl.LocalPlayer.PlayerId);
             if (!canUse) return false; // No need to execute the native method as using is disallowed anyways
 
             bool isEnter = !PlayerControl.LocalPlayer.inVent;
@@ -139,7 +139,7 @@ namespace TheOtherRoles.Patches {
     [HarmonyPatch(typeof(Vent), nameof(Vent.TryMoveToVent))]
     public static class MoveToVentPatch {
         public static bool Prefix(Vent otherVent) {
-            return !Trapper.playersOnMap.Contains(PlayerControl.LocalPlayer);
+            return !Trapper.playersOnMap.Contains(PlayerControl.LocalPlayer.PlayerId);
         }
     }
 
index cecd478be73a27406a56d4426c7b6caac32447cc..7b5c294b2214b188241d63bc2669d9004f0d659a 100644 (file)
@@ -17,6 +17,7 @@ using AmongUs.Data;
 using AmongUs.GameOptions;
 using Assets.CoreScripts;
 using Reactor.Utilities.Extensions;
+using TheOtherRoles.Modules;
 namespace TheOtherRoles
 {
     public enum RoleId {
@@ -164,10 +165,13 @@ namespace TheOtherRoles
         PropHuntStartTimer,
         PropHuntSetInvis,
         PropHuntSetSpeedboost,
+        DraftModePickOrder,
+        DraftModePick,
 
         // Other functionality
         ShareTimer,
         ShareGhostInfo,
+        EventKick,
     }
 
     public static class RPCProcedure {
@@ -186,12 +190,14 @@ namespace TheOtherRoles
             clearAndReloadRoles();
             clearGameHistory();
             setCustomButtonCooldowns();
+            CustomButton.ReloadHotkeys();
             reloadPluginOptions();
             Helpers.toggleZoom(reset : true);
             GameStartManagerPatch.GameStartManagerUpdatePatch.startingTimer = 0;
             SurveillanceMinigamePatch.nightVisionOverlays = null;
             EventUtility.clearAndReload();
             MapBehaviourPatch.clearAndReload();
+            HudManagerUpdate.CloseSummary();
         }
 
     public static void HandleShareOptions(byte numberOfOptions, MessageReader reader) {            
@@ -231,7 +237,10 @@ namespace TheOtherRoles
         }
 
         public static void stopStart(byte playerId) {
-            if (AmongUsClient.Instance.AmHost && CustomOptionHolder.anyPlayerCanStopStart.getBool()) {
+            if (!CustomOptionHolder.anyPlayerCanStopStart.getBool())
+                return;
+            SoundManager.Instance.StopSound(GameStartManager.Instance.gameStartSound);
+            if (AmongUsClient.Instance.AmHost) {
                 GameStartManager.Instance.ResetStartState();
                 PlayerControl.LocalPlayer.RpcSendChat($"{Helpers.playerById(playerId).Data.PlayerName} stopped the game start!");
             }
@@ -1649,6 +1658,12 @@ namespace TheOtherRoles
                 case (byte)CustomRPC.PropHuntSetSpeedboost:
                     RPCProcedure.propHuntSetSpeedboost(reader.ReadByte());
                     break;
+                case (byte)CustomRPC.DraftModePickOrder:
+                    RoleDraft.receivePickOrder(reader.ReadByte(), reader);
+                    break;
+                case (byte)CustomRPC.DraftModePick:
+                    RoleDraft.receivePick(reader.ReadByte(), reader.ReadByte());
+                    break;
                 case (byte)CustomRPC.ShareGhostInfo:
                     RPCProcedure.receiveGhostInfo(reader.ReadByte(), reader);
                     break;
@@ -1659,6 +1674,11 @@ namespace TheOtherRoles
                     byte roomId = reader.ReadByte();
                     RPCProcedure.shareRoom(roomPlayer, roomId);
                     break;
+                case (byte)CustomRPC.EventKick:
+                    byte kickSource = reader.ReadByte();
+                    byte kickTarget = reader.ReadByte();
+                    EventUtility.handleKick(Helpers.playerById(kickSource), Helpers.playerById(kickTarget), reader.ReadSingle());
+                    break;
             }
         }
     }
diff --git a/TheOtherRoles/Resources/Endscreen.png b/TheOtherRoles/Resources/Endscreen.png
new file mode 100644 (file)
index 0000000..32a1e5a
Binary files /dev/null and b/TheOtherRoles/Resources/Endscreen.png differ
diff --git a/TheOtherRoles/Resources/EndscreenActive.png b/TheOtherRoles/Resources/EndscreenActive.png
new file mode 100644 (file)
index 0000000..1fda220
Binary files /dev/null and b/TheOtherRoles/Resources/EndscreenActive.png differ
diff --git a/TheOtherRoles/Resources/EventKickButton.png b/TheOtherRoles/Resources/EventKickButton.png
new file mode 100644 (file)
index 0000000..e8d8efa
Binary files /dev/null and b/TheOtherRoles/Resources/EventKickButton.png differ
index 33f11e89ac64258825ed5a7043f3a22dfbd3f77e..3e2e26d7196c2afafed3377ca5950ea8d945d7e8 100644 (file)
@@ -40,3 +40,5 @@
 |portalUse | | [Static electronic noise - Xbox 360](https://freesound.org/people/scenes/sounds/431654/ ) + [Teleport Slurp](https://freesound.org/people/GameAudio/sounds/220163/  ) | CC0 1.0 both |
 |fail | | [twitch: SmeggyTV: Mein Auge](https://www.twitch.tv/smeggytv/clip/KindQuaintEndiveDancingBanana-Zoc-bXUnqNCoqQD5 )  | License granted for use in TOR |
 |trapperTrap | | [Karabiner_Click_04.wav](https://freesound.org/people/Rudmer_Rotteveel/sounds/457454/ ) &  [Steel Spring Bear Trap](https://freesound.org/people/fractionalist/sounds/644245/ )| CC0 1.0 both |
+|select | | [Game Menu Select Sound 2](https://freesound.org/people/digimistic/sounds/705174/) | CC0 1.0 |
+|draft| [Unreal Superhero 3 by Kenët & Rez](https://www.youtube.com/watch?v=9STiQ8cCIo0) | License granted for use in TOR | 
index b3fb5d0f11b86e9b3579e1749ae7917d824bdd95..b9b4e5d655806513710ef87cefdc52b64bd52f42 100644 (file)
Binary files a/TheOtherRoles/Resources/SoundEffects/toraudio and b/TheOtherRoles/Resources/SoundEffects/toraudio differ
index 9ed2a45cb267f41ca5ddabe84fd17130aada680e..1efad096093bdd0c4d664ecae799897dd57abd28 100644 (file)
@@ -19,6 +19,8 @@ namespace TheOtherRoles
         public RoleId roleId;
         public bool isNeutral;
         public bool isModifier;
+        public bool isImpostor => color == Palette.ImpostorRed && !(roleId == RoleId.Spy);
+        public static Dictionary<RoleId, RoleInfo> roleInfoById = new();
 
         public RoleInfo(string name, Color color, string introDescription, string shortDescription, RoleId roleId, bool isNeutral = false, bool isModifier = false) {
             this.color = color;
@@ -28,6 +30,7 @@ namespace TheOtherRoles
             this.roleId = roleId;
             this.isNeutral = isNeutral;
             this.isModifier = isModifier;
+            roleInfoById.TryAdd(roleId, this);
         }
 
         public static RoleInfo jester = new RoleInfo("Jester", Jester.color, "Get voted out", "Get voted out", RoleId.Jester, true);
index 8061ccaa84c2dd89a2c513fe76a1161050f47b03..7c62aafdaba2324a83eb8fb304ad156ed2f74ceb 100644 (file)
@@ -15,6 +15,7 @@ namespace TheOtherRoles
         
     {
         private static Dictionary<string, AudioClip> soundEffects = new();
+        //private static List<AudioSource> currentSources = new();
 
         public static void Load()
         {
@@ -44,32 +45,50 @@ namespace TheOtherRoles
         {
             // Convenience: As as SoundEffects are stored in the same folder, allow using just the name as well
             //if (!path.Contains(".")) path = "TheOtherRoles.Resources.SoundEffects." + path + ".raw";
-            path = "assets/audio/" + path.ToLower() + ".ogg";
+            if (!path.Contains("assets")) path = "assets/audio/" + path.ToLower() + ".ogg";
             AudioClip returnValue;
             return soundEffects.TryGetValue(path, out returnValue) ? returnValue : null;
         }
 
 
-        public static void play(string path, float volume=0.8f, bool loop = false)
+        public static AudioSource play(string path, float volume=0.8f, bool loop = false, bool musicChannel=false)
         {
-            if (!TORMapOptions.enableSoundEffects) return;
+            if (!TORMapOptions.enableSoundEffects) return null;
             AudioClip clipToPlay = get(path);
             stop(path);
             if (Constants.ShouldPlaySfx() && clipToPlay != null) {
-                AudioSource source = SoundManager.Instance.PlaySound(clipToPlay, false, volume);
+                AudioSource source = SoundManager.Instance.PlaySound(clipToPlay, false, volume, audioMixer: musicChannel ? SoundManager.Instance.MusicChannel : null);
+                //currentSources.Add(source);
                 source.loop = loop;
+                return source;
             }
+            return null;
         }
         public static void playAtPosition(string path, Vector2 position, float maxDuration = 15f, float range = 5f, bool loop = false) {
             if (!TORMapOptions.enableSoundEffects || !Constants.ShouldPlaySfx()) return;
             AudioClip clipToPlay = get(path);
+            TheOtherRolesPlugin.Logger.LogMessage("play at  position");
+            if (clipToPlay == null) {
+                TheOtherRolesPlugin.Logger.LogMessage("clip is null");
+                return;
+            }
 
             AudioSource source = SoundManager.Instance.PlaySound(clipToPlay, false, 1f);
+            if (source == null) {
+                TheOtherRolesPlugin.Logger.LogMessage("source is null");
+                return;
+            }
+            //currentSources.Add(source);
             source.loop = loop;
             HudManager.Instance.StartCoroutine(Effects.Lerp(maxDuration, new Action<float>((p) => {
                 if (source != null) {
-                    if (p == 1) {
+                    if (p == 1 && source.isPlaying) {
                         source.Stop();
+                        try {
+                            //currentSources.Remove(source);
+                            source.Destroy();
+                        } 
+                        catch { }
                     }
                     float distance, volume;
                     distance = Vector2.Distance(position, PlayerControl.LocalPlayer.GetTruePosition());
@@ -80,17 +99,35 @@ namespace TheOtherRoles
                     source.volume = volume;
                 }
             })));
+            TheOtherRolesPlugin.Logger.LogMessage("end play at position");
         }
 
         public static void stop(string path) {
             var soundToStop = get(path);
-            if (soundToStop != null)
-                if (Constants.ShouldPlaySfx()) SoundManager.Instance.StopSound(soundToStop);
+            if (soundToStop != null) {
+                try {
+                    SoundManager.Instance?.StopSound(soundToStop);
+                }
+                catch (Exception e) { TheOtherRolesPlugin.Logger.LogWarning($"Exception in stop sound: {e}"); }
+            } 
         }
 
         public static void stopAll() {
             if (soundEffects == null) return;
-            foreach (var path in soundEffects.Keys) stop(path);
+            try {
+                foreach (var path in soundEffects.Keys) {
+                    stop(path);
+                }
+            }
+            catch { }
+
+            /*try {
+                foreach (var source in currentSources) {
+                    source?.Stop();
+                }
+                currentSources.Clear();
+            }
+            catch { }*/
         }
     }
 }
index ee52ab34ed86a5694527617da4a172a24407c3b9..befce5626776d3481ca71186c27e46c4e9b35cf9 100644 (file)
@@ -1560,7 +1560,7 @@ namespace TheOtherRoles
             vision = CustomOptionHolder.lawyerVision.getFloat();
             lawyerKnowsRole = CustomOptionHolder.lawyerKnowsRole.getBool();
             targetCanBeJester = CustomOptionHolder.lawyerTargetCanBeJester.getBool();
-            canCallEmergency = CustomOptionHolder.jesterCanCallEmergency.getBool();
+            canCallEmergency = CustomOptionHolder.lawyerCanCallEmergency.getBool();
         }
     }
 
@@ -1723,7 +1723,7 @@ namespace TheOtherRoles
         public static int rechargedTasks = 3;
         public static int charges = 1;
         public static int trapCountToReveal = 2;
-        public static List<PlayerControl> playersOnMap = new List<PlayerControl>();
+        public static List<byte> playersOnMap = new List<Byte>();
         public static bool anonymousMap = false;
         public static int infoType = 0; // 0 = Role, 1 = Good/Evil, 2 = Name
         public static float trapDuration = 5f; 
@@ -1744,7 +1744,7 @@ namespace TheOtherRoles
             rechargedTasks = Mathf.RoundToInt(CustomOptionHolder.trapperRechargeTasksNumber.getFloat());
             charges = Mathf.RoundToInt(CustomOptionHolder.trapperMaxCharges.getFloat()) / 2;
             trapCountToReveal = Mathf.RoundToInt(CustomOptionHolder.trapperTrapNeededTriggerToReveal.getFloat());
-            playersOnMap = new List<PlayerControl>();
+            playersOnMap = new ();
             anonymousMap = CustomOptionHolder.trapperAnonymousMap.getBool();
             infoType = CustomOptionHolder.trapperInfoType.getSelection();
             trapDuration = CustomOptionHolder.trapperTrapDuration.getFloat();
@@ -1774,6 +1774,7 @@ namespace TheOtherRoles
         }
 
         public static void clearBomb(bool flag = true) {
+            TheOtherRolesPlugin.Logger.LogDebug("Clearing Bomb!");
             if (bomb != null) {
                 UnityEngine.Object.Destroy(bomb.bomb);
                 UnityEngine.Object.Destroy(bomb.background);
@@ -2052,6 +2053,8 @@ namespace TheOtherRoles
         public static PlayerControl futureShift;
         public static PlayerControl currentTarget;
 
+        public static bool shiftsMedicShield = false;
+
         private static Sprite buttonSprite;
         public static Sprite getButtonSprite() {
             if (buttonSprite) return buttonSprite;
@@ -2088,6 +2091,8 @@ namespace TheOtherRoles
             }  else if (Medic.medic != null && Medic.medic == player2) {
                 if (repeat) shiftRole(player2, player1, false);
                 Medic.medic = player1;
+                if (Medic.shielded != null && Medic.shielded == player1 && shiftsMedicShield)
+                    Medic.shielded = player2;
             } else if (Swapper.swapper != null && Swapper.swapper == player2) {
                 if (repeat) shiftRole(player2, player1, false);
                 Swapper.swapper = player1;
@@ -2128,6 +2133,7 @@ namespace TheOtherRoles
             shifter = null;
             currentTarget = null;
             futureShift = null;
+            shiftsMedicShield = CustomOptionHolder.modifierShifterShiftsMedicShield.getBool();
         }
     }
 }
index 4d0f4abae4cedf65636b076c3132b07b9a69549b..526508d20a5bcc92dad2e76f2160e50c3d5b4d38 100644 (file)
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <TargetFramework>net6.0</TargetFramework>
-        <Version>4.7.0</Version>
+        <Version>4.8.0</Version>
         <Description>TheOtherRoles</Description>
         <Authors>Eisbison</Authors>
                <LangVersion>latest</LangVersion>
index ea9a9eed339225e7361d1df6c50c5e473993ba19..4270f546c0ecf38557baf87d50dce5049b78c6df 100644 (file)
@@ -5,29 +5,48 @@ using UnityEngine;
 using TheOtherRoles;
 using TheOtherRoles.Patches;
 
-using static TheOtherRoles.TheOtherRoles;
 using System.Linq;
 using InnerNet;
 using TheOtherRoles.Modules;
 using HarmonyLib;
+using Hazel;
 
 namespace TheOtherRoles.Utilities;
 
 [HarmonyPatch]
 public static class EventUtility {
 
+    private static Sprite kickButtonSprite;
+
+    public static Sprite getKickButtonSprite() {
+        
+        if (kickButtonSprite) return kickButtonSprite;
+        kickButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.EventKickButton.png", 115f);
+        return kickButtonSprite;
+    }
+
     public static void Load() {
         if (!isEnabled) return;
     }
 
     public static void clearAndReload() {
+        kickCounter = 0;
     }
 
     public static void Update() {
-        if (!isEnabled || AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started || TheOtherRoles.rnd == null || IntroCutscene.Instance) return;
+        //if (!isEnabled || AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started || TheOtherRoles.rnd == null || IntroCutscene.Instance) return;
+
+        // set Target
+        var untargetablePlayers = new List<PlayerControl>();
+        foreach (var player in PlayerControl.AllPlayerControls) {
+            if (Mini.mini != player)
+                untargetablePlayers.Add(player);
+        }
+        currentTarget = PlayerControlFixedUpdatePatch.setTarget(untargetablePlayers: untargetablePlayers);
+        PlayerControlFixedUpdatePatch.setPlayerOutline(currentTarget, Color.yellow);
     }
 
-    public static DateTime enabled = DateTime.FromBinary(638475264000000000);
+    public static DateTime enabled = new DateTime(DateTime.Today.Year, 4, 1);
     public static bool isEventDate => DateTime.Today.Date == enabled;
 
     public static bool canBeEnabled => DateTime.Today.Date >= enabled && DateTime.Today.Date <= enabled.AddDays(7); // One Week after the EVENT
@@ -35,8 +54,6 @@ public static class EventUtility {
 
     public static void meetingEndsUpdate() {
         if (!isEnabled) return;
-        // TODO - Implement Horse hats
-        // PlayerControl.LocalPlayer.RpcSetHat(CustomHatLoader.horseHatProductIds[rnd.Next(CustomHatLoader.horseHatProductIds.Count)]);
     }
 
 
@@ -49,7 +66,80 @@ public static class EventUtility {
     }
 
     public static void gameEndsUpdate() {
-        if (!isEnabled) return;
+        
+    }
+
+
+    public static PlayerControl currentTarget;
+    private static bool currentlyKicking;
+    private static int kickCounter = 0;
+
+    public static void kickTarget() {
+        // send rpc to kick target
+        MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.EventKick, Hazel.SendOption.Reliable, -1);
+        writer.Write(PlayerControl.LocalPlayer.PlayerId);
+        writer.Write(currentTarget.PlayerId);
+        System.Random rnd = new System.Random();
+        float kickDistance = 1 + (float)rnd.NextDouble() * 1.5f; // 1- 2.5
+        writer.Write(kickDistance);
+        AmongUsClient.Instance.FinishRpcImmediately(writer);
+        handleKick(PlayerControl.LocalPlayer, currentTarget, kickDistance);
+    }
+
+    public static void handleKick(PlayerControl source, PlayerControl target, float kickDistance) {
+        if (currentlyKicking || !isEnabled) return;
+        
+        kickCounter++;
+
+        // actual movement, canceled if meeting started
+        if (Mini.growingProgress() * 18 >= CustomOptionHolder.eventHeavyAge.getFloat() || kickCounter > CustomOptionHolder.eventKicksPerRound.getFloat()) {  // boing flip
+            target = source;
+            source = Mini.mini;
+        }
+
+        SoundEffectsManager.playAtPosition("fail" , target.GetTruePosition(), 3, 3);
+
+        if (target == PlayerControl.LocalPlayer) {
+            PlayerControl.LocalPlayer.moveable = false;
+            PlayerControl.LocalPlayer.NetTransform.Halt();
+        }
+        Vector2 direction = Vector3.Normalize(target.transform.position - source.transform.position);      
+        
+        
+        float kickDuration = 3f;
+        float speed = kickDistance / kickDuration;
+        Vector2 targetPosition = (Vector2)target.transform.position + direction * kickDistance;
+        Vector2 startPosition = target.transform.position;
+
+        HudManager.Instance.StartCoroutine(Effects.Lerp(kickDuration, new Action<float>((p) => {
+            float rotAngle = 360 * 4 * (1 - Mathf.Pow(1 - p, 4)) * (direction.x > 0 ? 1 : -1);
+            currentlyKicking = true;
+
+            if (MeetingHud.Instance) {
+                currentlyKicking = false;
+                rotAngle = 0f;
+            } else {
+                if (p == 1) {
+                    if (target == PlayerControl.LocalPlayer) {
+                        PlayerControl.LocalPlayer.moveable = true;
+                        PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(PlayerControl.LocalPlayer.transform.position);
+                    }
+                    rotAngle = 0f;
+                    currentlyKicking = false;
+                    target.NetTransform.Halt();
+                }
+                target.transform.SetLocalEulerAngles(new Vector3(0f, 0f, rotAngle), RotationOrder.OrderXYZ);
+
+                // move the player:
+                Vector3 targetStep = startPosition + (1 - Mathf.Pow(1 - p, 4)) * direction * kickDistance;
+                
+                if (!PhysicsHelpers.AnythingBetween(target.GetTruePosition(), target.GetTruePosition() + direction * 1f, Constants.ShipAndObjectsMask, false)) {
+                    target.transform.position = targetStep;
+                }
+            }
+        })));
+        
+
     }