From: Eisbison Date: Fri, 26 Nov 2021 17:10:19 +0000 (+0100) Subject: v3.2.0 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=844b758d8437cbe678b9bc925abb7f49d2ce69b1;p=rhonda%2Ftheotherroles.git v3.2.0 --- diff --git a/Images/TOR_roles.jpg b/Images/TOR_roles.jpg index 69babf8..c854825 100644 Binary files a/Images/TOR_roles.jpg and b/Images/TOR_roles.jpg differ diff --git a/README.md b/README.md index f6dd0f4..716c07e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Even more roles are coming soon :) | [Bounty Hunter](#bounty-hunter) | [Detective](#detective) | [Jackal](#jackal) | | | [Camouflager](#camouflager) | [Engineer](#engineer) | [Sidekick](#sidekick) | | | [Cleaner](#cleaner) | [Hacker](#hacker) | [Vulture](#vulture) | | -| [Eraser](#eraser) | [Lighter](#lighter) | | | +| [Eraser](#eraser) | [Lighter](#lighter) | [Lawyer](#lawyer) | | | [Godfather (Mafia)](#mafia) | [Mayor](#mayor) | | | | [Mafioso (Mafia)](#mafia) | [Medic](#medic) | | | | [Janitor (Mafia)](#mafia) | [Security Guard](#security-guard) | | | @@ -26,7 +26,7 @@ Even more roles are coming soon :) | [Trickster](#trickster) | [Sheriff](#sheriff) | | | | [Vampire](#vampire) | [Shifter](#shifter) | | | | [Warlock](#warlock) | [Snitch](#snitch) | | | -| | [Spy](#spy) | | | +| [Witch](#witch) | [Spy](#spy) | | | | | [Swapper](#swapper) | | | | | [Time Master](#time-master) | | | | | [Tracker](#tracker) | | | @@ -38,6 +38,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein # Releases | Among Us - Version| Mod Version | Link | |----------|-------------|-----------------| +| 2021.11.9.5s| v3.2.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v3.2.0/TheOtherRoles.zip) | 2021.11.9.5s| v3.1.2| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v3.1.1/TheOtherRoles.zip) | 2021.11.9.5s| v3.1.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v3.1.1/TheOtherRoles.zip) | 2021.11.9.5s| v3.1.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v3.1.0/TheOtherRoles.zip) @@ -83,6 +84,14 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
Click to show the Changelog +**Version 3.2.0** +- **New Role:** [Witch](#witch) created by [Alex2911](https://github.com/Alex2911) +- **New Role:** [Lawyer](#lawyer) +- Bug fix: Choosing an Impostor as a Sidekick won't resulted in an Impostor/Sidekick mix anymore. +- Bug fix: The Guesser info now shows the right information, when the Guesser guesses the wrong role and kills himself. +- Bug fix: Hats are being displayed in alphabetic order. Hats demo in freeplay is working again. Fixed a bug where hats would not load when accessed from the main menu. +- Bug fix: The Detective now shows the name of the players in any case. + **Hotfix 3.1.2** - Don't ask, just update. I messed up. @@ -736,6 +745,27 @@ Depending on the options, there'll be an arrow pointing towards the current targ | Bounty Hunter Arrow Update Interval | Sets how often the position is being updated ----------------------- +## Witch +### **Team: Impostors** +The Witch is an Impostor who has the ability to cast a spell on other players.\ +During the next meeting, the spellbound player will be highlighted and he'll die right after the meeting.\ +There are multiple options listed down below with which you can configure to fit your taste.\ +\ +**NOTE:** +- Similar to the Vampire, shields and blanks will be checked twice (at the end of casting the spell on the player and at the end of the meeting, when the spell will be activated). This can result in players being marked as spelled during the meeting, but not dying in the end (when they get a shield or the Witch gets blanked after they were spelled by the Witch). + +\ +### Game Options +| Name | Description | +|----------|:-------------:| +| Witch Spawn Chance | - +| Witch Spell Casting Cooldown | - +| Witch Additional Cooldown | The spell casting cooldown will be increased by the amount you set here after each spell +| Witch Can Spell Everyone | If set to false, the witch can't spell the Spy and other Impostors +| Witch Spell Casting Duration | The time that you need to stay next to the target in order to cast a spell on it +| Trigger Both Cooldowns | If set to true, casting a spell will also trigger cooldown of the kill button and vice versa (but the two cooldowns may vary) +----------------------- + ## Guesser ### **Team: Crewmates or Impostors** @@ -1248,6 +1278,40 @@ Depending on the options, when a player dies, the Vulture gets an arrow pointing | Show Arrows Pointing Towards The Corpes | - ----------------------- +## Lawyer +### **Team: Neutral** +The Lawyer is a neutral role that has a client. +The client might be an Impostor, Jackal or Sidekick which is no Lover. +The Lawyer needs to keep his client alive in order to win as a Lawyer. +If his client dies or gets voted out, the Lawyer gets changes his role and becomes the [Pursuer](#pursuer), which has a different goal to win the game. +The main goal of the Lawyer is to win as Lawyer, he is not allowed to betray his client. + +The Lawyer can win in multiple ways: +- Lawyer dead and client alive: The Lawyer wins together with the winning team because he kept his client alive +- Lawyer alive and client alive: The Lawyer wins with the winning team because he kept his client alive. The client **doesn't** win (even if his Impostor/Team Jackal mate wins). Hence the client should keep the Lawyer alive for some time, to get some help during the meetings, but has to eliminate him soon enough to not get his win stolen. + +**NOTE:** +- If the client disconnects, the Lawyer will also turn into the Pursuer +- If "Lawyer Target Knows" is set to true, the client will know that he someone is his Lawyer, but he won't know who. + +### Game Options +| Name | Description | +|----------|:-------------:| +| Lawyer Target Knows | The target knows that it is the target (marked with "§", if lawyer dies the mark will disappear) +| Lawyer Wins After Meetings | If set to true, the Lawyer wins after a configurable amount of meetings (can`t start meetings himself) +| Lawyer Needed Meetings To Win | - +| Lawyer Vision | Pursuer has normal vision +| Pursuer Blank Cooldwon | - +| Pursuer Number Of Blanks | - +----------------------- + +## Pursuer +### **Team: Neutral** +The Pursuer is still a neutral role, but has a different goal to win the game: He has to be alive, when the game ends (no matter who caused the win). +In order to achieve this goal, the Pursuer has the ability "Blank": He can fill a killers (this also includes the Sheriff) weapon with a blank, so if the killer attempts to kill someone, the killer will miss the target and all cooldowns will be triggered as usual. +If the killer blanks, no shields (e.g. Medic shield or Time Master shield) will be triggered. +The Pursuer has tasks (which can already be done while being a Lawyer), that count towards the task win for the Crewmates. If the Pursuer dies, his tasks won't be counted anymore. + # Source code It's bad I know, this is a side project and my second week of modding. So there are no best practices around here. You can use parts of the code but don't copy paste the whole thing. Make sure you give credits to the other developers, because some parts of the code are based on theirs. diff --git a/TheOtherRoles/Buttons.cs b/TheOtherRoles/Buttons.cs index f471fe7..828ba05 100644 --- a/TheOtherRoles/Buttons.cs +++ b/TheOtherRoles/Buttons.cs @@ -36,7 +36,11 @@ namespace TheOtherRoles public static CustomButton arsonistButton; public static CustomButton vultureEatButton; public static CustomButton mediumButton; + public static CustomButton pursuerButton; + public static CustomButton witchSpellButton; + public static TMPro.TMP_Text securityGuardButtonScrewsText; + public static TMPro.TMP_Text pursuerButtonBlanksText; public static void setCustomButtonCooldowns() { engineerRepairButton.MaxTimer = 0f; @@ -64,7 +68,9 @@ namespace TheOtherRoles arsonistButton.MaxTimer = Arsonist.cooldown; vultureEatButton.MaxTimer = Vulture.cooldown; mediumButton.MaxTimer = Medium.cooldown; + pursuerButton.MaxTimer = Pursuer.cooldown; trackerTrackCorpsesButton.MaxTimer = Tracker.corpsesTrackingCooldown; + witchSpellButton.MaxTimer = Witch.cooldown; timeMasterShieldButton.EffectDuration = TimeMaster.shieldDuration; hackerButton.EffectDuration = Hacker.duration; @@ -76,6 +82,7 @@ namespace TheOtherRoles arsonistButton.EffectDuration = Arsonist.duration; mediumButton.EffectDuration = Medium.duration; trackerTrackCorpsesButton.EffectDuration = Tracker.corpsesTrackingDuration; + witchSpellButton.EffectDuration = Witch.spellCastingDuration; // Already set the timer to the max, as the button is enabled during the game and not available at the start lightsOutButton.Timer = lightsOutButton.MaxTimer; } @@ -172,32 +179,29 @@ namespace TheOtherRoles // Sheriff Kill sheriffKillButton = new CustomButton( () => { - // Not using Helpers.checkMuderAttemptAndKill here, since the Sheriff can kill himself eventhough he has a shield, ... - if (Medic.shielded != null && Medic.shielded == Sheriff.currentTarget) { - MessageWriter attemptWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ShieldedMurderAttempt, Hazel.SendOption.Reliable, -1); - AmongUsClient.Instance.FinishRpcImmediately(attemptWriter); - RPCProcedure.shieldedMurderAttempt(); - return; - } + MurderAttemptResult murderAttemptResult = Helpers.checkMuderAttempt(Sheriff.sheriff, Sheriff.currentTarget); + if (murderAttemptResult == MurderAttemptResult.SuppressKill) return; - byte targetId = 0; - if ((Sheriff.currentTarget.Data.Role.IsImpostor && (Sheriff.currentTarget != Mini.mini || Mini.isGrownUp())) || - (Sheriff.spyCanDieToSheriff && Spy.spy == Sheriff.currentTarget) || - (Sheriff.canKillNeutrals && (Arsonist.arsonist == Sheriff.currentTarget || Jester.jester == Sheriff.currentTarget || Vulture.vulture == Sheriff.currentTarget)) || - (Jackal.jackal == Sheriff.currentTarget || Sidekick.sidekick == Sheriff.currentTarget)) { - targetId = Sheriff.currentTarget.PlayerId; - } - else { - targetId = PlayerControl.LocalPlayer.PlayerId; - } + if (murderAttemptResult == MurderAttemptResult.PerformKill) { + byte targetId = 0; + if ((Sheriff.currentTarget.Data.Role.IsImpostor && (Sheriff.currentTarget != Mini.mini || Mini.isGrownUp())) || + (Sheriff.spyCanDieToSheriff && Spy.spy == Sheriff.currentTarget) || + (Sheriff.canKillNeutrals && (Arsonist.arsonist == Sheriff.currentTarget || Jester.jester == Sheriff.currentTarget || Vulture.vulture == Sheriff.currentTarget || Lawyer.lawyer == Sheriff.currentTarget || Pursuer.pursuer == Sheriff.currentTarget)) || + (Jackal.jackal == Sheriff.currentTarget || Sidekick.sidekick == Sheriff.currentTarget)) { + targetId = Sheriff.currentTarget.PlayerId; + } + else { + targetId = PlayerControl.LocalPlayer.PlayerId; + } - MessageWriter killWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1); - killWriter.Write(Sheriff.sheriff.Data.PlayerId); - killWriter.Write(targetId); - AmongUsClient.Instance.FinishRpcImmediately(killWriter); - RPCProcedure.uncheckedMurderPlayer(Sheriff.sheriff.Data.PlayerId, targetId); + MessageWriter killWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1); + killWriter.Write(Sheriff.sheriff.Data.PlayerId); + killWriter.Write(targetId); + AmongUsClient.Instance.FinishRpcImmediately(killWriter); + RPCProcedure.uncheckedMurderPlayer(Sheriff.sheriff.Data.PlayerId, targetId); + } - sheriffKillButton.Timer = sheriffKillButton.MaxTimer; + sheriffKillButton.Timer = sheriffKillButton.MaxTimer; Sheriff.currentTarget = null; }, () => { return Sheriff.sheriff != null && Sheriff.sheriff == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; }, @@ -396,7 +400,8 @@ namespace TheOtherRoles vampireKillButton = new CustomButton( () => { - if (Helpers.checkMuderAttempt(Vampire.vampire, Vampire.currentTarget)) { + MurderAttemptResult murder = Helpers.checkMuderAttempt(Vampire.vampire, Vampire.currentTarget); + if (murder == MurderAttemptResult.PerformKill) { if (Vampire.targetNearGarlic) { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1); writer.Write(Vampire.vampire.PlayerId); @@ -429,8 +434,11 @@ namespace TheOtherRoles vampireKillButton.HasEffect = true; // Trigger effect on this click } + } else if (murder == MurderAttemptResult.BlankKill) { + vampireKillButton.Timer = vampireKillButton.MaxTimer; + vampireKillButton.HasEffect = false; } else { - vampireKillButton.HasEffect = false; // Block effect if no action was fired + vampireKillButton.HasEffect = false; } }, () => { return Vampire.vampire != null && Vampire.vampire == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; }, @@ -505,7 +513,7 @@ namespace TheOtherRoles // Jackal Kill jackalKillButton = new CustomButton( () => { - if (!Helpers.checkMuderAttemptAndKill(Jackal.jackal, Jackal.currentTarget)) return; + if (Helpers.checkMuderAttemptAndKill(Jackal.jackal, Jackal.currentTarget) == MurderAttemptResult.SuppressKill) return; jackalKillButton.Timer = jackalKillButton.MaxTimer; Jackal.currentTarget = null; @@ -522,8 +530,7 @@ namespace TheOtherRoles // Sidekick Kill sidekickKillButton = new CustomButton( () => { - if (!Helpers.checkMuderAttemptAndKill(Sidekick.sidekick, Sidekick.currentTarget)) return; - + if (Helpers.checkMuderAttemptAndKill(Sidekick.sidekick, Sidekick.currentTarget) == MurderAttemptResult.SuppressKill) return; sidekickKillButton.Timer = sidekickKillButton.MaxTimer; Sidekick.currentTarget = null; }, @@ -666,20 +673,20 @@ namespace TheOtherRoles Warlock.curseVictim = Warlock.currentTarget; warlockCurseButton.Sprite = Warlock.getCurseKillButtonSprite(); warlockCurseButton.Timer = 1f; - } else if (Warlock.curseVictim != null && Warlock.curseVictimTarget != null && Helpers.checkMuderAttempt(Warlock.warlock, Warlock.curseVictimTarget)) { + } else if (Warlock.curseVictim != null && Warlock.curseVictimTarget != null) { + MurderAttemptResult murder = Helpers.checkMuderAttempt(Warlock.warlock, Warlock.curseVictimTarget); + if (murder == MurderAttemptResult.SuppressKill) return; + // Curse Kill // Not using Helpers.checkMuderAttemptAndKill here directly, since we need to share the Warlock.curseKillTarget - Warlock.curseKillTarget = Warlock.curseVictimTarget; - MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.WarlockCurseKill, Hazel.SendOption.Reliable, -1); - writer.Write(Warlock.curseKillTarget.PlayerId); - AmongUsClient.Instance.FinishRpcImmediately(writer); - RPCProcedure.warlockCurseKill(Warlock.curseKillTarget.PlayerId); + if (murder == MurderAttemptResult.PerformKill) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.WarlockCurseKill, Hazel.SendOption.Reliable, -1); + writer.Write(Warlock.curseKillTarget.PlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.warlockCurseKill(Warlock.curseVictimTarget.PlayerId); - Warlock.curseVictim = null; - Warlock.curseVictimTarget = null; - warlockCurseButton.Sprite = Warlock.getCurseButtonSprite(); - Warlock.warlock.killTimer = warlockCurseButton.Timer = warlockCurseButton.MaxTimer; + } if(Warlock.rootTime > 0) { PlayerControl.LocalPlayer.moveable = false; @@ -690,6 +697,12 @@ namespace TheOtherRoles } }))); } + + Warlock.curseVictim = null; + Warlock.curseVictimTarget = null; + warlockCurseButton.Sprite = Warlock.getCurseButtonSprite(); + Warlock.warlock.killTimer = warlockCurseButton.Timer = warlockCurseButton.MaxTimer; + } }, () => { return Warlock.warlock != null && Warlock.warlock == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; }, @@ -923,6 +936,91 @@ namespace TheOtherRoles } ); + // Pursuer button + pursuerButton = new CustomButton( + () => { + if (Pursuer.target != null) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetBlanked, Hazel.SendOption.Reliable, -1); + writer.Write(Pursuer.target.PlayerId); + writer.Write(Byte.MaxValue); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.setBlanked(Pursuer.target.PlayerId, Byte.MaxValue); + + Pursuer.target = null; + + Pursuer.blanks++; + pursuerButton.Timer = pursuerButton.MaxTimer; + } + + }, + () => { return Pursuer.pursuer != null && Pursuer.pursuer == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead && Pursuer.blanks < Pursuer.blanksNumber; }, + () => { + if (pursuerButtonBlanksText != null) pursuerButtonBlanksText.text = $"{Pursuer.blanksNumber - Pursuer.blanks}"; + + return Pursuer.blanksNumber > Pursuer.blanks && PlayerControl.LocalPlayer.CanMove && Pursuer.target != null; + }, + () => { pursuerButton.Timer = pursuerButton.MaxTimer; }, + Pursuer.getTargetSprite(), + new Vector3(-1.8f, -0.06f, 0), + __instance, + KeyCode.F + ); + + // Pursuer button blanks left + pursuerButtonBlanksText = GameObject.Instantiate(pursuerButton.actionButton.cooldownTimerText, pursuerButton.actionButton.cooldownTimerText.transform.parent); + pursuerButtonBlanksText.text = ""; + pursuerButtonBlanksText.enableWordWrapping = false; + pursuerButtonBlanksText.transform.localScale = Vector3.one * 0.5f; + pursuerButtonBlanksText.transform.localPosition += new Vector3(-0.05f, 0.7f, 0); + + + // Witch Spell button + witchSpellButton = new CustomButton( + () => { + if (Witch.currentTarget != null) { + Witch.spellCastingTarget = Witch.currentTarget; + } + }, + () => { return Witch.witch != null && Witch.witch == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; }, + () => { + if (witchSpellButton.isEffectActive && Witch.spellCastingTarget != Witch.currentTarget) { + Witch.spellCastingTarget = null; + witchSpellButton.Timer = 0f; + witchSpellButton.isEffectActive = false; + } + return PlayerControl.LocalPlayer.CanMove && Witch.currentTarget != null; + }, + () => { + witchSpellButton.Timer = witchSpellButton.MaxTimer; + witchSpellButton.isEffectActive = false; + Witch.spellCastingTarget = null; + }, + Witch.getButtonSprite(), + new Vector3(-1.8f, -0.06f, 0), + __instance, + KeyCode.F, + true, + Witch.spellCastingDuration, + () => { + if (Witch.spellCastingTarget == null) return; + MurderAttemptResult attempt = Helpers.checkMuderAttempt(Witch.witch, Witch.spellCastingTarget); + if (attempt == MurderAttemptResult.PerformKill) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetFutureSpelled, Hazel.SendOption.Reliable, -1); + writer.Write(Witch.currentTarget.PlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.setFutureSpelled(Witch.currentTarget.PlayerId); + } + if (attempt == MurderAttemptResult.BlankKill || attempt == MurderAttemptResult.PerformKill) { + witchSpellButton.MaxTimer += Witch.cooldownAddition; + witchSpellButton.Timer = witchSpellButton.MaxTimer; + if (Witch.triggerBothCooldowns) + Witch.witch.killTimer = PlayerControl.GameOptions.KillCooldown; + } else { + witchSpellButton.Timer = 0f; + } + Witch.spellCastingTarget = null; + } + ); // Set the default (or settings from the previous game) timers/durations when spawning the buttons setCustomButtonCooldowns(); diff --git a/TheOtherRoles/CustomOptionHolder.cs b/TheOtherRoles/CustomOptionHolder.cs index 0e08a33..a4c6643 100644 --- a/TheOtherRoles/CustomOptionHolder.cs +++ b/TheOtherRoles/CustomOptionHolder.cs @@ -84,6 +84,13 @@ namespace TheOtherRoles { public static CustomOption bountyHunterShowArrow; public static CustomOption bountyHunterArrowUpdateIntervall; + public static CustomOption witchSpawnRate; + public static CustomOption witchCooldown; + public static CustomOption witchAdditionalCooldown; + public static CustomOption witchCanSpellAnyone; + public static CustomOption witchSpellCastingDuration; + public static CustomOption witchTriggerBothCooldowns; + public static CustomOption shifterSpawnRate; public static CustomOption shifterShiftsModifiers; @@ -187,6 +194,14 @@ namespace TheOtherRoles { public static CustomOption mediumDuration; public static CustomOption mediumOneTimeUse; + public static CustomOption lawyerSpawnRate; + public static CustomOption lawyerTargetKnows; + public static CustomOption lawyerWinsAfterMeetings; + public static CustomOption lawyerNeededMeetings; + public static CustomOption lawyerVision; + public static CustomOption pursuerCooldown; + public static CustomOption pursuerBlanksNumber; + public static CustomOption maxNumberOfMeetings; public static CustomOption blockSkippingInEmergencyMeetings; public static CustomOption noVoteIsSelfVote; @@ -258,6 +273,13 @@ namespace TheOtherRoles { bountyHunterShowArrow = CustomOption.Create(324, "Show Arrow Pointing Towards The Bounty", true, bountyHunterSpawnRate); bountyHunterArrowUpdateIntervall = CustomOption.Create(325, "Arrow Update Intervall", 15f, 2.5f, 60f, 2.5f, bountyHunterShowArrow); + witchSpawnRate = CustomOption.Create(350, cs(Witch.color, "Witch"), rates, null, true); + witchCooldown = CustomOption.Create(351, "Witch Spell Casting Cooldown", 30f, 10f, 120f, 5f, witchSpawnRate); + witchAdditionalCooldown = CustomOption.Create(352, "Witch Additional Cooldown", 10f, 0f, 60f, 5f, witchSpawnRate); + witchCanSpellAnyone = CustomOption.Create(353, "Witch Can Spell Anyone", false, witchSpawnRate); + witchSpellCastingDuration = CustomOption.Create(354, "Spell Casting Duration", 1f, 0f, 10f, 1f, witchSpawnRate); + witchTriggerBothCooldowns = CustomOption.Create(355, "Trigger Both Cooldowns", true, witchSpawnRate); + miniSpawnRate = CustomOption.Create(180, cs(Mini.color, "Mini"), rates, null, true); miniGrowingUpDuration = CustomOption.Create(181, "Mini Growing Up Duration", 400f, 100f, 1500f, 100f, miniSpawnRate); @@ -299,6 +321,14 @@ namespace TheOtherRoles { vultureCanUseVents = CustomOption.Create(343, "Vulture Can Use Vents", true, vultureSpawnRate); vultureShowArrows = CustomOption.Create(344, "Show Arrows Pointing Towards The Corpes", true, vultureSpawnRate); + lawyerSpawnRate = CustomOption.Create(350, cs(Lawyer.color, "Lawyer"), rates, null, true); + lawyerTargetKnows = CustomOption.Create(351, "Lawyer Target Knows", true, lawyerSpawnRate); + lawyerWinsAfterMeetings = CustomOption.Create(352, "Lawyer Wins After Meetings", false, lawyerSpawnRate); + lawyerNeededMeetings = CustomOption.Create(353, "Lawyer Needed Meetings To Win", 5f, 1f, 15f, 1f, lawyerWinsAfterMeetings); + lawyerVision = CustomOption.Create(354, "Lawyer Vision", 1f, 0.25f, 3f, 0.25f, lawyerSpawnRate); + pursuerCooldown = CustomOption.Create(355, "Pursuer Blank Cooldown", 30f, 5f, 60f, 2.5f, lawyerSpawnRate); + pursuerBlanksNumber = CustomOption.Create(356, "Pursuer Number Of Blanks", 5f, 0f, 20f, 1f, lawyerSpawnRate); + shifterSpawnRate = CustomOption.Create(70, cs(Shifter.color, "Shifter"), rates, null, true); shifterShiftsModifiers = CustomOption.Create(71, "Shifter Shifts Modifiers", false, shifterSpawnRate); diff --git a/TheOtherRoles/Helpers.cs b/TheOtherRoles/Helpers.cs index c557d34..4fe6284 100644 --- a/TheOtherRoles/Helpers.cs +++ b/TheOtherRoles/Helpers.cs @@ -12,6 +12,12 @@ using HarmonyLib; using Hazel; namespace TheOtherRoles { + + public enum MurderAttemptResult { + PerformKill, + SuppressKill, + BlankKill + } public static class Helpers { public static Sprite loadSpriteFromResources(string path, float pixelsPerUnit) { @@ -265,50 +271,61 @@ namespace TheOtherRoles { return roleCouldUse; } - public static bool checkMuderAttempt(PlayerControl killer, PlayerControl target, bool isMeetingStart = false) { + public static MurderAttemptResult checkMuderAttempt(PlayerControl killer, PlayerControl target, bool blockRewind = false) { // Modified vanilla checks - if (AmongUsClient.Instance.IsGameOver) return false; - if (killer == null || killer.Data == null || killer.Data.IsDead || killer.Data.Disconnected) return false; // Allow non Impostor kills compared to vanilla code - if (target == null || target.Data == null || target.Data.IsDead || target.Data.Disconnected) return false; // Allow killing players in vents compared to vanilla code + if (AmongUsClient.Instance.IsGameOver) return MurderAttemptResult.SuppressKill; + if (killer == null || killer.Data == null || killer.Data.IsDead || killer.Data.Disconnected) return MurderAttemptResult.SuppressKill; // Allow non Impostor kills compared to vanilla code + if (target == null || target.Data == null || target.Data.IsDead || target.Data.Disconnected) return MurderAttemptResult.SuppressKill; // Allow killing players in vents compared to vanilla code + + // Handle blank shot + if (Pursuer.blankedList.Any(x => x.PlayerId == killer.PlayerId)) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetBlanked, Hazel.SendOption.Reliable, -1); + writer.Write(killer.PlayerId); + writer.Write((byte)0); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.setBlanked(killer.PlayerId, 0); + + return MurderAttemptResult.BlankKill; + } // Block impostor shielded kill if (Medic.shielded != null && Medic.shielded == target) { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(killer.NetId, (byte)CustomRPC.ShieldedMurderAttempt, Hazel.SendOption.Reliable, -1); AmongUsClient.Instance.FinishRpcImmediately(writer); RPCProcedure.shieldedMurderAttempt(); - return false; + return MurderAttemptResult.SuppressKill; } // Block impostor not fully grown mini kill else if (Mini.mini != null && target == Mini.mini && !Mini.isGrownUp()) { - return false; + return MurderAttemptResult.SuppressKill; } // Block Time Master with time shield kill else if (TimeMaster.shieldActive && TimeMaster.timeMaster != null && TimeMaster.timeMaster == target) { - if (!isMeetingStart) { // Only rewind the attempt was not called because a meeting startet + if (!blockRewind) { // Only rewind the attempt was not called because a meeting startet MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(killer.NetId, (byte)CustomRPC.TimeMasterRewindTime, Hazel.SendOption.Reliable, -1); AmongUsClient.Instance.FinishRpcImmediately(writer); RPCProcedure.timeMasterRewindTime(); } - return false; + return MurderAttemptResult.SuppressKill; } - return true; + return MurderAttemptResult.PerformKill; } - public static bool checkMuderAttemptAndKill(PlayerControl killer, PlayerControl target, bool isMeetingStart = false) { + public static MurderAttemptResult checkMuderAttemptAndKill(PlayerControl killer, PlayerControl target, bool isMeetingStart = false) { // The local player checks for the validity of the kill and performs it afterwards (different to vanilla, where the host performs all the checks) // The kill attempt will be shared using a custom RPC, hence combining modded and unmodded versions is impossible - bool attemptIsSuccessful = checkMuderAttempt(killer, target, isMeetingStart); - if (attemptIsSuccessful) { + MurderAttemptResult murder = checkMuderAttempt(killer, target, isMeetingStart); + if (murder == MurderAttemptResult.PerformKill) { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1); writer.Write(killer.PlayerId); writer.Write(target.PlayerId); AmongUsClient.Instance.FinishRpcImmediately(writer); RPCProcedure.uncheckedMurderPlayer(killer.PlayerId, target.PlayerId); } - return attemptIsSuccessful; + return murder; } public static void shareGameVersion() { @@ -322,5 +339,16 @@ namespace TheOtherRoles { AmongUsClient.Instance.FinishRpcImmediately(writer); RPCProcedure.versionHandshake(TheOtherRolesPlugin.Version.Major, TheOtherRolesPlugin.Version.Minor, TheOtherRolesPlugin.Version.Build, TheOtherRolesPlugin.Version.Revision, Assembly.GetExecutingAssembly().ManifestModule.ModuleVersionId, AmongUsClient.Instance.ClientId); } + + public static List getKillerTeamMembers(PlayerControl player) { + List team = new List(); + foreach(PlayerControl p in PlayerControl.AllPlayerControls) { + if (player.Data.Role.IsImpostor && p.Data.Role.IsImpostor && player.PlayerId != p.PlayerId && team.All(x => x.PlayerId != p.PlayerId)) team.Add(p); + else if (player == Jackal.jackal && p == Sidekick.sidekick) team.Add(p); + else if (player == Sidekick.sidekick && p == Jackal.jackal) team.Add(p); + } + + return team; + } } } diff --git a/TheOtherRoles/Main.cs b/TheOtherRoles/Main.cs index 5950b34..1190a86 100644 --- a/TheOtherRoles/Main.cs +++ b/TheOtherRoles/Main.cs @@ -21,7 +21,7 @@ namespace TheOtherRoles public class TheOtherRolesPlugin : BasePlugin { public const string Id = "me.eisbison.theotherroles"; - public const string VersionString = "3.1.2"; + public const string VersionString = "3.2.0"; public static System.Version Version = System.Version.Parse(VersionString); internal static BepInEx.Logging.ManualLogSource Logger; diff --git a/TheOtherRoles/Modules/CustomHats.cs b/TheOtherRoles/Modules/CustomHats.cs index ab5de20..d46d26b 100644 --- a/TheOtherRoles/Modules/CustomHats.cs +++ b/TheOtherRoles/Modules/CustomHats.cs @@ -236,9 +236,9 @@ namespace TheOtherRoles.Modules { } } - [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetHat), new System.Type[] { typeof(HatBehaviour), typeof(int) })] + [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetHat), new System.Type[] { typeof(string), typeof(int) })] private static class HatParentSetHatPatch { - static void Postfix(HatParent __instance, HatBehaviour hat, int color) { + static void Postfix(HatParent __instance, string hatId, int color) { if (DestroyableSingleton.InstanceExists) { try { string filePath = Path.GetDirectoryName(Application.dataPath) + @"\TheOtherHats\Test"; @@ -263,6 +263,8 @@ namespace TheOtherRoles.Modules { public static float createHatPackage(List> hats, string packageName, float YStart, HatsTab __instance) { bool isDefaultPackage = innerslothPackageName == packageName; + if (!isDefaultPackage) + hats = hats.OrderBy(x => x.Item1.name).ToList(); float offset = YStart; if (textTemplate != null) { @@ -311,7 +313,7 @@ namespace TheOtherRoles.Modules { } colorChip.transform.localPosition = new Vector3(xpos, ypos, -1f); - colorChip.Inner.SetHat(hat, PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId); + colorChip.Inner.SetHat(hat, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : ((int)SaveManager.BodyColor)); colorChip.Inner.transform.localPosition = hat.ChipOffset; colorChip.Tag = hat; colorChip.SelectionHighlight.gameObject.SetActive(false); diff --git a/TheOtherRoles/Modules/CustomOptions.cs b/TheOtherRoles/Modules/CustomOptions.cs index 4629a38..699d018 100644 --- a/TheOtherRoles/Modules/CustomOptions.cs +++ b/TheOtherRoles/Modules/CustomOptions.cs @@ -370,7 +370,7 @@ namespace TheOtherRoles { var hudString = sb.ToString(); int defaultSettingsLines = 23; - int roleSettingsLines = defaultSettingsLines + 38; + int roleSettingsLines = defaultSettingsLines + 39; int detailedSettingsP1 = roleSettingsLines + 40; int detailedSettingsP2 = detailedSettingsP1 + 42; int detailedSettingsP3 = detailedSettingsP2 + 42; @@ -394,7 +394,7 @@ namespace TheOtherRoles { gap = 19; index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count(); hudString = hudString.Insert(index + 1, "\n"); - gap = 24; + gap = 25; index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count(); hudString = hudString.Insert(index + 1, "\n"); } else if (counter == 2) { diff --git a/TheOtherRoles/Patches/CustomServerPatch.cs b/TheOtherRoles/Patches/CustomServerPatch.cs index ae5ec0e..591cdb7 100644 --- a/TheOtherRoles/Patches/CustomServerPatch.cs +++ b/TheOtherRoles/Patches/CustomServerPatch.cs @@ -2,9 +2,11 @@ // Orginial code was written by 6pak and miniduikboot as part of the Reactor api for Among Us (https://github.com/NuclearPowered/Reactor) using HarmonyLib; -namespace TheOtherRoles.Patches { +namespace TheOtherRoles.Patches +{ [HarmonyPatch(typeof(InnerNet.InnerNetClient), nameof(InnerNet.InnerNetClient.GetConnectionData))] - public static class InnerNetClientGetConnectionDataPatch { + public static class InnerNetClientGetConnectionDataPatch + { public static void Prefix(ref bool useDtlsLayout) { var serverManager = ServerManager.Instance; @@ -15,8 +17,10 @@ namespace TheOtherRoles.Patches { } [HarmonyPatch(typeof(AuthManager), nameof(AuthManager.CreateDtlsConnection))] - public static class AuthManagerCreateDtlsConnection { - public static bool Prefix(ref Hazel.Udp.UnityUdpClientConnection __result, string targetIp, ushort targetPort) { + public static class AuthManagerCreateDtlsConnection + { + public static bool Prefix(ref Hazel.Udp.UnityUdpClientConnection __result, string targetIp, ushort targetPort) + { var serverManager = ServerManager.Instance; DnsRegionInfo region = serverManager.CurrentRegion.TryCast(); if (region == null || !region.Fqdn.EndsWith("among.us")) { @@ -27,4 +31,4 @@ namespace TheOtherRoles.Patches { return true; } } -} +} \ No newline at end of file diff --git a/TheOtherRoles/Patches/EndGamePatch.cs b/TheOtherRoles/Patches/EndGamePatch.cs index 2b4d39b..faefbfb 100644 --- a/TheOtherRoles/Patches/EndGamePatch.cs +++ b/TheOtherRoles/Patches/EndGamePatch.cs @@ -18,7 +18,8 @@ namespace TheOtherRoles.Patches { MiniLose = 12, JesterWin = 13, ArsonistWin = 14, - VultureWin = 15 + VultureWin = 15, + LawyerSoloWin = 16 } enum WinCondition { @@ -29,16 +30,22 @@ namespace TheOtherRoles.Patches { JackalWin, MiniLose, ArsonistWin, - VultureWin + VultureWin, + LawyerSoloWin, + AdditionalLawyerBonusWin, + AdditionalLawyerStolenWin, + AdditionalAlivePursuerWin } static class AdditionalTempData { // Should be implemented using a proper GameOverReason in the future public static WinCondition winCondition = WinCondition.Default; + public static List additionalWinConditions = new List(); public static List playerRoles = new List(); public static void clear() { playerRoles.Clear(); + additionalWinConditions.Clear(); winCondition = WinCondition.Default; } @@ -75,6 +82,9 @@ namespace TheOtherRoles.Patches { if (Jackal.jackal != null) notWinners.Add(Jackal.jackal); if (Arsonist.arsonist != null) notWinners.Add(Arsonist.arsonist); if (Vulture.vulture != null) notWinners.Add(Vulture.vulture); + if (Lawyer.lawyer != null) notWinners.Add(Lawyer.lawyer); + if (Pursuer.pursuer != null) notWinners.Add(Pursuer.pursuer); + notWinners.AddRange(Jackal.formerJackals); List winnersToRemove = new List(); @@ -89,6 +99,7 @@ namespace TheOtherRoles.Patches { bool loversWin = Lovers.existingAndAlive() && (gameOverReason == (GameOverReason)CustomGameOverReason.LoversWin || (TempData.DidHumansWin(gameOverReason) && !Lovers.existingWithKiller())); // Either they win if they are among the last 3 players, or they win if they are both Crewmates and both alive and the Crew wins (Team Imp/Jackal Lovers can only win solo wins) bool teamJackalWin = gameOverReason == (GameOverReason)CustomGameOverReason.TeamJackalWin && ((Jackal.jackal != null && !Jackal.jackal.Data.IsDead) || (Sidekick.sidekick != null && !Sidekick.sidekick.Data.IsDead)); bool vultureWin = Vulture.vulture != null && gameOverReason == (GameOverReason)CustomGameOverReason.VultureWin; + bool lawyerSoloWin = Lawyer.lawyer != null && gameOverReason == (GameOverReason)CustomGameOverReason.LawyerSoloWin; // Mini lose if (miniLose) { @@ -133,6 +144,8 @@ namespace TheOtherRoles.Patches { if (p == null) continue; if (p == Lovers.lover1 || p == Lovers.lover2) TempData.winners.Add(new WinningPlayerData(p.Data)); + else if (p == Pursuer.pursuer && !Pursuer.pursuer.Data.IsDead) + TempData.winners.Add(new WinningPlayerData(p.Data)); else if (p != Jester.jester && p != Jackal.jackal && p != Sidekick.sidekick && p != Arsonist.arsonist && !Jackal.formerJackals.Contains(p) && !p.Data.Role.IsImpostor) TempData.winners.Add(new WinningPlayerData(p.Data)); } @@ -167,6 +180,39 @@ namespace TheOtherRoles.Patches { } } + // Lawyer solo win + else if (lawyerSoloWin) { + TempData.winners = new Il2CppSystem.Collections.Generic.List(); + WinningPlayerData wpd = new WinningPlayerData(Lawyer.lawyer.Data); + TempData.winners.Add(wpd); + AdditionalTempData.winCondition = WinCondition.LawyerSoloWin; + } + + // Possible Additional winner: Lawyer + if (!lawyerSoloWin && Lawyer.lawyer != null && Lawyer.target != null && !Lawyer.target.Data.IsDead) { + if (!TempData.winners.ToArray().Any(x => x.PlayerName == Lawyer.lawyer.Data.PlayerName)) + TempData.winners.Add(new WinningPlayerData(Lawyer.lawyer.Data)); + WinningPlayerData client = null; + foreach (WinningPlayerData winner in TempData.winners) { + if (winner.PlayerName == Lawyer.target.Data.PlayerName) + client = winner; + } + + if (!Lawyer.lawyer.Data.IsDead && client != null) { // Lawyer wins with the winning team but steals the win from his client + TempData.winners.Remove(client); + AdditionalTempData.additionalWinConditions.Add(WinCondition.AdditionalLawyerStolenWin); + } else { + AdditionalTempData.additionalWinConditions.Add(WinCondition.AdditionalLawyerBonusWin); + } + } + + // Possible Additional winner: Pursuer + if (Pursuer.pursuer != null && !Pursuer.pursuer.Data.IsDead) { + if (!TempData.winners.ToArray().Any(x => x.PlayerName == Pursuer.pursuer.Data.PlayerName)) + TempData.winners.Add(new WinningPlayerData(Pursuer.pursuer.Data)); + AdditionalTempData.additionalWinConditions.Add(WinCondition.AdditionalAlivePursuerWin); + } + // Reset Settings RPCProcedure.resetVariables(); } @@ -176,7 +222,7 @@ namespace TheOtherRoles.Patches { public class EndGameManagerSetUpPatch { public static void Postfix(EndGameManager __instance) { GameObject bonusText = UnityEngine.Object.Instantiate(__instance.WinText.gameObject); - bonusText.transform.position = new Vector3(__instance.WinText.transform.position.x, __instance.WinText.transform.position.y - 0.8f, __instance.WinText.transform.position.z); + bonusText.transform.position = new Vector3(__instance.WinText.transform.position.x, __instance.WinText.transform.position.y - 0.5f, __instance.WinText.transform.position.z); bonusText.transform.localScale = new Vector3(0.7f, 0.7f, 1f); TMPro.TMP_Text textRenderer = bonusText.GetComponent(); textRenderer.text = ""; @@ -193,6 +239,10 @@ namespace TheOtherRoles.Patches { textRenderer.text = "Vulture Wins"; textRenderer.color = Vulture.color; } + else if (AdditionalTempData.winCondition == WinCondition.LawyerSoloWin) { + textRenderer.text = "Lawyer Wins"; + textRenderer.color = Lawyer.color; + } else if (AdditionalTempData.winCondition == WinCondition.LoversTeamWin) { textRenderer.text = "Lovers And Crewmates Win"; textRenderer.color = Lovers.color; @@ -212,6 +262,16 @@ namespace TheOtherRoles.Patches { textRenderer.color = Mini.color; } + foreach (WinCondition cond in AdditionalTempData.additionalWinConditions) { + if (cond == WinCondition.AdditionalLawyerStolenWin) { + textRenderer.text += $"\n{Helpers.cs(Lawyer.color, "Lawyer stole the win of the client")}"; + } else if (cond == WinCondition.AdditionalLawyerBonusWin) { + textRenderer.text += $"\n{Helpers.cs(Lawyer.color, "Lawyer kept the client alive")}"; + } else if (cond == WinCondition.AdditionalAlivePursuerWin) { + textRenderer.text += $"\n{Helpers.cs(Pursuer.color, "Pursuer survived")}"; + } + } + if (MapOptions.showRoleSummary) { var position = Camera.main.ViewportToWorldPoint(new Vector3(0f, 1f, Camera.main.nearClipPlane)); GameObject roleSummary = UnityEngine.Object.Instantiate(__instance.WinText.gameObject); @@ -249,6 +309,7 @@ namespace TheOtherRoles.Patches { var statistics = new PlayerStatistics(__instance); if (CheckAndEndGameForMiniLose(__instance)) return false; if (CheckAndEndGameForJesterWin(__instance)) return false; + if (CheckAndEndGameForLawyerMeetingWin(__instance)) return false; if (CheckAndEndGameForArsonistWin(__instance)) return false; if (CheckAndEndGameForVultureWin(__instance)) return false; if (CheckAndEndGameForSabotageWin(__instance)) return false; @@ -295,6 +356,15 @@ namespace TheOtherRoles.Patches { } return false; } + + private static bool CheckAndEndGameForLawyerMeetingWin(ShipStatus __instance) { + if (Lawyer.triggerLawyerWin) { + __instance.enabled = false; + ShipStatus.RpcEndGame((GameOverReason)CustomGameOverReason.LawyerSoloWin, false); + return true; + } + return false; + } private static bool CheckAndEndGameForSabotageWin(ShipStatus __instance) { if (__instance.Systems == null) return false; diff --git a/TheOtherRoles/Patches/ExileControllerPatch.cs b/TheOtherRoles/Patches/ExileControllerPatch.cs index a0648a0..0f4b1ad 100644 --- a/TheOtherRoles/Patches/ExileControllerPatch.cs +++ b/TheOtherRoles/Patches/ExileControllerPatch.cs @@ -51,6 +51,21 @@ namespace TheOtherRoles.Patches { JackInTheBox.convertToVents(); } + + // Witch execute casted spells + if (Witch.witch != null && Witch.futureSpelled != null && AmongUsClient.Instance.AmHost) { + foreach (PlayerControl target in Witch.futureSpelled) { + if (target != null && !target.Data.IsDead && Helpers.checkMuderAttempt(Witch.witch, target, true) == MurderAttemptResult.PerformKill) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedExilePlayer, Hazel.SendOption.Reliable, -1); + writer.Write(target.PlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.uncheckedExilePlayer(target.PlayerId); + } + } + } + Witch.futureSpelled = new List(); + + // SecurityGuard vents and cameras var allCameras = ShipStatus.Instance.AllCameras.ToList(); MapOptions.camerasToAdd.ForEach(camera => { @@ -176,6 +191,8 @@ namespace TheOtherRoles.Patches { } } + if (Lawyer.lawyer != null && PlayerControl.LocalPlayer == Lawyer.lawyer && !Lawyer.lawyer.Data.IsDead) + Lawyer.meetings++; } } diff --git a/TheOtherRoles/Patches/IntroPatch.cs b/TheOtherRoles/Patches/IntroPatch.cs index 0bfb761..c67303d 100644 --- a/TheOtherRoles/Patches/IntroPatch.cs +++ b/TheOtherRoles/Patches/IntroPatch.cs @@ -50,15 +50,16 @@ namespace TheOtherRoles.Patches { BountyHunter.cooldownText.transform.localPosition = bottomLeft + new Vector3(0f, -1f, -1f); BountyHunter.cooldownText.gameObject.SetActive(true); } + } + } - } } [HarmonyPatch] class IntroPatch { public static void setupIntroTeamIcons(IntroCutscene __instance, ref Il2CppSystem.Collections.Generic.List yourTeam) { // Intro solo teams - if (PlayerControl.LocalPlayer == Jester.jester || PlayerControl.LocalPlayer == Jackal.jackal || PlayerControl.LocalPlayer == Arsonist.arsonist || PlayerControl.LocalPlayer == Vulture.vulture) { + if (PlayerControl.LocalPlayer == Jester.jester || PlayerControl.LocalPlayer == Jackal.jackal || PlayerControl.LocalPlayer == Arsonist.arsonist || PlayerControl.LocalPlayer == Vulture.vulture || PlayerControl.LocalPlayer == Lawyer.lawyer) { var soloTeam = new Il2CppSystem.Collections.Generic.List(); soloTeam.Add(PlayerControl.LocalPlayer); yourTeam = soloTeam; diff --git a/TheOtherRoles/Patches/MeetingPatch.cs b/TheOtherRoles/Patches/MeetingPatch.cs index f88850f..d14070a 100644 --- a/TheOtherRoles/Patches/MeetingPatch.cs +++ b/TheOtherRoles/Patches/MeetingPatch.cs @@ -260,8 +260,8 @@ namespace TheOtherRoles.Patches { Transform buttonMask = UnityEngine.Object.Instantiate(maskTemplate, buttonParent); TMPro.TextMeshPro label = UnityEngine.Object.Instantiate(textTemplate, button); buttons.Add(button); - int row = i/4, col = i%4; - buttonParent.localPosition = new Vector3(-2.725f + 1.83f * col, 1.5f - 0.45f * row, -5); + int row = i/5, col = i%5; + buttonParent.localPosition = new Vector3(-3.47f + 1.75f * col, 1.5f - 0.45f * row, -5); buttonParent.localScale = new Vector3(0.55f, 0.55f, 1f); label.text = Helpers.cs(roleInfo.color, roleInfo.name); label.alignment = TMPro.TextAlignmentOptions.Center; @@ -275,10 +275,10 @@ namespace TheOtherRoles.Patches { selectedButton = button; buttons.ForEach(x => x.GetComponent().color = x == selectedButton ? Color.red : Color.white); } else { - PlayerControl target = Helpers.playerById((byte)__instance.playerStates[buttonTarget].TargetPlayerId); - if (!(__instance.state == MeetingHud.VoteStates.Voted || __instance.state == MeetingHud.VoteStates.NotVoted) || target == null || Guesser.remainingShots <= 0 ) return; + PlayerControl focusedTarget = Helpers.playerById((byte)__instance.playerStates[buttonTarget].TargetPlayerId); + if (!(__instance.state == MeetingHud.VoteStates.Voted || __instance.state == MeetingHud.VoteStates.NotVoted) || focusedTarget == null || Guesser.remainingShots <= 0 ) return; - if (!Guesser.killsThroughShield && target == Medic.shielded) { // Depending on the options, shooting the shielded player will not allow the guess, notifiy everyone about the kill attempt and close the window + if (!Guesser.killsThroughShield && focusedTarget == Medic.shielded) { // Depending on the options, shooting the shielded player will not allow the guess, notifiy everyone about the kill attempt and close the window __instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(true)); UnityEngine.Object.Destroy(container.gameObject); @@ -288,24 +288,26 @@ namespace TheOtherRoles.Patches { return; } - var mainRoleInfo = RoleInfo.getRoleInfoForPlayer(target).FirstOrDefault(); + var mainRoleInfo = RoleInfo.getRoleInfoForPlayer(focusedTarget).FirstOrDefault(); if (mainRoleInfo == null) return; - target = (mainRoleInfo == roleInfo) ? target : PlayerControl.LocalPlayer; + PlayerControl dyingTarget = (mainRoleInfo == roleInfo) ? focusedTarget : PlayerControl.LocalPlayer; // Reset the GUI __instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(true)); UnityEngine.Object.Destroy(container.gameObject); - if (Guesser.hasMultipleShotsPerMeeting && Guesser.remainingShots > 1 && target != PlayerControl.LocalPlayer) - __instance.playerStates.ToList().ForEach(x => { if (x.TargetPlayerId == target.PlayerId && x.transform.FindChild("ShootButton") != null) UnityEngine.Object.Destroy(x.transform.FindChild("ShootButton").gameObject); }); + if (Guesser.hasMultipleShotsPerMeeting && Guesser.remainingShots > 1 && dyingTarget != PlayerControl.LocalPlayer) + __instance.playerStates.ToList().ForEach(x => { if (x.TargetPlayerId == dyingTarget.PlayerId && x.transform.FindChild("ShootButton") != null) UnityEngine.Object.Destroy(x.transform.FindChild("ShootButton").gameObject); }); else __instance.playerStates.ToList().ForEach(x => { if (x.transform.FindChild("ShootButton") != null) UnityEngine.Object.Destroy(x.transform.FindChild("ShootButton").gameObject); }); // Shoot player and send chat info if activated MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.GuesserShoot, Hazel.SendOption.Reliable, -1); - writer.Write(target.PlayerId); + writer.Write(dyingTarget.PlayerId); + writer.Write(focusedTarget.PlayerId); + writer.Write((byte)roleInfo.roleId); AmongUsClient.Instance.FinishRpcImmediately(writer); - RPCProcedure.guesserShoot(target.PlayerId); + RPCProcedure.guesserShoot(dyingTarget.PlayerId, focusedTarget.PlayerId, (byte)roleInfo.roleId); } })); @@ -351,6 +353,19 @@ namespace TheOtherRoles.Patches { } } + // Add overlay for spelled players + if (Witch.witch != null && Witch.futureSpelled != null) { + foreach (PlayerVoteArea pva in __instance.playerStates) { + if (Witch.futureSpelled.Any(x => x.PlayerId == pva.TargetPlayerId)) { + SpriteRenderer rend = (new GameObject()).AddComponent(); + rend.transform.SetParent(pva.transform); + rend.gameObject.layer = pva.Megaphone.gameObject.layer; + rend.transform.localPosition = new Vector3(-1.21f, -0.12f, -1f); + rend.sprite = Witch.getSpelledOverlaySprite(); + } + } + } + // Add Guesser Buttons if (Guesser.guesser != null && PlayerControl.LocalPlayer == Guesser.guesser && !Guesser.guesser.Data.IsDead && Guesser.remainingShots > 0) { for (int i = 0; i < __instance.playerStates.Length; i++) { diff --git a/TheOtherRoles/Patches/PlayerControlPatch.cs b/TheOtherRoles/Patches/PlayerControlPatch.cs index 4a73702..1fc1ded 100644 --- a/TheOtherRoles/Patches/PlayerControlPatch.cs +++ b/TheOtherRoles/Patches/PlayerControlPatch.cs @@ -12,8 +12,7 @@ using UnityEngine; namespace TheOtherRoles.Patches { [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))] - public static class PlayerControlFixedUpdatePatch - { + public static class PlayerControlFixedUpdatePatch { // Helpers static PlayerControl setTarget(bool onlyCrewmates = false, bool targetPlayersInVents = false, List untargetablePlayers = null, PlayerControl targetingPlayer = null) { @@ -22,26 +21,22 @@ namespace TheOtherRoles.Patches { if (!ShipStatus.Instance) return result; if (targetingPlayer == null) targetingPlayer = PlayerControl.LocalPlayer; if (targetingPlayer.Data.IsDead) return result; - + Vector2 truePosition = targetingPlayer.GetTruePosition(); Il2CppSystem.Collections.Generic.List allPlayers = GameData.Instance.AllPlayers; - for (int i = 0; i < allPlayers.Count; i++) - { + for (int i = 0; i < allPlayers.Count; i++) { GameData.PlayerInfo playerInfo = allPlayers[i]; - if (!playerInfo.Disconnected && playerInfo.PlayerId != targetingPlayer.PlayerId && !playerInfo.IsDead && (!onlyCrewmates || !playerInfo.Role.IsImpostor)) - { + if (!playerInfo.Disconnected && playerInfo.PlayerId != targetingPlayer.PlayerId && !playerInfo.IsDead && (!onlyCrewmates || !playerInfo.Role.IsImpostor)) { PlayerControl @object = playerInfo.Object; - if(untargetablePlayers != null && untargetablePlayers.Any(x => x == @object)) { + if (untargetablePlayers != null && untargetablePlayers.Any(x => x == @object)) { // if that player is not targetable: skip check continue; } - if (@object && (!@object.inVent || targetPlayersInVents)) - { + if (@object && (!@object.inVent || targetPlayersInVents)) { Vector2 vector = @object.GetTruePosition() - truePosition; float magnitude = vector.magnitude; - if (magnitude <= num && !PhysicsHelpers.AnyNonTriggersBetween(truePosition, vector.normalized, magnitude, Constants.ShipAndObjectsMask)) - { + if (magnitude <= num && !PhysicsHelpers.AnyNonTriggersBetween(truePosition, vector.normalized, magnitude, Constants.ShipAndObjectsMask)) { result = @object; num = magnitude; } @@ -53,7 +48,7 @@ namespace TheOtherRoles.Patches { static void setPlayerOutline(PlayerControl target, Color color) { if (target == null || target.myRend == null) return; - + target.myRend.material.SetFloat("_Outline", 1f); target.myRend.material.SetColor("_OutlineColor", color); } @@ -63,7 +58,7 @@ namespace TheOtherRoles.Patches { static void setBasePlayerOutlines() { foreach (PlayerControl target in PlayerControl.AllPlayerControls) { if (target == null || target.myRend == null) continue; - + bool isMorphedMorphling = target == Morphling.morphling && Morphling.morphTarget != null && Morphling.morphTimer > 0f; bool hasVisibleShield = false; if (Camouflager.camouflageTimer <= 0f && Medic.shielded != null && ((target == Medic.shielded && !isMorphedMorphling) || (isMorphedMorphling && Morphling.morphTarget == Medic.shielded))) { @@ -75,7 +70,8 @@ namespace TheOtherRoles.Patches { if (hasVisibleShield) { target.myRend.material.SetFloat("_Outline", 1f); target.myRend.material.SetColor("_OutlineColor", Medic.shieldedColor); - } else { + } + else { target.myRend.material.SetFloat("_Outline", 0f); } } @@ -101,18 +97,21 @@ namespace TheOtherRoles.Patches { } // Set position PlayerControl.LocalPlayer.transform.position = next.Item1; - } else if (localPlayerPositions.Any(x => x.Item2 == true)) { + } + else if (localPlayerPositions.Any(x => x.Item2 == true)) { PlayerControl.LocalPlayer.transform.position = next.Item1; } - + localPlayerPositions.RemoveAt(0); if (localPlayerPositions.Count > 1) localPlayerPositions.RemoveAt(0); // Skip every second position to rewinde twice as fast, but never skip the last position - } else { + } + else { TimeMaster.isRewinding = false; PlayerControl.LocalPlayer.moveable = true; } - } else { + } + else { while (localPlayerPositions.Count >= Mathf.Round(TimeMaster.rewindTime / Time.fixedDeltaTime)) localPlayerPositions.RemoveAt(localPlayerPositions.Count - 1); localPlayerPositions.Insert(0, new Tuple(PlayerControl.LocalPlayer.transform.position, PlayerControl.LocalPlayer.CanMove)); // CanMove = CanMove } @@ -130,13 +129,13 @@ namespace TheOtherRoles.Patches { if (Shifter.futureShift == null) setPlayerOutline(Shifter.currentTarget, Shifter.color); } - + static void morphlingSetTarget() { if (Morphling.morphling == null || Morphling.morphling != PlayerControl.LocalPlayer) return; Morphling.currentTarget = setTarget(); setPlayerOutline(Morphling.currentTarget, Morphling.color); } - + static void sheriffSetTarget() { if (Sheriff.sheriff == null || Sheriff.sheriff != PlayerControl.LocalPlayer) return; Sheriff.currentTarget = setTarget(); @@ -149,7 +148,7 @@ namespace TheOtherRoles.Patches { if (!Tracker.usedTracker) setPlayerOutline(Tracker.currentTarget, Tracker.color); } - static void detectiveUpdateFootPrints() { + static void detectiveUpdateFootPrints() { if (Detective.detective == null || Detective.detective != PlayerControl.LocalPlayer) return; Detective.timer -= Time.fixedDeltaTime; @@ -166,14 +165,16 @@ namespace TheOtherRoles.Patches { static void vampireSetTarget() { if (Vampire.vampire == null || Vampire.vampire != PlayerControl.LocalPlayer) return; - PlayerControl target = null; + PlayerControl target = null; if (Spy.spy != null) { if (Spy.impostorsCanKillAnyone) { target = setTarget(false, true); - } else { + } + else { target = setTarget(true, true, new List() { Spy.spy }); } - } else { + } + else { target = setTarget(true, true); } @@ -193,21 +194,21 @@ namespace TheOtherRoles.Patches { static void jackalSetTarget() { if (Jackal.jackal == null || Jackal.jackal != PlayerControl.LocalPlayer) return; var untargetablePlayers = new List(); - if(Jackal.canCreateSidekickFromImpostor) { + if (Jackal.canCreateSidekickFromImpostor) { // Only exclude sidekick from beeing targeted if the jackal can create sidekicks from impostors - if(Sidekick.sidekick != null) untargetablePlayers.Add(Sidekick.sidekick); + if (Sidekick.sidekick != null) untargetablePlayers.Add(Sidekick.sidekick); } - if(Mini.mini != null && !Mini.isGrownUp()) untargetablePlayers.Add(Mini.mini); // Exclude Jackal from targeting the Mini unless it has grown up - Jackal.currentTarget = setTarget(untargetablePlayers : untargetablePlayers); + if (Mini.mini != null && !Mini.isGrownUp()) untargetablePlayers.Add(Mini.mini); // Exclude Jackal from targeting the Mini unless it has grown up + Jackal.currentTarget = setTarget(untargetablePlayers: untargetablePlayers); setPlayerOutline(Jackal.currentTarget, Palette.ImpostorRed); } static void sidekickSetTarget() { if (Sidekick.sidekick == null || Sidekick.sidekick != PlayerControl.LocalPlayer) return; var untargetablePlayers = new List(); - if(Jackal.jackal != null) untargetablePlayers.Add(Jackal.jackal); - if(Mini.mini != null && !Mini.isGrownUp()) untargetablePlayers.Add(Mini.mini); // Exclude Sidekick from targeting the Mini unless it has grown up - Sidekick.currentTarget = setTarget(untargetablePlayers : untargetablePlayers); + if (Jackal.jackal != null) untargetablePlayers.Add(Jackal.jackal); + if (Mini.mini != null && !Mini.isGrownUp()) untargetablePlayers.Add(Mini.mini); // Exclude Sidekick from targeting the Mini unless it has grown up + Sidekick.currentTarget = setTarget(untargetablePlayers: untargetablePlayers); if (Sidekick.canKill) setPlayerOutline(Sidekick.currentTarget, Palette.ImpostorRed); } @@ -218,7 +219,7 @@ namespace TheOtherRoles.Patches { if (Jackal.jackal == null || Jackal.jackal?.Data?.Disconnected == true) { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SidekickPromotes, Hazel.SendOption.Reliable, -1); AmongUsClient.Instance.FinishRpcImmediately(writer); - RPCProcedure.sidekickPromotes(); + RPCProcedure.sidekickPromotes(); } } @@ -241,11 +242,13 @@ namespace TheOtherRoles.Patches { if (Engineer.engineer != null && Engineer.engineer.inVent) { vent.myRend.material.SetFloat("_Outline", 1f); vent.myRend.material.SetColor("_OutlineColor", Engineer.color); - } else if (vent.myRend.material.GetColor("_AddColor") != Color.red) { + } + else if (vent.myRend.material.GetColor("_AddColor") != Color.red) { vent.myRend.material.SetFloat("_Outline", 0); } } - } catch {} + } + catch { } } } } @@ -255,15 +258,17 @@ namespace TheOtherRoles.Patches { HudManager.Instance.KillButton.SetTarget(null); return; } - - PlayerControl target = null; + + PlayerControl target = null; if (Spy.spy != null) { if (Spy.impostorsCanKillAnyone) { target = setTarget(false, true); - } else { + } + else { target = setTarget(true, true, new List() { Spy.spy }); } - } else { + } + else { target = setTarget(true, true); } @@ -279,7 +284,8 @@ namespace TheOtherRoles.Patches { if (Warlock.curseVictim == null) { Warlock.currentTarget = setTarget(); setPlayerOutline(Warlock.currentTarget, Warlock.color); - } else { + } + else { Warlock.curseVictimTarget = setTarget(targetingPlayer: Warlock.curseVictim); setPlayerOutline(Warlock.curseVictimTarget, Warlock.color); } @@ -342,13 +348,13 @@ namespace TheOtherRoles.Patches { public static void playerSizeUpdate(PlayerControl p) { // Set default player size CircleCollider2D collider = p.GetComponent(); - + p.transform.localScale = new Vector3(0.7f, 0.7f, 1f); collider.radius = Mini.defaultColliderRadius; collider.offset = Mini.defaultColliderOffset * Vector2.down; // Set adapted player size to Mini and Morphling - if (Mini.mini == null || Camouflager.camouflageTimer > 0f) return; + if (Mini.mini == null || Camouflager.camouflageTimer > 0f) return; float growingProgress = Mini.growingProgress(); float scale = growingProgress * 0.35f + 0.35f; @@ -386,13 +392,13 @@ namespace TheOtherRoles.Patches { meetingInfo.fontSize *= 0.75f; meetingInfo.gameObject.name = "Info"; } - + var (tasksCompleted, tasksTotal) = TasksHandler.taskInfo(p.Data); - string roleNames = String.Join(" ", RoleInfo.getRoleInfoForPlayer(p).Select(x => Helpers.cs(x.color, x.name)).ToArray()); + string roleNames = RoleInfo.GetRole(p); string taskInfo = tasksTotal > 0 ? $"({tasksCompleted}/{tasksTotal})" : ""; - + string playerInfoText = ""; - string meetingInfoText =""; + string meetingInfoText = ""; if (p == PlayerControl.LocalPlayer) { playerInfoText = $"{roleNames}"; if (DestroyableSingleton.InstanceExists) { @@ -400,7 +406,7 @@ namespace TheOtherRoles.Patches { tabText.SetText($"Tasks {taskInfo}"); } meetingInfoText = $"{roleNames} {taskInfo}".Trim(); - } + } else if (MapOptions.ghostsSeeRoles && MapOptions.ghostsSeeTasks) { playerInfoText = $"{roleNames} {taskInfo}".Trim(); meetingInfoText = playerInfoText; @@ -449,8 +455,7 @@ namespace TheOtherRoles.Patches { if (Arsonist.currentTarget != null) setPlayerOutline(Arsonist.currentTarget, Arsonist.color); } - static void snitchUpdate() - { + static void snitchUpdate() { if (Snitch.localArrows == null) return; foreach (Arrow arrow in Snitch.localArrows) arrow.arrow.SetActive(false); @@ -462,14 +467,12 @@ namespace TheOtherRoles.Patches { if (numberOfTasks <= Snitch.taskCountForReveal && (PlayerControl.LocalPlayer.Data.Role.IsImpostor || (Snitch.includeTeamJackal && (PlayerControl.LocalPlayer == Jackal.jackal || PlayerControl.LocalPlayer == Sidekick.sidekick)))) { if (Snitch.localArrows.Count == 0) Snitch.localArrows.Add(new Arrow(Color.blue)); - if (Snitch.localArrows.Count != 0 && Snitch.localArrows[0] != null) - { + if (Snitch.localArrows.Count != 0 && Snitch.localArrows[0] != null) { Snitch.localArrows[0].arrow.SetActive(true); Snitch.localArrows[0].Update(Snitch.snitch.transform.position); } } - else if (PlayerControl.LocalPlayer == Snitch.snitch && numberOfTasks == 0) - { + else if (PlayerControl.LocalPlayer == Snitch.snitch && numberOfTasks == 0) { int arrowIndex = 0; foreach (PlayerControl p in PlayerControl.AllPlayerControls) { @@ -531,7 +534,7 @@ namespace TheOtherRoles.Patches { // Update Cooldown Text if (BountyHunter.cooldownText != null) { BountyHunter.cooldownText.text = Mathf.CeilToInt(Mathf.Clamp(BountyHunter.bountyUpdateTimer, 0, BountyHunter.bountyDuration)).ToString(); - } + } // Update Arrow if (BountyHunter.showArrow && BountyHunter.bounty != null) { @@ -552,7 +555,7 @@ namespace TheOtherRoles.Patches { Bait.reportDelay -= Time.fixedDeltaTime; DeadPlayer deadPlayer = deadPlayers?.Where(x => x.player?.PlayerId == Bait.bait.PlayerId)?.FirstOrDefault(); if (deadPlayer.killerIfExisting != null && Bait.reportDelay <= 0f) { - + Helpers.handleVampireBiteOnBodyReport(); // Manually call Vampire handling, since the CmdReportDeadBody Prefix won't be called RPCProcedure.uncheckedCmdReportDeadBody(deadPlayer.killerIfExisting.PlayerId, Bait.bait.PlayerId); @@ -579,7 +582,8 @@ namespace TheOtherRoles.Patches { if (ventsWithPlayers.Contains(vent.Id) || (ventsWithPlayers.Count > 0 && Bait.highlightAllVents)) { vent.myRend.material.SetFloat("_Outline", 1f); vent.myRend.material.SetColor("_OutlineColor", Color.yellow); - } else { + } + else { vent.myRend.material.SetFloat("_Outline", 0); } } @@ -591,7 +595,7 @@ namespace TheOtherRoles.Patches { if (Vulture.vulture.Data.IsDead) { foreach (Arrow arrow in Vulture.localArrows) UnityEngine.Object.Destroy(arrow.arrow); Vulture.localArrows = new List(); - return; + return; } DeadBody[] deadBodies = UnityEngine.Object.FindObjectsOfType(); @@ -602,10 +606,10 @@ namespace TheOtherRoles.Patches { foreach (Arrow arrow in Vulture.localArrows) UnityEngine.Object.Destroy(arrow.arrow); Vulture.localArrows = new List(); } - + foreach (DeadBody db in deadBodies) { if (arrowUpdate) { - Vulture.localArrows.Add(new Arrow(Vulture.color)); + Vulture.localArrows.Add(new Arrow(Color.blue)); Vulture.localArrows[index].arrow.SetActive(true); } if (Vulture.localArrows[index] != null) Vulture.localArrows[index].Update(db.transform.position); @@ -636,7 +640,7 @@ namespace TheOtherRoles.Patches { Camouflager.camouflageTimer = Mathf.Max(0f, Camouflager.camouflageTimer - Time.fixedDeltaTime); Morphling.morphTimer = Mathf.Max(0f, Morphling.morphTimer - Time.fixedDeltaTime); - + // Camouflage reset and set Morphling look if necessary if (oldCamouflageTimer > 0f && Camouflager.camouflageTimer <= 0f) { Camouflager.resetCamouflage(); @@ -651,6 +655,47 @@ namespace TheOtherRoles.Patches { Morphling.resetMorph(); } + public static void lawyerUpdate() { + if (Lawyer.lawyer == null || Lawyer.lawyer != PlayerControl.LocalPlayer) return; + + // Meeting win + if (Lawyer.winsAfterMeetings && Lawyer.neededMeetings == Lawyer.meetings && Lawyer.target != null && !Lawyer.target.Data.IsDead) { + Lawyer.winsAfterMeetings = false; // Avoid sending mutliple RPCs until the host finshes the game + MessageWriter winWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.LawyerWin, Hazel.SendOption.Reliable, -1); + AmongUsClient.Instance.FinishRpcImmediately(winWriter); + RPCProcedure.lawyerWin(); + return; + } + + // Promote to Pursuer + if (Lawyer.target != null && Lawyer.target.Data.Disconnected) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.LawyerPromotesToPursuer, Hazel.SendOption.Reliable, -1); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.lawyerPromotesToPursuer(); + return; + } + } + + static void pursuerSetTarget() { + if (Pursuer.pursuer == null || Pursuer.pursuer != PlayerControl.LocalPlayer) return; + Pursuer.target = setTarget(); + setPlayerOutline(Pursuer.target, Pursuer.color); + } + + static void witchSetTarget() { + if (Witch.witch == null || Witch.witch != PlayerControl.LocalPlayer) return; + List untargetables; + if (Witch.spellCastingTarget != null) + untargetables = PlayerControl.AllPlayerControls.ToArray().Where(x => x.PlayerId != Witch.spellCastingTarget.PlayerId).ToList(); // Don't switch the target from the the one you're currently casting a spell on + else { + untargetables = new List(); // Also target players that have already been spelled, to hide spells that were blanks/blocked by shields + if (Spy.spy != null && !Witch.canSpellAnyone) untargetables.Add(Spy.spy); + } + Witch.currentTarget = setTarget(onlyCrewmates: !Witch.canSpellAnyone, untargetablePlayers: untargetables); + setPlayerOutline(Witch.currentTarget, Witch.color); + } + + public static void Postfix(PlayerControl __instance) { if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return; @@ -716,6 +761,12 @@ namespace TheOtherRoles.Patches { mediumSetTarget(); // Morphling and Camouflager morphlingAndCamouflagerUpdate(); + // Lawyer + lawyerUpdate(); + // Pursuer + pursuerSetTarget(); + // Witch + witchSetTarget(); } } } @@ -759,7 +810,7 @@ namespace TheOtherRoles.Patches { msg = $"Body Report: Killed {Math.Round(timeSinceDeath / 1000)}s ago!"; } else if (isDetectiveReport) { if (timeSinceDeath < Detective.reportNameDuration * 1000) { - msg = $"Body Report: The killer appears to be {deadPlayer.killerIfExisting.name}!"; + msg = $"Body Report: The killer appears to be {deadPlayer.killerIfExisting.Data.PlayerName}!"; } else if (timeSinceDeath < Detective.reportColorDuration * 1000) { var typeOfColor = Helpers.isLighterColor(deadPlayer.killerIfExisting.Data.DefaultOutfit.ColorId) ? "lighter" : "darker"; msg = $"Body Report: The killer appears to be a {typeOfColor} color!"; @@ -828,10 +879,22 @@ namespace TheOtherRoles.Patches { RPCProcedure.sidekickPromotes(); } + // Pursuer promotion trigger on murder (the host sends the call such that everyone recieves the update before a possible game End) + if (target == Lawyer.target && AmongUsClient.Instance.AmHost) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.LawyerPromotesToPursuer, Hazel.SendOption.Reliable, -1); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.lawyerPromotesToPursuer(); + } + // Cleaner Button Sync if (Cleaner.cleaner != null && PlayerControl.LocalPlayer == Cleaner.cleaner && __instance == Cleaner.cleaner && HudManagerStartPatch.cleanerCleanButton != null) HudManagerStartPatch.cleanerCleanButton.Timer = Cleaner.cleaner.killTimer; + + // Witch Button Sync + if (Witch.triggerBothCooldowns && Witch.witch != null && PlayerControl.LocalPlayer == Witch.witch && __instance == Witch.witch && HudManagerStartPatch.witchSpellButton != null) + HudManagerStartPatch.witchSpellButton.Timer = HudManagerStartPatch.witchSpellButton.MaxTimer; + // Warlock Button Sync if (Warlock.warlock != null && PlayerControl.LocalPlayer == Warlock.warlock && __instance == Warlock.warlock && HudManagerStartPatch.warlockCurseButton != null) { if(Warlock.warlock.killTimer > HudManagerStartPatch.warlockCurseButton.Timer) { @@ -936,6 +999,13 @@ namespace TheOtherRoles.Patches { AmongUsClient.Instance.FinishRpcImmediately(writer); RPCProcedure.sidekickPromotes(); } + + // Pursuer promotion trigger on exile (the host sends the call such that everyone recieves the update before a possible game End) + if (__instance == Lawyer.target && AmongUsClient.Instance.AmHost) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.LawyerPromotesToPursuer, Hazel.SendOption.Reliable, -1); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.lawyerPromotesToPursuer(); + } } } } diff --git a/TheOtherRoles/Patches/RoleAssignmentPatch.cs b/TheOtherRoles/Patches/RoleAssignmentPatch.cs index 95db273..f2bd6e4 100644 --- a/TheOtherRoles/Patches/RoleAssignmentPatch.cs +++ b/TheOtherRoles/Patches/RoleAssignmentPatch.cs @@ -32,6 +32,7 @@ namespace TheOtherRoles.Patches { selectFactionForFactionIndependentRoles(data); 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 + assignRoleTargets(data); } private static RoleAssignmentData getRoleAssignmentData() { @@ -76,11 +77,13 @@ namespace TheOtherRoles.Patches { impSettings.Add((byte)RoleId.Cleaner, CustomOptionHolder.cleanerSpawnRate.getSelection()); impSettings.Add((byte)RoleId.Warlock, CustomOptionHolder.warlockSpawnRate.getSelection()); impSettings.Add((byte)RoleId.BountyHunter, CustomOptionHolder.bountyHunterSpawnRate.getSelection()); + impSettings.Add((byte)RoleId.Witch, CustomOptionHolder.witchSpawnRate.getSelection()); neutralSettings.Add((byte)RoleId.Jester, CustomOptionHolder.jesterSpawnRate.getSelection()); neutralSettings.Add((byte)RoleId.Arsonist, CustomOptionHolder.arsonistSpawnRate.getSelection()); neutralSettings.Add((byte)RoleId.Jackal, CustomOptionHolder.jackalSpawnRate.getSelection()); neutralSettings.Add((byte)RoleId.Vulture, CustomOptionHolder.vultureSpawnRate.getSelection()); + neutralSettings.Add((byte)RoleId.Lawyer, CustomOptionHolder.lawyerSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Mayor, CustomOptionHolder.mayorSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Engineer, CustomOptionHolder.engineerSpawnRate.getSelection()); @@ -263,6 +266,28 @@ namespace TheOtherRoles.Patches { } } + private static void assignRoleTargets(RoleAssignmentData data) { + // Set Lawyer Target + if (Lawyer.lawyer != null) { + var possibleTargets = new List(); + foreach (PlayerControl p in PlayerControl.AllPlayerControls) { + if (!p.Data.IsDead && !p.Data.Disconnected && p != Lovers.lover1 && p != Lovers.lover2 && (p.Data.Role.IsImpostor || p == Jackal.jackal)) + possibleTargets.Add(p); + } + if (possibleTargets.Count == 0) { + MessageWriter w = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.LawyerPromotesToPursuer, Hazel.SendOption.Reliable, -1); + AmongUsClient.Instance.FinishRpcImmediately(w); + RPCProcedure.lawyerPromotesToPursuer(); + } else { + var target = possibleTargets[TheOtherRoles.rnd.Next(0, possibleTargets.Count)]; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.LawyerSetTarget, Hazel.SendOption.Reliable, -1); + writer.Write(target.PlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.lawyerSetTarget(target.PlayerId); + } + } + } + private static byte setRoleToRandomPlayer(byte roleId, List playerList, byte flag = 0, bool removePlayer = true) { var index = rnd.Next(0, playerList.Count); byte playerId = playerList[index].PlayerId; diff --git a/TheOtherRoles/Patches/ShipStatusPatch.cs b/TheOtherRoles/Patches/ShipStatusPatch.cs index 64f2b12..23fbcc4 100644 --- a/TheOtherRoles/Patches/ShipStatusPatch.cs +++ b/TheOtherRoles/Patches/ShipStatusPatch.cs @@ -16,7 +16,7 @@ namespace TheOtherRoles.Patches { if (switchSystem == null) return true; float num = (float)switchSystem.Value / 255f; - + if (player == null || player.IsDead) // IsDead __result = __instance.MaxLightRadius; else if (player.Role.IsImpostor @@ -29,9 +29,11 @@ namespace TheOtherRoles.Patches { else if (Trickster.trickster != null && Trickster.lightsOutTimer > 0f) { float lerpValue = 1f; if (Trickster.lightsOutDuration - Trickster.lightsOutTimer < 0.5f) lerpValue = Mathf.Clamp01((Trickster.lightsOutDuration - Trickster.lightsOutTimer) * 2); - else if (Trickster.lightsOutTimer < 0.5) lerpValue = Mathf.Clamp01(Trickster.lightsOutTimer*2); + else if (Trickster.lightsOutTimer < 0.5) lerpValue = Mathf.Clamp01(Trickster.lightsOutTimer * 2); __result = Mathf.Lerp(__instance.MinLightRadius, __instance.MaxLightRadius, 1 - lerpValue) * PlayerControl.GameOptions.CrewLightMod; // Instant lights out? Maybe add a smooth transition? } + else if (Lawyer.lawyer != null && Lawyer.lawyer.PlayerId == player.PlayerId) // if player is Lighter and Lighter has his ability active + __result = Mathf.Lerp(__instance.MinLightRadius, __instance.MaxLightRadius * Lawyer.vision, num); else __result = Mathf.Lerp(__instance.MinLightRadius, __instance.MaxLightRadius, num) * PlayerControl.GameOptions.CrewLightMod; return false; diff --git a/TheOtherRoles/Patches/UpdatePatch.cs b/TheOtherRoles/Patches/UpdatePatch.cs index dc96ff7..3e1d6fa 100644 --- a/TheOtherRoles/Patches/UpdatePatch.cs +++ b/TheOtherRoles/Patches/UpdatePatch.cs @@ -115,6 +115,10 @@ namespace TheOtherRoles.Patches { setPlayerNameColor(Vulture.vulture, Vulture.color); } else if (Medium.medium != null && Medium.medium == PlayerControl.LocalPlayer) { setPlayerNameColor(Medium.medium, Medium.color); + } else if (Lawyer.lawyer != null && Lawyer.lawyer == PlayerControl.LocalPlayer) { + setPlayerNameColor(Lawyer.lawyer, Lawyer.color); + } else if (Pursuer.pursuer != null && Pursuer.pursuer == PlayerControl.LocalPlayer) { + setPlayerNameColor(Pursuer.pursuer, Pursuer.color); } // No else if here, as a Lover of team Jackal needs the colors @@ -132,7 +136,7 @@ namespace TheOtherRoles.Patches { } // Crewmate roles with no changes: Mini - // Impostor roles with no changes: Morphling, Camouflager, Vampire, Godfather, Eraser, Janitor, Cleaner, Warlock, BountyHunter and Mafioso + // Impostor roles with no changes: Morphling, Camouflager, Vampire, Godfather, Eraser, Janitor, Cleaner, Warlock, BountyHunter, Witch and Mafioso } static void setNameTags() { @@ -167,6 +171,19 @@ namespace TheOtherRoles.Patches { player.NameText.text += suffix; } + // Lawyer + bool localIsLawyer = Lawyer.lawyer != null && Lawyer.target != null && Lawyer.lawyer == PlayerControl.LocalPlayer; + bool localIsKnowingTarget = Lawyer.lawyer != null && Lawyer.target != null && Lawyer.targetKnows && Lawyer.target == PlayerControl.LocalPlayer; + if (localIsLawyer || localIsKnowingTarget) { + string suffix = Helpers.cs(Lawyer.color, " §"); + Lawyer.target.nameText.text += suffix; + + if (MeetingHud.Instance != null) + foreach (PlayerVoteArea player in MeetingHud.Instance.playerStates) + if (player.TargetPlayerId == Lawyer.target.PlayerId) + player.NameText.text += suffix; + } + // Hacker and Detective if (PlayerControl.LocalPlayer != null && !PlayerControl.LocalPlayer.Data.IsDead && (PlayerControl.LocalPlayer == Hacker.hacker || PlayerControl.LocalPlayer == Detective.detective || PlayerControl.LocalPlayer == Medium.medium)) { if (MeetingHud.Instance != null) { diff --git a/TheOtherRoles/Patches/UsablesPatch.cs b/TheOtherRoles/Patches/UsablesPatch.cs index 30b8bad..5ccd09e 100644 --- a/TheOtherRoles/Patches/UsablesPatch.cs +++ b/TheOtherRoles/Patches/UsablesPatch.cs @@ -123,7 +123,19 @@ namespace TheOtherRoles.Patches { public static bool Prefix(KillButton __instance) { if (__instance.isActiveAndEnabled && __instance.currentTarget && !__instance.isCoolingDown && !PlayerControl.LocalPlayer.Data.IsDead && PlayerControl.LocalPlayer.CanMove) { // Use an unchecked kill command, to allow shorter kill cooldowns etc. without getting kicked - Helpers.checkMuderAttemptAndKill(PlayerControl.LocalPlayer, __instance.currentTarget); + MurderAttemptResult res = Helpers.checkMuderAttemptAndKill(PlayerControl.LocalPlayer, __instance.currentTarget); + // Handle blank kill + if (res == MurderAttemptResult.BlankKill) { + PlayerControl.LocalPlayer.killTimer = PlayerControl.GameOptions.KillCooldown; + if (PlayerControl.LocalPlayer == Cleaner.cleaner) + Cleaner.cleaner.killTimer = HudManagerStartPatch.cleanerCleanButton.Timer = HudManagerStartPatch.cleanerCleanButton.MaxTimer; + else if (PlayerControl.LocalPlayer == Warlock.warlock) + Warlock.warlock.killTimer = HudManagerStartPatch.warlockCurseButton.Timer = HudManagerStartPatch.warlockCurseButton.MaxTimer; + else if (PlayerControl.LocalPlayer == Mini.mini && Mini.mini.Data.Role.IsImpostor) + Mini.mini.SetKillTimer(PlayerControl.GameOptions.KillCooldown * (Mini.isGrownUp() ? 0.66f : 2f)); + else if (PlayerControl.LocalPlayer == Witch.witch) + Witch.witch.killTimer = HudManagerStartPatch.witchSpellButton.Timer = HudManagerStartPatch.witchSpellButton.MaxTimer; + } __instance.SetTarget(null); } return false; @@ -159,6 +171,11 @@ namespace TheOtherRoles.Patches { roleCanCallEmergency = false; statusText = "The Jester can't start an emergency meeting"; } + // Potentially deactivate emergency button for Lawyer + if (Lawyer.lawyer != null && Lawyer.lawyer == PlayerControl.LocalPlayer && Lawyer.winsAfterMeetings) { + roleCanCallEmergency = false; + statusText = "The Lawyer can't start an emergency meeting (" + Lawyer.meetings + "/" + Lawyer.neededMeetings + " meetings)"; + } if (!roleCanCallEmergency) { __instance.StatusText.text = statusText; diff --git a/TheOtherRoles/RPC.cs b/TheOtherRoles/RPC.cs index 119c157..f24490b 100644 --- a/TheOtherRoles/RPC.cs +++ b/TheOtherRoles/RPC.cs @@ -50,6 +50,9 @@ namespace TheOtherRoles Bait, Vulture, Medium, + Lawyer, + Pursuer, + Witch, Crewmate, Impostor } @@ -66,6 +69,7 @@ namespace TheOtherRoles UseUncheckedVent, UncheckedMurderPlayer, UncheckedCmdReportDeadBody, + UncheckedExilePlayer, // Role functionality @@ -89,6 +93,7 @@ namespace TheOtherRoles SetFutureErased, SetFutureShifted, SetFutureShielded, + SetFutureSpelled, PlaceJackInTheBox, LightsOut, WarlockCurseKill, @@ -96,7 +101,11 @@ namespace TheOtherRoles SealVent, ArsonistWin, GuesserShoot, - VultureWin + VultureWin, + LawyerWin, + LawyerSetTarget, + LawyerPromotesToPursuer, + SetBlanked, } public static class RPCProcedure { @@ -242,6 +251,15 @@ namespace TheOtherRoles case RoleId.Medium: Medium.medium = player; break; + case RoleId.Lawyer: + Lawyer.lawyer = player; + break; + case RoleId.Pursuer: + Pursuer.pursuer = player; + break; + case RoleId.Witch: + Witch.witch = player; + break; } } } @@ -282,6 +300,10 @@ namespace TheOtherRoles if (source != null && target != null) source.ReportDeadBody(target.Data); } + public static void uncheckedExilePlayer(byte targetId) { + PlayerControl target = Helpers.playerById(targetId); + if (target != null) target.Exiled(); + } // Role functionality @@ -370,7 +392,7 @@ namespace TheOtherRoles Shifter.clearAndReload(); // Suicide (exile) when impostor or impostor variants - if (player.Data.Role.IsImpostor || player == Jackal.jackal || player == Sidekick.sidekick || Jackal.formerJackals.Contains(player) || player == Jester.jester || player == Arsonist.arsonist || player == Vulture.vulture) { + if (player.Data.Role.IsImpostor || player == Jackal.jackal || player == Sidekick.sidekick || Jackal.formerJackals.Contains(player) || player == Jester.jester || player == Arsonist.arsonist || player == Vulture.vulture || player == Lawyer.lawyer) { oldShifter.Exiled(); return; } @@ -490,21 +512,17 @@ namespace TheOtherRoles } public static void jackalCreatesSidekick(byte targetId) { - foreach (PlayerControl player in PlayerControl.AllPlayerControls) - { - if (player.PlayerId == targetId) - { - if (!Jackal.canCreateSidekickFromImpostor && player.Data.Role.IsImpostor) { - Jackal.fakeSidekick = player; - } else { - player.RemoveInfected(); - erasePlayerRoles(player.PlayerId, true); - Sidekick.sidekick = player; - } - Jackal.canCreateSidekick = false; - return; - } + PlayerControl player = Helpers.playerById(targetId); + if (player == null) return; + + if (!Jackal.canCreateSidekickFromImpostor && player.Data.Role.IsImpostor) { + Jackal.fakeSidekick = player; + } else { + DestroyableSingleton.Instance.SetRole(player, RoleTypes.Crewmate); + erasePlayerRoles(player.PlayerId, true); + Sidekick.sidekick = player; } + Jackal.canCreateSidekick = false; } public static void sidekickPromotes() { @@ -550,7 +568,8 @@ namespace TheOtherRoles if (player == Trickster.trickster) Trickster.clearAndReload(); if (player == Cleaner.cleaner) Cleaner.clearAndReload(); if (player == Warlock.warlock) Warlock.clearAndReload(); - + if (player == Witch.witch) Witch.clearAndReload(); + // Other roles if (player == Jester.jester) Jester.clearAndReload(); if (player == Arsonist.arsonist) Arsonist.clearAndReload(); @@ -568,6 +587,8 @@ namespace TheOtherRoles if (player == Sidekick.sidekick) Sidekick.clearAndReload(); if (player == BountyHunter.bountyHunter) BountyHunter.clearAndReload(); if (player == Vulture.vulture) Vulture.clearAndReload(); + if (player == Lawyer.lawyer) Lawyer.clearAndReload(); + if (player == Pursuer.pursuer) Pursuer.clearAndReload(); } public static void setFutureErased(byte playerId) { @@ -587,7 +608,17 @@ namespace TheOtherRoles Medic.futureShielded = Helpers.playerById(playerId); Medic.usedShield = true; } - + + public static void setFutureSpelled(byte playerId) { + PlayerControl player = Helpers.playerById(playerId); + if (Witch.futureSpelled == null) + Witch.futureSpelled = new List(); + if (player != null) { + Witch.futureSpelled.Add(player); + } + } + + public static void placeJackInTheBox(byte[] buff) { Vector3 position = Vector3.zero; position.x = BitConverter.ToSingle(buff, 0*sizeof(float)); @@ -658,23 +689,42 @@ namespace TheOtherRoles public static void arsonistWin() { Arsonist.triggerArsonistWin = true; + foreach (PlayerControl p in PlayerControl.AllPlayerControls) { + if (p != Arsonist.arsonist) p.Exiled(); + } } public static void vultureWin() { Vulture.triggerVultureWin = true; } - public static void guesserShoot(byte playerId) { - PlayerControl target = Helpers.playerById(playerId); - if (target == null) return; - target.Exiled(); - PlayerControl dyingLoverPartner = Lovers.bothDie ? target.getPartner() : null; // Lover check - byte partnerId = dyingLoverPartner != null ? dyingLoverPartner.PlayerId : playerId; + public static void lawyerWin() { + Lawyer.triggerLawyerWin = true; + } + + public static void lawyerSetTarget(byte playerId) { + Lawyer.target = Helpers.playerById(playerId); + } + + public static void lawyerPromotesToPursuer() { + PlayerControl player = Lawyer.lawyer; + Lawyer.clearAndReload(); + Pursuer.pursuer = player; + return; + } + + public static void guesserShoot(byte dyingTargetId, byte guessedTargetId, byte guessedRoleId) { + PlayerControl dyingTarget = Helpers.playerById(dyingTargetId); + if (dyingTarget == null ) return; + dyingTarget.Exiled(); + PlayerControl dyingLoverPartner = Lovers.bothDie ? dyingTarget.getPartner() : null; // Lover check + byte partnerId = dyingLoverPartner != null ? dyingLoverPartner.PlayerId : dyingTargetId; + Guesser.remainingShots = Mathf.Max(0, Guesser.remainingShots - 1); - if (Constants.ShouldPlaySfx()) SoundManager.Instance.PlaySound(target.KillSfx, false, 0.8f); + if (Constants.ShouldPlaySfx()) SoundManager.Instance.PlaySound(dyingTarget.KillSfx, false, 0.8f); if (MeetingHud.Instance) { foreach (PlayerVoteArea pva in MeetingHud.Instance.playerStates) { - if (pva.TargetPlayerId == playerId || pva.TargetPlayerId == partnerId) { + if (pva.TargetPlayerId == dyingTargetId || pva.TargetPlayerId == partnerId) { pva.SetDead(pva.DidReport, true); pva.Overlay.gameObject.SetActive(true); } @@ -683,20 +733,28 @@ namespace TheOtherRoles MeetingHud.Instance.CheckForEndVoting(); } if (HudManager.Instance != null && Guesser.guesser != null) - if (PlayerControl.LocalPlayer == target) - HudManager.Instance.KillOverlay.ShowKillAnimation(Guesser.guesser.Data, target.Data); + if (PlayerControl.LocalPlayer == dyingTarget) + HudManager.Instance.KillOverlay.ShowKillAnimation(Guesser.guesser.Data, dyingTarget.Data); else if (dyingLoverPartner != null && PlayerControl.LocalPlayer == dyingLoverPartner) HudManager.Instance.KillOverlay.ShowKillAnimation(dyingLoverPartner.Data, dyingLoverPartner.Data); - var mainRoleInfo = RoleInfo.getRoleInfoForPlayer(target).FirstOrDefault(); - if (Guesser.showInfoInGhostChat && mainRoleInfo != null && PlayerControl.LocalPlayer.Data.IsDead) { - string msg = $"Guesser guessed the role {mainRoleInfo.name} for {target.Data.PlayerName}!"; + PlayerControl guessedTarget = Helpers.playerById(guessedTargetId); + if (Guesser.showInfoInGhostChat && PlayerControl.LocalPlayer.Data.IsDead && guessedTarget != null) { + RoleInfo roleInfo = RoleInfo.allRoleInfos.FirstOrDefault(x => (byte)x.roleId == guessedRoleId); + string msg = $"Guesser guessed the role {roleInfo?.name ?? ""} for {guessedTarget.Data.PlayerName}!"; if (AmongUsClient.Instance.AmClient && DestroyableSingleton.Instance) DestroyableSingleton.Instance.Chat.AddChat(Guesser.guesser, msg); if (msg.IndexOf("who", StringComparison.OrdinalIgnoreCase) >= 0) DestroyableSingleton.Instance.SendWho(); } } + + public static void setBlanked(byte playerId, byte value) { + PlayerControl target = Helpers.playerById(playerId); + if (target == null) return; + Pursuer.blankedList.RemoveAll(x => x.PlayerId == playerId); + if (value > 0) Pursuer.blankedList.Add(target); + } } [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] @@ -754,6 +812,10 @@ namespace TheOtherRoles byte target = reader.ReadByte(); RPCProcedure.uncheckedMurderPlayer(source, target); break; + case (byte)CustomRPC.UncheckedExilePlayer: + byte exileTarget = reader.ReadByte(); + RPCProcedure.uncheckedExilePlayer(exileTarget); + break; case (byte)CustomRPC.UncheckedCmdReportDeadBody: byte reportSource = reader.ReadByte(); byte reportTarget = reader.ReadByte(); @@ -845,11 +907,31 @@ namespace TheOtherRoles RPCProcedure.arsonistWin(); break; case (byte)CustomRPC.GuesserShoot: - RPCProcedure.guesserShoot(reader.ReadByte()); + byte dyingTarget = reader.ReadByte(); + byte guessedTarget = reader.ReadByte(); + byte guessedRoleId = reader.ReadByte(); + RPCProcedure.guesserShoot(dyingTarget, guessedTarget, guessedRoleId); break; case (byte)CustomRPC.VultureWin: RPCProcedure.vultureWin(); break; + case (byte)CustomRPC.LawyerWin: + RPCProcedure.lawyerWin(); + break; + case (byte)CustomRPC.LawyerSetTarget: + RPCProcedure.lawyerSetTarget(reader.ReadByte()); + break; + case (byte)CustomRPC.LawyerPromotesToPursuer: + RPCProcedure.lawyerPromotesToPursuer(); + break; + case (byte)CustomRPC.SetBlanked: + var pid = reader.ReadByte(); + var blankedValue = reader.ReadByte(); + RPCProcedure.setBlanked(pid, blankedValue); + break; + case (byte)CustomRPC.SetFutureSpelled: + RPCProcedure.setFutureSpelled(reader.ReadByte()); + break; } } } diff --git a/TheOtherRoles/Resources/PursuerButton.png b/TheOtherRoles/Resources/PursuerButton.png new file mode 100644 index 0000000..5cd9da8 Binary files /dev/null and b/TheOtherRoles/Resources/PursuerButton.png differ diff --git a/TheOtherRoles/Resources/SpellButton.png b/TheOtherRoles/Resources/SpellButton.png new file mode 100644 index 0000000..dc878f8 Binary files /dev/null and b/TheOtherRoles/Resources/SpellButton.png differ diff --git a/TheOtherRoles/Resources/SpellButtonMeeting.png b/TheOtherRoles/Resources/SpellButtonMeeting.png new file mode 100644 index 0000000..30c5482 Binary files /dev/null and b/TheOtherRoles/Resources/SpellButtonMeeting.png differ diff --git a/TheOtherRoles/RoleInfo.cs b/TheOtherRoles/RoleInfo.cs index 6f99102..813ee4d 100644 --- a/TheOtherRoles/RoleInfo.cs +++ b/TheOtherRoles/RoleInfo.cs @@ -61,9 +61,12 @@ namespace TheOtherRoles public static RoleInfo bait = new RoleInfo("Bait", Bait.color, "Bait your enemies", "Bait your enemies", RoleId.Bait); public static RoleInfo vulture = new RoleInfo("Vulture", Vulture.color, "Eat Corpses to win", "Eat dead bodies", RoleId.Vulture, true); public static RoleInfo medium = new RoleInfo("Medium", Medium.color, "Question the souls of the dead to gain informations", "Question the souls", RoleId.Medium); + public static RoleInfo lawyer = new RoleInfo("Lawyer", Lawyer.color, "Defend your client", "Defend your client", RoleId.Lawyer, true); + public static RoleInfo pursuer = new RoleInfo("Pursuer", Pursuer.color, "Blank the Impostors", "Blank the Impostors", RoleId.Pursuer); public static RoleInfo impostor = new RoleInfo("Impostor", Palette.ImpostorRed, Helpers.cs(Palette.ImpostorRed, "Sabotage and kill everyone"), "Sabotage and kill everyone", RoleId.Impostor); public static RoleInfo crewmate = new RoleInfo("Crewmate", Color.white, "Find the Impostors", "Find the Impostors", RoleId.Crewmate); public static RoleInfo lover = new RoleInfo("Lover", Lovers.color, $"You are in love", $"You are in love", RoleId.Lover); + public static RoleInfo witch = new RoleInfo("Witch", Witch.color, "Cast a spell upon your foes", "Cast a spell upon your foes", RoleId.Witch); public static List allRoleInfos = new List() { impostor, @@ -78,6 +81,7 @@ namespace TheOtherRoles cleaner, warlock, bountyHunter, + witch, niceMini, evilMini, goodGuesser, @@ -88,6 +92,8 @@ namespace TheOtherRoles jackal, sidekick, vulture, + pursuer, + lawyer, crewmate, shifter, mayor, @@ -128,6 +134,7 @@ namespace TheOtherRoles if (p == Trickster.trickster) infos.Add(trickster); if (p == Cleaner.cleaner) infos.Add(cleaner); if (p == Warlock.warlock) infos.Add(warlock); + if (p == Witch.witch) infos.Add(witch); if (p == Detective.detective) infos.Add(detective); if (p == TimeMaster.timeMaster) infos.Add(timeMaster); if (p == Medic.medic) infos.Add(medic); @@ -148,6 +155,8 @@ namespace TheOtherRoles if (p == Bait.bait) infos.Add(bait); if (p == Vulture.vulture) infos.Add(vulture); if (p == Medium.medium) infos.Add(medium); + if (p == Lawyer.lawyer) infos.Add(lawyer); + if (p == Pursuer.pursuer) infos.Add(pursuer); // Default roles if (infos.Count == 0 && p.Data.Role.IsImpostor) infos.Add(impostor); // Just Impostor @@ -161,8 +170,9 @@ namespace TheOtherRoles public static String GetRole(PlayerControl p) { string roleName; - roleName = String.Join("", getRoleInfoForPlayer(p).Select(x => x.name).ToArray()); - if (roleName.Contains("Lover")) roleName.Replace("Lover", ""); + roleName = String.Join("", getRoleInfoForPlayer(p).Select(x => Helpers.cs(x.color, x.name)).ToArray()); + if (roleName.Contains("Lover")) roleName.Replace("Lover", Helpers.cs(Lovers.color, " Lover")); + if (Lawyer.target != null && p.PlayerId == Lawyer.target.PlayerId && PlayerControl.LocalPlayer != Lawyer.target) roleName += Helpers.cs(Pursuer.color, " §"); return roleName; } } diff --git a/TheOtherRoles/TasksHandler.cs b/TheOtherRoles/TasksHandler.cs index 8b5aa12..a9b4c63 100644 --- a/TheOtherRoles/TasksHandler.cs +++ b/TheOtherRoles/TasksHandler.cs @@ -35,7 +35,11 @@ namespace TheOtherRoles { __instance.CompletedTasks = 0; for (int i = 0; i < __instance.AllPlayers.Count; i++) { GameData.PlayerInfo playerInfo = __instance.AllPlayers[i]; - if (playerInfo.Object && playerInfo.Object.hasAliveKillingLover()) + if (playerInfo.Object + && playerInfo.Object.hasAliveKillingLover() // Tasks do not count if a Crewmate has an alive killing Lover + || playerInfo.PlayerId == Lawyer.lawyer?.PlayerId // Tasks of the Lawyer do not count + || (playerInfo.PlayerId == Pursuer.pursuer?.PlayerId && Pursuer.pursuer.Data.IsDead) // Tasks of the Pursuer only count, if he's alive + ) continue; var (playerCompleted, playerTotal) = taskInfo(playerInfo); __instance.TotalTasks += playerTotal; diff --git a/TheOtherRoles/TheOtherRoles.cs b/TheOtherRoles/TheOtherRoles.cs index b173d4e..db069c1 100644 --- a/TheOtherRoles/TheOtherRoles.cs +++ b/TheOtherRoles/TheOtherRoles.cs @@ -56,6 +56,9 @@ namespace TheOtherRoles Bait.clearAndReload(); Vulture.clearAndReload(); Medium.clearAndReload(); + Lawyer.clearAndReload(); + Pursuer.clearAndReload(); + Witch.clearAndReload(); } public static class Jester { @@ -1126,4 +1129,106 @@ namespace TheOtherRoles oneTimeUse = CustomOptionHolder.mediumOneTimeUse.getBool(); } } + + public static class Lawyer { + public static PlayerControl lawyer; + public static PlayerControl target; + public static Color color = new Color32(134, 153, 25, byte.MaxValue); + public static Sprite targetSprite; + public static bool triggerLawyerWin = false; + public static int meetings = 0; + + public static bool targetKnows = false; + public static float vision = 1f; + public static bool winsAfterMeetings = false; + public static int neededMeetings = 4; + + public static Sprite getTargetSprite() { + if (targetSprite) return targetSprite; + targetSprite = Helpers.loadSpriteFromResources("", 150f); + return targetSprite; + } + + public static void clearAndReload() { + lawyer = null; + target = null; + triggerLawyerWin = false; + meetings = 0; + + targetKnows = CustomOptionHolder.lawyerTargetKnows.getBool(); + winsAfterMeetings = CustomOptionHolder.lawyerWinsAfterMeetings.getBool(); + neededMeetings = Mathf.RoundToInt(CustomOptionHolder.lawyerNeededMeetings.getFloat()); + vision = CustomOptionHolder.lawyerVision.getFloat(); + } + } + + public static class Pursuer { + public static PlayerControl pursuer; + public static PlayerControl target; + public static Color color = Lawyer.color; + public static List blankedList = new List(); + public static int blanks = 0; + public static Sprite blank; + + public static float cooldown = 30f; + public static int blanksNumber = 5; + + public static Sprite getTargetSprite() { + if (blank) return blank; + blank = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.PursuerButton.png", 115f); + return blank; + } + + public static void clearAndReload() { + pursuer = null; + target = null; + blankedList = new List(); + blanks = 0; + + cooldown = CustomOptionHolder.pursuerCooldown.getFloat(); + blanksNumber = Mathf.RoundToInt(CustomOptionHolder.pursuerBlanksNumber.getFloat()); + } + } + + public static class Witch { + public static PlayerControl witch; + public static Color color = Palette.ImpostorRed; + + public static List futureSpelled = new List(); + public static PlayerControl currentTarget; + public static PlayerControl spellCastingTarget; + public static float cooldown = 30f; + public static float spellCastingDuration = 2f; + public static float cooldownAddition = 10f; + public static float currentCooldownAddition = 0f; + public static bool canSpellAnyone = false; + public static bool triggerBothCooldowns = true; + + private static Sprite buttonSprite; + public static Sprite getButtonSprite() { + if (buttonSprite) return buttonSprite; + buttonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.SpellButton.png", 115f); + return buttonSprite; + } + + private static Sprite spelledOverlaySprite; + public static Sprite getSpelledOverlaySprite() { + if (spelledOverlaySprite) return spelledOverlaySprite; + spelledOverlaySprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.SpellButtonMeeting.png", 225f); + return spelledOverlaySprite; + } + + + public static void clearAndReload() { + witch = null; + futureSpelled = new List(); + currentTarget = spellCastingTarget = null; + cooldown = CustomOptionHolder.witchCooldown.getFloat(); + cooldownAddition = CustomOptionHolder.witchAdditionalCooldown.getFloat(); + currentCooldownAddition = CustomOptionHolder.witchCooldown.getFloat(); + canSpellAnyone = CustomOptionHolder.witchCanSpellAnyone.getBool(); + spellCastingDuration = CustomOptionHolder.witchSpellCastingDuration.getFloat(); + triggerBothCooldowns = CustomOptionHolder.witchTriggerBothCooldowns.getBool(); + } + } } \ No newline at end of file diff --git a/TheOtherRoles/TheOtherRoles.csproj b/TheOtherRoles/TheOtherRoles.csproj index e66b92e..a120c0f 100644 --- a/TheOtherRoles/TheOtherRoles.csproj +++ b/TheOtherRoles/TheOtherRoles.csproj @@ -1,7 +1,7 @@ netstandard2.1 - 3.1.2 + 3.2.0 TheOtherRoles Eisbison