diff --git a/README.md b/README.md index 1a893101f..949dce451 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein # Releases | Among Us - Version| Mod Version | Link | |----------|-------------|-----------------| +| 2021.5.10s| v2.6.3| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.6.3/TheOtherRoles.zip) | 2021.5.10s| v2.6.2| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.6.2/TheOtherRoles.zip) | 2021.4.14s| v2.6.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.6.1/TheOtherRoles.zip) | 2021.4.14s| v2.6.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.6.0/TheOtherRoles.zip) @@ -61,6 +62,10 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
Click to show the Changelog +**Version 2.6.3** +- Changed the role limits options to allow for minimum and maximum bounds +- Changed the role assignment to be more random when assigning roles (previously assigned the neutral roles before assigning the crewmate roles) + **Version 2.6.2** - The Other Roles now supports the new Among Us version **2021.5.10s** - Added a chat command to kick players as the host of a lobby (/kick playerName) @@ -376,7 +381,7 @@ First you need to choose how many special roles of each kind (Impostor/Neutral/C The count you set will only be reached, if there are enough Crewmates/Impostors in the game and if enough roles are set to be in the game (i.e. they are set to > 0%). The roles are then being distributed as follows: - First all roles that are set to 100% are being assigned to arbitrary players - After that each role that has 10%-90% selected adds 1-9 tickets to a ticket pool (there exists a ticket pool for Crewmates, Neutrals and Impostors). Then the roles will be selected randomly from the pools as long it's possible (until the selected number is reached, until there are no more Crewmates/Impostors or until there are no more tickets). If a role is selected from the pool, obviously all the tickets of that role are being removed. -- The Mafia, Lovers and Child are being selected independently (without using the ticket system) according to the spawn chance you selected. After that all Neutral roles are being selected, then all Crewmate roles and in the very end all Impostor roles. +- The Mafia, Lovers and Child are being selected independently (without using the ticket system) according to the spawn chance you selected. After that the Crewmate, Neutral and Impostor roles are selected and assigned in a random order. **Example:**\ Settings: 2 special Crewmate roles, Snitch: 100%, Hacker: 10%, Tracker: 30%\ diff --git a/Source Code/CredentialsPatch.cs b/Source Code/CredentialsPatch.cs index df950a2b8..a7e9e687f 100644 --- a/Source Code/CredentialsPatch.cs +++ b/Source Code/CredentialsPatch.cs @@ -11,39 +11,72 @@ namespace TheOtherRoles [HarmonyPatch] public static class CredentialsPatch { public static string fullCredentials = -$@"TheOtherRoles v{TheOtherRolesPlugin.Version.ToString()}: -- Modded by Eisbison, - Thunderstorm584 and EndOfFile -- Balanced with Dhalucard -- Button design by Bavari"; +$@"TheOtherRoles v{TheOtherRolesPlugin.Version.ToString()} +Modded by Eisbison, +Thunderstorm584 & EndOfFile +Balanced with Dhalucard +Button design by Bavari"; + + public static string mainMenuCredentials = +$@"Modded by Eisbison, Thunderstorm584 & EndOfFile +Balanced with Dhalucard Design by Bavari"; [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] private static class VersionShowerPatch { static void Postfix(VersionShower __instance) { - string spacer = new String('\n', 8); + var amongUsLogo = GameObject.Find("bannerLogo_AmongUs"); + if (amongUsLogo == null) return; + + var credentials = UnityEngine.Object.Instantiate(__instance.text); + credentials.transform.position = new Vector3(0, 0.1f, 0); + credentials.SetText(mainMenuCredentials); + credentials.alignment = TMPro.TextAlignmentOptions.Center; + credentials.fontSize *= 0.75f; + + var version = UnityEngine.Object.Instantiate(credentials); + version.transform.position = new Vector3(0, -0.25f, 0); + version.SetText($"v{TheOtherRolesPlugin.Version.ToString()}"); - if (__instance.text.text.Contains(spacer)) - __instance.text.text = __instance.text.text + "\n" + fullCredentials; - else - __instance.text.text = __instance.text.text + spacer + fullCredentials; - __instance.text.alignment = TMPro.TextAlignmentOptions.TopLeft; + credentials.transform.SetParent(amongUsLogo.transform); + version.transform.SetParent(amongUsLogo.transform); } } [HarmonyPatch(typeof(PingTracker), nameof(PingTracker.Update))] private static class PingTrackerPatch { - static void Postfix(PingTracker __instance) - { + static void Postfix(PingTracker __instance){ + __instance.text.alignment = TMPro.TextAlignmentOptions.TopRight; if (AmongUsClient.Instance.GameState == InnerNet.InnerNetClient.GameStates.Started) { - __instance.text.text = "TheOtherRoles\nModded by Eisbison\n" + __instance.text.text; - __instance.transform.localPosition = new Vector3(2.583f, 2.675f, __instance.transform.localPosition.z); + __instance.text.text = $"TheOtherRoles v{TheOtherRolesPlugin.Version.ToString()}\n" + __instance.text.text; + if (PlayerControl.LocalPlayer.Data.IsDead) { + __instance.transform.localPosition = new Vector3(3.45f, 2.675f, __instance.transform.localPosition.z); + } else { + __instance.transform.localPosition = new Vector3(4.2f, 2.675f, __instance.transform.localPosition.z); + } } else { __instance.text.text = $"{fullCredentials}\n{__instance.text.text}"; - __instance.transform.localPosition = new Vector3(1.25f, 2.675f, __instance.transform.localPosition.z); + __instance.transform.localPosition = new Vector3(3.5f, 2.675f, __instance.transform.localPosition.z); } } } + + [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start))] + private static class LogoPatch + { + static void Postfix(PingTracker __instance) { + var amongUsLogo = GameObject.Find("bannerLogo_AmongUs"); + if (amongUsLogo != null) { + amongUsLogo.transform.localScale *= 0.6f; + amongUsLogo.transform.position += Vector3.up * 0.25f; + } + + var torLogo = new GameObject("bannerLogo_TOR"); + torLogo.transform.position = Vector3.up; + var renderer = torLogo.AddComponent(); + renderer.sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.Banner.png", 300f); + } + } } } diff --git a/Source Code/CustomHats.cs b/Source Code/CustomHats.cs index 3a74d8d33..834532959 100644 --- a/Source Code/CustomHats.cs +++ b/Source Code/CustomHats.cs @@ -1,4 +1,3 @@ -extern alias Il2CppNewtonsoft; using System; using BepInEx; using BepInEx.Configuration; @@ -19,8 +18,8 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Security.Cryptography; -using Il2CppNewtonsoft::Newtonsoft.Json.Linq; -using Il2CppNewtonsoft::Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; namespace TheOtherRoles { [HarmonyPatch] @@ -29,11 +28,14 @@ public class CustomHats { public static Material hatShader; public static Dictionary CustomHatRegistry = new Dictionary(); + public static HatExtension TestExt = null; public class HatExtension { public string author { get; set;} public string package { get; set;} public string condition { get; set;} + public Sprite FlipImage { get; set;} + public Sprite BackFlipImage { get; set;} public bool isUnlocked() { if (condition == null || condition.ToLower() == "none") @@ -48,6 +50,8 @@ public class CustomHat { public string condition { get; set;} public string name { get; set;} public string resource { get; set;} + public string flipresource { get; set;} + public string backflipresource { get; set;} public string backresource { get; set;} public string climbresource { get; set;} public bool bounce { get; set;} @@ -58,6 +62,8 @@ public class CustomHat { private static List createCustomHatDetails(string[] hats, bool fromDisk = false) { Dictionary fronts = new Dictionary(); Dictionary backs = new Dictionary(); + Dictionary flips = new Dictionary(); + Dictionary backflips = new Dictionary(); Dictionary climbs = new Dictionary(); for (int i = 0; i < hats.Length; i++) { @@ -68,11 +74,15 @@ private static List createCustomHatDetails(string[] hats, bool fromDi for (int j = 1; j < p.Length; j++) options.Add(p[j]); - if (options.Contains("climb")) + if (options.Contains("back") && options.Contains("flip")) + backflips.Add(p[0], hats[i]); + else if (options.Contains("climb")) climbs.Add(p[0], hats[i]); - if (options.Contains("back")) + else if (options.Contains("back")) backs.Add(p[0], hats[i]); - if (!options.Contains("back") && !options.Contains("climb")) { + else if (options.Contains("flip")) + flips.Add(p[0], hats[i]); + else { CustomHat custom = new CustomHat { resource = hats[i] }; custom.name = p[0].Replace('-', ' '); custom.bounce = options.Contains("bounce"); @@ -87,13 +97,19 @@ private static List createCustomHatDetails(string[] hats, bool fromDi foreach (string k in fronts.Keys) { CustomHat hat = fronts[k]; - string br, cr; + string br, cr, fr, bfr; backs.TryGetValue(k, out br); climbs.TryGetValue(k, out cr); + flips.TryGetValue(k, out fr); + backflips.TryGetValue(k, out bfr); if (br != null) hat.backresource = br; if (cr != null) hat.climbresource = cr; + if (fr != null) + hat.flipresource = fr; + if (bfr != null) + hat.backflipresource = bfr; if (hat.backresource != null) hat.behind = true; @@ -136,12 +152,20 @@ private static HatBehaviour CreateHatBehaviour(CustomHat ch, bool fromDisk = fal if (ch.adaptive && hatShader != null) hat.AltShader = hatShader; - if (!testOnly) { - HatExtension extend = new HatExtension(); - extend.author = ch.author != null ? ch.author : "Unknown"; - extend.package = ch.package != null ? ch.package : "Misc."; - extend.condition = ch.condition != null ? ch.condition : "none"; + HatExtension extend = new HatExtension(); + extend.author = ch.author != null ? ch.author : "Unknown"; + extend.package = ch.package != null ? ch.package : "Misc."; + extend.condition = ch.condition != null ? ch.condition : "none"; + if (ch.flipresource != null) + extend.FlipImage = CreateHatSprite(ch.flipresource, fromDisk); + if (ch.backflipresource != null) + extend.BackFlipImage = CreateHatSprite(ch.backflipresource, fromDisk); + + if (testOnly) { + TestExt = extend; + TestExt.condition = hat.name; + } else { CustomHatRegistry.Add(hat.name, extend); } @@ -155,6 +179,10 @@ private static HatBehaviour CreateHatBehaviour(CustomHatLoader.CustomHatOnline c chd.backresource = filePath + chd.backresource; if (chd.climbresource != null) chd.climbresource = filePath + chd.climbresource; + if (chd.flipresource != null) + chd.flipresource = filePath + chd.flipresource; + if (chd.backflipresource != null) + chd.backflipresource = filePath + chd.backflipresource; return CreateHatBehaviour(chd, true); } @@ -188,6 +216,32 @@ where r.StartsWith(hatres) && r.EndsWith(".png") } } + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleAnimation))] + private static class PlayerPhysicsHandleAnimationPatch { + private static void Postfix(PlayerPhysics __instance) { + AnimationClip currentAnimation = __instance.Animator.GetCurrentAnimation(); + if (currentAnimation == __instance.ClimbAnim || currentAnimation == __instance.ClimbDownAnim) return; + HatParent hp = __instance.myPlayer.HatRenderer; + if (hp.Hat == null) return; + HatExtension extend = hp.Hat.getHatExtension(); + if (extend == null) return; + if (extend.FlipImage != null) { + if (__instance.rend.flipX) { + hp.FrontLayer.sprite = extend.FlipImage; + } else { + hp.FrontLayer.sprite = hp.Hat.MainImage; + } + } + if (extend.BackFlipImage != null) { + if (__instance.rend.flipX) { + hp.BackLayer.sprite = extend.BackFlipImage; + } else { + hp.BackLayer.sprite = hp.Hat.BackImage; + } + } + } + } + [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetHat), new System.Type[] { typeof(uint), typeof(int) })] private static class HatParentSetHatPatch { static void Postfix(HatParent __instance, [HarmonyArgument(0)]uint hatId, [HarmonyArgument(1)]int color) { @@ -315,7 +369,10 @@ public static bool Prefix(HatsTab __instance) { List> value = packages[key]; YOffset = createHatPackage(value, key, YOffset, __instance); } - __instance.scroller.YBounds.max = YOffset * -0.875f; // probably needs to fix up the entire messed math to solve this correctly + + // __instance.scroller.YBounds.max = -(__instance.YStart - (float)(unlockedHats.Length / this.NumPerRow) * this.YOffset) - 3f; + // __instance.scroller.YBounds.max = YOffset * -0.875f; // probably needs to fix up the entire messed math to solve this correctly + __instance.scroller.YBounds.max = -(YOffset + 4.1f); return false; } } @@ -399,6 +456,11 @@ public static async Task FetchHats() { info.reshashb = current["reshashb"]?.ToString(); info.climbresource = sanitizeResourcePath(current["climbresource"]?.ToString()); info.reshashc = current["reshashc"]?.ToString(); + info.flipresource = sanitizeResourcePath(current["flipresource"]?.ToString()); + info.reshashf = current["reshashf"]?.ToString(); + info.backflipresource = sanitizeResourcePath(current["backflipresource"]?.ToString()); + info.reshashbf = current["reshashbf"]?.ToString(); + info.author = current["author"]?.ToString(); info.package = current["package"]?.ToString(); info.condition = current["condition"]?.ToString(); @@ -420,6 +482,10 @@ public static async Task FetchHats() { markedfordownload.Add(data.backresource); if (data.climbresource != null && doesResourceRequireDownload(filePath + data.climbresource, data.reshashc, md5)) markedfordownload.Add(data.climbresource); + if (data.flipresource != null && doesResourceRequireDownload(filePath + data.flipresource, data.reshashf, md5)) + markedfordownload.Add(data.flipresource); + if (data.backflipresource != null && doesResourceRequireDownload(filePath + data.backflipresource, data.reshashbf, md5)) + markedfordownload.Add(data.backflipresource); } foreach(var file in markedfordownload) { @@ -455,11 +521,16 @@ public class CustomHatOnline : CustomHats.CustomHat { public string reshasha { get; set;} public string reshashb { get; set;} public string reshashc { get; set;} + public string reshashf { get; set;} + public string reshashbf { get; set;} } } public static class CustomHatExtensions { public static CustomHats.HatExtension getHatExtension(this HatBehaviour hat) { CustomHats.HatExtension ret = null; + if (CustomHats.TestExt != null && CustomHats.TestExt.condition.Equals(hat.name)) { + return CustomHats.TestExt; + } CustomHats.CustomHatRegistry.TryGetValue(hat.name, out ret); return ret; } diff --git a/Source Code/CustomOptions.cs b/Source Code/CustomOptions.cs index 9fa798601..1d26fd815 100644 --- a/Source Code/CustomOptions.cs +++ b/Source Code/CustomOptions.cs @@ -12,14 +12,15 @@ namespace TheOtherRoles { public class CustomOptionHolder { public static string[] rates = new string[]{"0%", "10%", "20%", "30%", "40%", "50%", "60%", "70%", "80%", "90%", "100%"}; - public static string[] crewmateAndNeutralRoleCaps = new string[]{"0", "0-1", "1", "1-2", "2", "2-3", "3", "3-4", "4", "4-5", "5", "5-6", "6", "6-7", "7", "7-8", "8", "8-9", "9", "9-10", "10", "10-11", "11", "11-12", "12", "12-13", "13", "13-14", "14", "14-15", "15"}; - public static string[] impostorRoleCaps = new string[]{"0", "0-1", "1", "1-2", "2", "2-3", "3"}; public static string[] presets = new string[]{"Preset 1", "Preset 2", "Preset 3", "Preset 4", "Preset 5"}; public static CustomOption presetSelection; - public static CustomOption crewmateRolesCount; - public static CustomOption neutralRolesCount; - public static CustomOption impostorRolesCount; + public static CustomOption crewmateRolesCountMin; + public static CustomOption crewmateRolesCountMax; + public static CustomOption neutralRolesCountMin; + public static CustomOption neutralRolesCountMax; + public static CustomOption impostorRolesCountMin; + public static CustomOption impostorRolesCountMax; public static CustomOption mafiaSpawnRate; public static CustomOption janitorCooldown; @@ -160,9 +161,13 @@ public static void Load() { // Role Options presetSelection = CustomOption.Create(0, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Preset"), presets, null, true); - crewmateRolesCount = CustomOption.Create(1, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Number Of Crewmate Roles"), crewmateAndNeutralRoleCaps, null, true); - neutralRolesCount = CustomOption.Create(7, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Number Of Neutral Roles"), crewmateAndNeutralRoleCaps); - impostorRolesCount = CustomOption.Create(2, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Number Of Impostor Roles"), impostorRoleCaps); + // Using new id's for the options to not break compatibilty with older versions + crewmateRolesCountMin = CustomOption.Create(300, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Crewmate Roles"), 0f, 0f, 15f, 1f, null, true); + crewmateRolesCountMax = CustomOption.Create(301, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Crewmate Roles"), 0f, 0f, 15f, 1f); + neutralRolesCountMin = CustomOption.Create(302, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Neutral Roles"), 0f, 0f, 15f, 1f); + neutralRolesCountMax = CustomOption.Create(303, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Neutral Roles"), 0f, 0f, 15f, 1f); + impostorRolesCountMin = CustomOption.Create(304, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Impostor Roles"), 0f, 0f, 3f, 1f); + impostorRolesCountMax = CustomOption.Create(305, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Impostor Roles"), 0f, 0f, 3f, 1f); mafiaSpawnRate = CustomOption.Create(10, cs(Janitor.color, "Mafia"), rates, null, true); janitorCooldown = CustomOption.Create(11, "Janitor Cooldown", 30f, 10f, 60f, 2.5f, mafiaSpawnRate); @@ -545,9 +550,37 @@ private static IEnumerable TargetMethods() { private static void Postfix(ref string __result) { StringBuilder sb = new StringBuilder(__result); - foreach (CustomOption option in CustomOption.options) - if (option.parent == null) - sb.AppendLine($"{option.name}: {option.selections[option.selection].ToString()}"); + foreach (CustomOption option in CustomOption.options) { + if (option.parent == null) { + if (option == CustomOptionHolder.crewmateRolesCountMin) { + var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Crewmate Roles"); + var min = CustomOptionHolder.crewmateRolesCountMin.getSelection(); + var max = CustomOptionHolder.crewmateRolesCountMax.getSelection(); + if (min > max) min = max; + var optionValue = (min == max) ? $"{max}" : $"{min} - {max}"; + sb.AppendLine($"{optionName}: {optionValue}"); + } else if (option == CustomOptionHolder.neutralRolesCountMin) { + var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Neutral Roles"); + var min = CustomOptionHolder.neutralRolesCountMin.getSelection(); + var max = CustomOptionHolder.neutralRolesCountMax.getSelection(); + if (min > max) min = max; + var optionValue = (min == max) ? $"{max}" : $"{min} - {max}"; + sb.AppendLine($"{optionName}: {optionValue}"); + } else if (option == CustomOptionHolder.impostorRolesCountMin) { + var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Impostor Roles"); + var min = CustomOptionHolder.impostorRolesCountMin.getSelection(); + var max = CustomOptionHolder.impostorRolesCountMax.getSelection(); + if (min > max) min = max; + var optionValue = (min == max) ? $"{max}" : $"{min} - {max}"; + sb.AppendLine($"{optionName}: {optionValue}"); + } else if ((option == CustomOptionHolder.crewmateRolesCountMax) || (option == CustomOptionHolder.neutralRolesCountMax) || (option == CustomOptionHolder.impostorRolesCountMax)) { + continue; + } else { + sb.AppendLine($"{option.name}: {option.selections[option.selection].ToString()}"); + } + + } + } CustomOption parent = null; foreach (CustomOption option in CustomOption.options) if (option.parent != null) { diff --git a/Source Code/Main.cs b/Source Code/Main.cs index 3420c3d88..0fe7b20ef 100644 --- a/Source Code/Main.cs +++ b/Source Code/Main.cs @@ -19,7 +19,7 @@ namespace TheOtherRoles public class TheOtherRolesPlugin : BasePlugin { public const string Id = "me.eisbison.theotherroles"; - public const string VersionString = "2.6.2"; + public const string VersionString = "2.6.3"; public static System.Version Version = System.Version.Parse(VersionString); public Harmony Harmony { get; } = new Harmony(Id); diff --git a/Source Code/ModUpdater.cs b/Source Code/ModUpdater.cs index 102ff0aed..bbb54e6a5 100644 --- a/Source Code/ModUpdater.cs +++ b/Source Code/ModUpdater.cs @@ -1,4 +1,3 @@ -extern alias Il2CppNewtonsoft; using System; using BepInEx; using BepInEx.Configuration; @@ -22,8 +21,8 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Security.Cryptography; -using Il2CppNewtonsoft::Newtonsoft.Json.Linq; -using Il2CppNewtonsoft::Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; using Twitch; namespace TheOtherRoles { diff --git a/Source Code/PlayerControlPatch.cs b/Source Code/PlayerControlPatch.cs index f0a736ffc..4aad0dd07 100644 --- a/Source Code/PlayerControlPatch.cs +++ b/Source Code/PlayerControlPatch.cs @@ -365,23 +365,32 @@ public static void updatePlayerInfo() { string roleNames = String.Join(", ", RoleInfo.getRoleInfoForPlayer(p).Select(x => Helpers.cs(x.color, x.name)).ToArray()); string taskInfo = tasksTotal > 0 ? $"({tasksCompleted}/{tasksTotal})" : ""; - string info = ""; + string playerInfoText = ""; + string meetingInfoText =""; if (p == PlayerControl.LocalPlayer) { - info = $"{roleNames}"; + playerInfoText = $"{roleNames}"; if (DestroyableSingleton.InstanceExists) { TMPro.TextMeshPro tabText = DestroyableSingleton.Instance.tab.transform.FindChild("TabText_TMP").GetComponent(); tabText.SetText($"Tasks {taskInfo}"); } - } else if (MapOptions.ghostsSeeRoles && MapOptions.ghostsSeeTasks) - info = $"{roleNames} {taskInfo}".Trim(); - else if (MapOptions.ghostsSeeTasks) - info = $"{taskInfo}".Trim(); - else if (MapOptions.ghostsSeeRoles) - info = $"{roleNames}"; - - playerInfo.text = info; + meetingInfoText = $"{roleNames} {taskInfo}".Trim(); + } + else if (MapOptions.ghostsSeeRoles && MapOptions.ghostsSeeTasks) { + playerInfoText = $"{roleNames} {taskInfo}".Trim(); + meetingInfoText = playerInfoText; + } + else if (MapOptions.ghostsSeeTasks) { + playerInfoText = $"{taskInfo}".Trim(); + meetingInfoText = playerInfoText; + } + else if (MapOptions.ghostsSeeRoles) { + playerInfoText = $"{roleNames}"; + meetingInfoText = playerInfoText; + } + + playerInfo.text = playerInfoText; playerInfo.gameObject.SetActive(p.Visible); - if (meetingInfo != null) meetingInfo.text = MeetingHud.Instance.state == MeetingHud.VoteStates.Results ? "" : info; + if (meetingInfo != null) meetingInfo.text = MeetingHud.Instance.state == MeetingHud.VoteStates.Results ? "" : meetingInfoText; } } diff --git a/Source Code/Resources/Banner.png b/Source Code/Resources/Banner.png new file mode 100644 index 000000000..73d63decd Binary files /dev/null and b/Source Code/Resources/Banner.png differ diff --git a/Source Code/RoleAssignmentPatch.cs b/Source Code/RoleAssignmentPatch.cs index 34a4dbb19..51aea5095 100644 --- a/Source Code/RoleAssignmentPatch.cs +++ b/Source Code/RoleAssignmentPatch.cs @@ -13,45 +13,52 @@ namespace TheOtherRoles class SetInfectedPatch { - private static void setRoleToRandomPlayer(byte roleId, List playerList) { - var index = rnd.Next(0, playerList.Count); - byte playerId = playerList[index].PlayerId; - playerList.RemoveAt(index); - - setRoleToPlayer(roleId, playerId); - } - - private static void setRoleToPlayer(byte roleId, byte playerId) { - MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetRole, Hazel.SendOption.Reliable, -1); - writer.Write(roleId); - writer.Write(playerId); - AmongUsClient.Instance.FinishRpcImmediately(writer); - RPCProcedure.setRole(roleId, playerId); - } - public static void Postfix([HarmonyArgument(0)]Il2CppReferenceArray infected) { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ResetVaribles, Hazel.SendOption.Reliable, -1); AmongUsClient.Instance.FinishRpcImmediately(writer); RPCProcedure.resetVariables(); + assignRoles(); + } + + private static void assignRoles() { + var data = getRoleAssignmentData(); + assignSpecialRoles(data); // Assign special roles like mafia and lovers first as they assign a role to multiple players and the chances are independent of the ticket system + assignEnsuredRoles(data); // Assign roles that should always be in the game next + assignChanceRoles(data); // Assign roles that may or may not be in the game last + } + + private static RoleAssignmentData getRoleAssignmentData() { + // Get the players that we want to assign the roles to. Crewmate and Neutral roles are assigned to natural crewmates. Impostor roles to impostors. List crewmates = PlayerControl.AllPlayerControls.ToArray().ToList().OrderBy(x => Guid.NewGuid()).ToList(); crewmates.RemoveAll(x => x.Data.IsImpostor); List impostors = PlayerControl.AllPlayerControls.ToArray().ToList().OrderBy(x => Guid.NewGuid()).ToList(); impostors.RemoveAll(x => !x.Data.IsImpostor); - float crewCountSettings = (float)CustomOptionHolder.crewmateRolesCount.getSelection() / 2; - float neutralCountSettings = (float)CustomOptionHolder.neutralRolesCount.getSelection() / 2; - float impCountSettings = (float)CustomOptionHolder.impostorRolesCount.getSelection() / 2; + var crewmateMin = CustomOptionHolder.crewmateRolesCountMin.getSelection(); + var crewmateMax = CustomOptionHolder.crewmateRolesCountMax.getSelection(); + var neutralMin = CustomOptionHolder.neutralRolesCountMin.getSelection(); + var neutralMax = CustomOptionHolder.neutralRolesCountMax.getSelection(); + var impostorMin = CustomOptionHolder.impostorRolesCountMin.getSelection(); + var impostorMax = CustomOptionHolder.impostorRolesCountMax.getSelection(); + + // Make sure min is less or equal to max + if (crewmateMin > crewmateMax) crewmateMin = crewmateMax; + if (neutralMin > neutralMax) neutralMin = neutralMax; + if (impostorMin > impostorMax) impostorMin = impostorMax; - if (crewCountSettings % 1 == 0.5f) crewCountSettings += 0.5f * (rnd.Next(2) * 2 - 1); - if (neutralCountSettings % 1 == 0.5f) neutralCountSettings += 0.5f * (rnd.Next(2) * 2 - 1); - if (impCountSettings % 1 == 0.5f) impCountSettings += 0.5f * (rnd.Next(2) * 2 - 1); + // Get the maximum allowed count of each role type based on the minimum and maximum option + int crewCountSettings = rnd.Next(crewmateMin, crewmateMax + 1); + int neutralCountSettings = rnd.Next(neutralMin, neutralMax + 1); + int impCountSettings = rnd.Next(impostorMin, impostorMax + 1); - int maxCrewmateRoles = Mathf.Min(crewmates.Count, Mathf.RoundToInt(crewCountSettings)); - int maxNeutralRoles = Mathf.Min(crewmates.Count, Mathf.RoundToInt(neutralCountSettings)); - int maxImpostorRoles = Mathf.Min(impostors.Count, Mathf.RoundToInt(impCountSettings)); + // Potentially lower the actual maximum to the assignable players + int maxCrewmateRoles = Mathf.Min(crewmates.Count, crewCountSettings); + int maxNeutralRoles = Mathf.Min(crewmates.Count, neutralCountSettings); + int maxImpostorRoles = Mathf.Min(impostors.Count, impCountSettings); + // Fill in the lists with the roles that should be assigned to players. Note that the special roles (like Mafia or Lovers) are NOT included in these lists Dictionary impSettings = new Dictionary(); Dictionary neutralSettings = new Dictionary(); Dictionary crewSettings = new Dictionary(); @@ -81,159 +88,190 @@ public static void Postfix([HarmonyArgument(0)]Il2CppReferenceArray 1) // Spy is useless with less than 2 Impostors + if (impostors.Count > 1) { + // Only add Spy if more than 1 impostor as the spy role is otherwise useless crewSettings.Add((byte)RoleId.Spy, CustomOptionHolder.spySpawnRate.getSelection()); + } crewSettings.Add((byte)RoleId.SecurityGuard, CustomOptionHolder.securityGuardSpawnRate.getSelection()); - // Set special roles - if (impostors.Count >= 3 && maxImpostorRoles >= 3 && (rnd.Next(1, 101) <= CustomOptionHolder.mafiaSpawnRate.getSelection() * 10)) { - setRoleToRandomPlayer((byte)RoleId.Godfather, impostors); - setRoleToRandomPlayer((byte)RoleId.Janitor, impostors); - setRoleToRandomPlayer((byte)RoleId.Mafioso, impostors); - maxImpostorRoles -= 3; + return new RoleAssignmentData { + crewmates = crewmates, + impostors = impostors, + crewSettings = crewSettings, + neutralSettings = neutralSettings, + impSettings = impSettings, + maxCrewmateRoles = maxCrewmateRoles, + maxNeutralRoles = maxNeutralRoles, + maxImpostorRoles = maxImpostorRoles + }; + } + + private static void assignSpecialRoles(RoleAssignmentData data) { + // Assign Mafia + if (data.impostors.Count >= 3 && data.maxImpostorRoles >= 3 && (rnd.Next(1, 101) <= CustomOptionHolder.mafiaSpawnRate.getSelection() * 10)) { + setRoleToRandomPlayer((byte)RoleId.Godfather, data.impostors); + setRoleToRandomPlayer((byte)RoleId.Janitor, data.impostors); + setRoleToRandomPlayer((byte)RoleId.Mafioso, data.impostors); + data.maxImpostorRoles -= 3; } + // Assign Lovers if (rnd.Next(1, 101) <= CustomOptionHolder.loversSpawnRate.getSelection() * 10) { - if (impostors.Count > 0 && crewmates.Count > 0 && maxCrewmateRoles > 0 && maxImpostorRoles > 0 && rnd.Next(1, 101) <= CustomOptionHolder.loversImpLoverRate.getSelection() * 10) { - setRoleToRandomPlayer((byte)RoleId.Lover1, impostors); - setRoleToRandomPlayer((byte)RoleId.Lover2, crewmates); - maxCrewmateRoles--; - maxImpostorRoles--; - } else if (crewmates.Count >= 2 && maxCrewmateRoles >= 2) { - setRoleToRandomPlayer((byte)RoleId.Lover1, crewmates); - setRoleToRandomPlayer((byte)RoleId.Lover2, crewmates); - maxCrewmateRoles -= 2; + if (data.impostors.Count > 0 && data.crewmates.Count > 0 && data.maxCrewmateRoles > 0 && data.maxImpostorRoles > 0 && rnd.Next(1, 101) <= CustomOptionHolder.loversImpLoverRate.getSelection() * 10) { + setRoleToRandomPlayer((byte)RoleId.Lover1, data.impostors); + setRoleToRandomPlayer((byte)RoleId.Lover2, data.crewmates); + data.maxCrewmateRoles--; + data.maxImpostorRoles--; + } else if (data.crewmates.Count >= 2 && data.maxCrewmateRoles >= 2) { + setRoleToRandomPlayer((byte)RoleId.Lover1, data.crewmates); + setRoleToRandomPlayer((byte)RoleId.Lover2, data.crewmates); + data.maxCrewmateRoles -= 2; } } + // Assign Child if (rnd.Next(1, 101) <= CustomOptionHolder.childSpawnRate.getSelection() * 10) { - if (impostors.Count > 0 && maxImpostorRoles > 0 && rnd.Next(1, 101) <= 33) { - setRoleToRandomPlayer((byte)RoleId.Child, impostors); - maxImpostorRoles--; - } else if (crewmates.Count > 0 && maxCrewmateRoles > 0) { - setRoleToRandomPlayer((byte)RoleId.Child, crewmates); - maxCrewmateRoles--; + if (data.impostors.Count > 0 && data.maxImpostorRoles > 0 && rnd.Next(1, 101) <= 33) { + setRoleToRandomPlayer((byte)RoleId.Child, data.impostors); + data.maxImpostorRoles--; + } else if (data.crewmates.Count > 0 && data.maxCrewmateRoles > 0) { + setRoleToRandomPlayer((byte)RoleId.Child, data.crewmates); + data.maxCrewmateRoles--; } } + } - // Set tickets and always active roles - - List crewTickets = new List(); - List neutralTickets = new List(); - List impTickets = new List(); - - for (int i = 0; i < neutralSettings.Count; i++) { - var entry = neutralSettings.ElementAt(i); - if (entry.Value == 0) { // Never - } else if (entry.Value == 10) { // Always - if (crewmates.Count > 0 && maxNeutralRoles > 0) { - setRoleToRandomPlayer(entry.Key, crewmates); - maxNeutralRoles--; - if (CustomOptionHolder.blockedRolePairings.ContainsKey(entry.Key)) { - foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[entry.Key]) { - if(impSettings.ContainsKey(blockedRoleId)) impSettings[blockedRoleId] = 0; - if (neutralSettings.ContainsKey(blockedRoleId)) neutralSettings[blockedRoleId] = 0; - if(crewSettings.ContainsKey(blockedRoleId)) crewSettings[blockedRoleId] = 0; - } - } - } - } else { // Other - for (int j = 0; j < entry.Value; j++) neutralTickets.Add(entry.Key); - } - } + private static void assignEnsuredRoles(RoleAssignmentData data) { + // Get all roles where the chance to occur is set to 100% + List ensuredCrewmateRoles = data.crewSettings.Where(x => x.Value == 10).Select(x => x.Key).ToList(); + List ensuredNeutralRoles = data.neutralSettings.Where(x => x.Value == 10).Select(x => x.Key).ToList(); + List ensuredImpostorRoles = data.impSettings.Where(x => x.Value == 10).Select(x => x.Key).ToList(); + + // Assign roles until we run out of either players we can assign roles to or run out of roles we can assign to players + while ( + (data.impostors.Count > 0 && data.maxImpostorRoles > 0 && ensuredImpostorRoles.Count > 0) || + (data.crewmates.Count > 0 && ( + (data.maxCrewmateRoles > 0 && ensuredCrewmateRoles.Count > 0) || + (data.maxNeutralRoles > 0 && ensuredNeutralRoles.Count > 0) + ))) { + + Dictionary> rolesToAssign = new Dictionary>(); + if (data.crewmates.Count > 0 && data.maxCrewmateRoles > 0 && ensuredCrewmateRoles.Count > 0) rolesToAssign.Add(RoleType.Crewmate, ensuredCrewmateRoles); + if (data.crewmates.Count > 0 && data.maxNeutralRoles > 0 && ensuredNeutralRoles.Count > 0) rolesToAssign.Add(RoleType.Neutral, ensuredNeutralRoles); + if (data.impostors.Count > 0 && data.maxImpostorRoles > 0 && ensuredImpostorRoles.Count > 0) rolesToAssign.Add(RoleType.Impostor, ensuredImpostorRoles); + + // Randomly select a pool of roles to assign a role from next (Crewmate role, Neutral role or Impostor role) + // then select one of the roles from the selected pool to a player + // and remove the role (and any potentially blocked role pairings) from the pool(s) + var roleType = rolesToAssign.Keys.ElementAt(rnd.Next(0, rolesToAssign.Keys.Count())); + var players = roleType == RoleType.Crewmate || roleType == RoleType.Neutral ? data.crewmates : data.impostors; + var index = rnd.Next(0, rolesToAssign[roleType].Count); + var roleId = rolesToAssign[roleType][index]; + setRoleToRandomPlayer(rolesToAssign[roleType][index], players); + rolesToAssign[roleType].RemoveAt(index); - for (int i = 0; i < crewSettings.Count; i++) { - var entry = crewSettings.ElementAt(i); - if (entry.Value == 0) { // Never - } else if (entry.Value == 10) { // Always - if (crewmates.Count > 0 && maxCrewmateRoles > 0) { - setRoleToRandomPlayer(entry.Key, crewmates); - maxCrewmateRoles--; - if(CustomOptionHolder.blockedRolePairings.ContainsKey(entry.Key)) { - foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[entry.Key]) { - if (impSettings.ContainsKey(blockedRoleId)) impSettings[blockedRoleId] = 0; - if (neutralSettings.ContainsKey(blockedRoleId)) neutralSettings[blockedRoleId] = 0; - if (crewSettings.ContainsKey(blockedRoleId)) crewSettings[blockedRoleId] = 0; - } + if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) { + foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) { + // Set chance for the blocked roles to 0 for chances less than 100% + if (data.impSettings.ContainsKey(blockedRoleId)) data.impSettings[blockedRoleId] = 0; + if (data.neutralSettings.ContainsKey(blockedRoleId)) data.neutralSettings[blockedRoleId] = 0; + if (data.crewSettings.ContainsKey(blockedRoleId)) data.crewSettings[blockedRoleId] = 0; + // Remove blocked roles even if the chance was 100% + foreach(var ensuredRolesList in rolesToAssign.Values) { + ensuredRolesList.RemoveAll(x => x == blockedRoleId); } } - } else { // Other - for (int j = 0; j < entry.Value; j++) crewTickets.Add(entry.Key); } - } - for (int i = 0; i < impSettings.Count; i++) { - var entry = impSettings.ElementAt(i); - if (entry.Value == 0) { // Never - } else if (entry.Value == 10) { // Always - if (impostors.Count > 0 && maxImpostorRoles > 0) { - setRoleToRandomPlayer(entry.Key, impostors); - if(CustomOptionHolder.blockedRolePairings.ContainsKey(entry.Key)) { - foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[entry.Key]) { - if (impSettings.ContainsKey(blockedRoleId)) impSettings[blockedRoleId] = 0; - if (neutralSettings.ContainsKey(blockedRoleId)) neutralSettings[blockedRoleId] = 0; - if (crewSettings.ContainsKey(blockedRoleId)) crewSettings[blockedRoleId] = 0; - } - } - maxImpostorRoles--; - } - } else { // Other - for (int j = 0; j < entry.Value; j++) impTickets.Add(entry.Key); + // Adjust the role limit + switch (roleType) { + case RoleType.Crewmate: data.maxCrewmateRoles--; break; + case RoleType.Neutral: data.maxNeutralRoles--;break; + case RoleType.Impostor: data.maxImpostorRoles--;break; } } + } - // Set solo player roles + + private static void assignChanceRoles(RoleAssignmentData data) { + // Get all roles where the chance to occur is set grater than 0% but not 100% and build a ticket pool based on their weight + List crewmateTickets = data.crewSettings.Where(x => x.Value > 0 && x.Value < 10).Select(x => Enumerable.Repeat(x.Key, x.Value)).SelectMany(x => x).ToList(); + List neutralTickets = data.neutralSettings.Where(x => x.Value > 0 && x.Value < 10).Select(x => Enumerable.Repeat(x.Key, x.Value)).SelectMany(x => x).ToList(); + List impostorTickets = data.impSettings.Where(x => x.Value > 0 && x.Value < 10).Select(x => Enumerable.Repeat(x.Key, x.Value)).SelectMany(x => x).ToList(); - for (int i = 0; i < maxNeutralRoles; i++) { - if (neutralTickets.Count > 0 && crewmates.Count > 0) { - var index = rnd.Next(0, neutralTickets.Count); - byte roleId = neutralTickets[index]; - neutralTickets.RemoveAll(x => x == roleId); - setRoleToRandomPlayer(roleId, crewmates); + // Assign roles until we run out of either players we can assign roles to or run out of roles we can assign to players + while ( + (data.impostors.Count > 0 && data.maxImpostorRoles > 0 && impostorTickets.Count > 0) || + (data.crewmates.Count > 0 && ( + (data.maxCrewmateRoles > 0 && crewmateTickets.Count > 0) || + (data.maxNeutralRoles > 0 && neutralTickets.Count > 0) + ))) { + + Dictionary> rolesToAssign = new Dictionary>(); + if (data.crewmates.Count > 0 && data.maxCrewmateRoles > 0 && crewmateTickets.Count > 0) rolesToAssign.Add(RoleType.Crewmate, crewmateTickets); + if (data.crewmates.Count > 0 && data.maxNeutralRoles > 0 && neutralTickets.Count > 0) rolesToAssign.Add(RoleType.Neutral, neutralTickets); + if (data.impostors.Count > 0 && data.maxImpostorRoles > 0 && impostorTickets.Count > 0) rolesToAssign.Add(RoleType.Impostor, impostorTickets); + + // Randomly select a pool of role tickets to assign a role from next (Crewmate role, Neutral role or Impostor role) + // then select one of the roles from the selected pool to a player + // and remove all tickets of this role (and any potentially blocked role pairings) from the pool(s) + var roleType = rolesToAssign.Keys.ElementAt(rnd.Next(0, rolesToAssign.Keys.Count())); + var players = roleType == RoleType.Crewmate || roleType == RoleType.Neutral ? data.crewmates : data.impostors; + var index = rnd.Next(0, rolesToAssign[roleType].Count); + var roleId = rolesToAssign[roleType][index]; + setRoleToRandomPlayer(rolesToAssign[roleType][index], players); + rolesToAssign[roleType].RemoveAll(x => x == roleId); - if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) { - foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) { - crewTickets.RemoveAll(x => x == blockedRoleId); - neutralTickets.RemoveAll(x => x == blockedRoleId); - impTickets.RemoveAll(x => x == blockedRoleId); - } + if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) { + foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) { + // Remove tickets of blocked roles from all pools + crewmateTickets.RemoveAll(x => x == blockedRoleId); + neutralTickets.RemoveAll(x => x == blockedRoleId); + impostorTickets.RemoveAll(x => x == blockedRoleId); } } - } - for (int i = 0; i < maxCrewmateRoles; i++) { - if (crewTickets.Count > 0 && crewmates.Count > 0) { - var index = rnd.Next(0, crewTickets.Count); - byte roleId = crewTickets[index]; - crewTickets.RemoveAll(x => x == roleId); - setRoleToRandomPlayer(roleId, crewmates); - - if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) { - foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) { - crewTickets.RemoveAll(x => x == blockedRoleId); - neutralTickets.RemoveAll(x => x == blockedRoleId); - impTickets.RemoveAll(x => x == blockedRoleId); - } - } + // Adjust the role limit + switch (roleType) { + case RoleType.Crewmate: data.maxCrewmateRoles--; break; + case RoleType.Neutral: data.maxNeutralRoles--;break; + case RoleType.Impostor: data.maxImpostorRoles--;break; } } + } - for (int i = 0; i < maxImpostorRoles; i++) { - if (impTickets.Count > 0 && impostors.Count > 0) { - var index = rnd.Next(0, impTickets.Count); - byte roleId = impTickets[index]; - impTickets.RemoveAll(x => x == roleId); - setRoleToRandomPlayer(roleId, impostors); - - if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) { - foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) { - crewTickets.RemoveAll(x => x == blockedRoleId); - neutralTickets.RemoveAll(x => x == blockedRoleId); - impTickets.RemoveAll(x => x == blockedRoleId); - } - } - } - } + private static void setRoleToRandomPlayer(byte roleId, List playerList) { + var index = rnd.Next(0, playerList.Count); + byte playerId = playerList[index].PlayerId; + playerList.RemoveAt(index); + + setRoleToPlayer(roleId, playerId); } + + private static void setRoleToPlayer(byte roleId, byte playerId) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetRole, Hazel.SendOption.Reliable, -1); + writer.Write(roleId); + writer.Write(playerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.setRole(roleId, playerId); + } + + private class RoleAssignmentData { + public List crewmates {get;set;} + public List impostors {get;set;} + public Dictionary impSettings = new Dictionary(); + public Dictionary neutralSettings = new Dictionary(); + public Dictionary crewSettings = new Dictionary(); + public int maxCrewmateRoles {get;set;} + public int maxNeutralRoles {get;set;} + public int maxImpostorRoles {get;set;} + } + + private enum RoleType { + Crewmate = 0, + Neutral = 1, + Impostor = 2 + } + } } diff --git a/Source Code/TheOtherRoles.csproj b/Source Code/TheOtherRoles.csproj index 0e55cd61c..110f4a915 100644 --- a/Source Code/TheOtherRoles.csproj +++ b/Source Code/TheOtherRoles.csproj @@ -1,7 +1,7 @@ netstandard2.1 - 2.6.2 + 2.6.3 TheOtherRoles Eisbison @@ -27,15 +27,4 @@ - - - - - $(AmongUs)\BepInEx\unhollowed\Newtonsoft.Json.dll - Il2CppNewtonsoft - - - - $(NoWarn);MSB3243 - \ No newline at end of file