| [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) | | |
| [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) | | |
# 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)
<details>
<summary>Click to show the Changelog</summary>
+**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.
| 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**
| 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.
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;
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;
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;
}
// 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; },
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);
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; },
// 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;
// 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;
},
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;
}
})));
}
+
+ 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; },
}
);
+ // 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();
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;
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;
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);
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);
using Hazel;
namespace TheOtherRoles {
+
+ public enum MurderAttemptResult {
+ PerformKill,
+ SuppressKill,
+ BlankKill
+ }
public static class Helpers {
public static Sprite loadSpriteFromResources(string path, float pixelsPerUnit) {
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() {
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<PlayerControl> getKillerTeamMembers(PlayerControl player) {
+ List<PlayerControl> team = new List<PlayerControl>();
+ 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;
+ }
}
}
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;
}
}
- [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<TutorialManager>.InstanceExists) {
try {
string filePath = Path.GetDirectoryName(Application.dataPath) + @"\TheOtherHats\Test";
public static float createHatPackage(List<System.Tuple<HatBehaviour, HatExtension>> 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) {
}
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);
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;
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) {
// 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;
}
[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<DnsRegionInfo>();
if (region == null || !region.Fqdn.EndsWith("among.us")) {
return true;
}
}
-}
+}
\ No newline at end of file
MiniLose = 12,
JesterWin = 13,
ArsonistWin = 14,
- VultureWin = 15
+ VultureWin = 15,
+ LawyerSoloWin = 16
}
enum WinCondition {
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<WinCondition> additionalWinConditions = new List<WinCondition>();
public static List<PlayerRoleInfo> playerRoles = new List<PlayerRoleInfo>();
public static void clear() {
playerRoles.Clear();
+ additionalWinConditions.Clear();
winCondition = WinCondition.Default;
}
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<WinningPlayerData> winnersToRemove = new List<WinningPlayerData>();
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) {
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));
}
}
}
+ // Lawyer solo win
+ else if (lawyerSoloWin) {
+ TempData.winners = new Il2CppSystem.Collections.Generic.List<WinningPlayerData>();
+ 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();
}
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<TMPro.TMP_Text>();
textRenderer.text = "";
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;
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);
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;
}
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;
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<PlayerControl>();
+
+
// SecurityGuard vents and cameras
var allCameras = ShipStatus.Instance.AllCameras.ToList();
MapOptions.camerasToAdd.ForEach(camera => {
}
}
+ if (Lawyer.lawyer != null && PlayerControl.LocalPlayer == Lawyer.lawyer && !Lawyer.lawyer.Data.IsDead)
+ Lawyer.meetings++;
}
}
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<PlayerControl> 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<PlayerControl>();
soloTeam.Add(PlayerControl.LocalPlayer);
yourTeam = soloTeam;
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;
selectedButton = button;
buttons.ForEach(x => x.GetComponent<SpriteRenderer>().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);
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);
}
}));
}
}
+ // 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<SpriteRenderer>();
+ 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++) {
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<PlayerControl> untargetablePlayers = null, PlayerControl targetingPlayer = null) {
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<GameData.PlayerInfo> 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;
}
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);
}
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))) {
if (hasVisibleShield) {
target.myRend.material.SetFloat("_Outline", 1f);
target.myRend.material.SetColor("_OutlineColor", Medic.shieldedColor);
- } else {
+ }
+ else {
target.myRend.material.SetFloat("_Outline", 0f);
}
}
}
// 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<Vector3, bool>(PlayerControl.LocalPlayer.transform.position, PlayerControl.LocalPlayer.CanMove)); // CanMove = CanMove
}
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();
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;
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<PlayerControl>() { Spy.spy });
}
- } else {
+ }
+ else {
target = setTarget(true, true);
}
static void jackalSetTarget() {
if (Jackal.jackal == null || Jackal.jackal != PlayerControl.LocalPlayer) return;
var untargetablePlayers = new List<PlayerControl>();
- 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<PlayerControl>();
- 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);
}
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();
}
}
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 { }
}
}
}
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<PlayerControl>() { Spy.spy });
}
- } else {
+ }
+ else {
target = setTarget(true, true);
}
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);
}
public static void playerSizeUpdate(PlayerControl p) {
// Set default player size
CircleCollider2D collider = p.GetComponent<CircleCollider2D>();
-
+
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;
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 ? $"<color=#FAD934FF>({tasksCompleted}/{tasksTotal})</color>" : "";
-
+
string playerInfoText = "";
- string meetingInfoText ="";
+ string meetingInfoText = "";
if (p == PlayerControl.LocalPlayer) {
playerInfoText = $"{roleNames}";
if (DestroyableSingleton<TaskPanelBehaviour>.InstanceExists) {
tabText.SetText($"Tasks {taskInfo}");
}
meetingInfoText = $"{roleNames} {taskInfo}".Trim();
- }
+ }
else if (MapOptions.ghostsSeeRoles && MapOptions.ghostsSeeTasks) {
playerInfoText = $"{roleNames} {taskInfo}".Trim();
meetingInfoText = playerInfoText;
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);
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)
{
// 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) {
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);
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);
}
}
if (Vulture.vulture.Data.IsDead) {
foreach (Arrow arrow in Vulture.localArrows) UnityEngine.Object.Destroy(arrow.arrow);
Vulture.localArrows = new List<Arrow>();
- return;
+ return;
}
DeadBody[] deadBodies = UnityEngine.Object.FindObjectsOfType<DeadBody>();
foreach (Arrow arrow in Vulture.localArrows) UnityEngine.Object.Destroy(arrow.arrow);
Vulture.localArrows = new List<Arrow>();
}
-
+
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);
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();
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<PlayerControl> 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<PlayerControl>(); // 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;
mediumSetTarget();
// Morphling and Camouflager
morphlingAndCamouflagerUpdate();
+ // Lawyer
+ lawyerUpdate();
+ // Pursuer
+ pursuerSetTarget();
+ // Witch
+ witchSetTarget();
}
}
}
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!";
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) {
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();
+ }
}
}
}
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() {
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());
}
}
+ private static void assignRoleTargets(RoleAssignmentData data) {
+ // Set Lawyer Target
+ if (Lawyer.lawyer != null) {
+ var possibleTargets = new List<PlayerControl>();
+ 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<PlayerControl> playerList, byte flag = 0, bool removePlayer = true) {
var index = rnd.Next(0, playerList.Count);
byte playerId = playerList[index].PlayerId;
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
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;
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
}
// 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() {
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) {
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;
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;
Bait,
Vulture,
Medium,
+ Lawyer,
+ Pursuer,
+ Witch,
Crewmate,
Impostor
}
UseUncheckedVent,
UncheckedMurderPlayer,
UncheckedCmdReportDeadBody,
+ UncheckedExilePlayer,
// Role functionality
SetFutureErased,
SetFutureShifted,
SetFutureShielded,
+ SetFutureSpelled,
PlaceJackInTheBox,
LightsOut,
WarlockCurseKill,
SealVent,
ArsonistWin,
GuesserShoot,
- VultureWin
+ VultureWin,
+ LawyerWin,
+ LawyerSetTarget,
+ LawyerPromotesToPursuer,
+ SetBlanked,
}
public static class RPCProcedure {
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;
}
}
}
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
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;
}
}
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<RoleManager>.Instance.SetRole(player, RoleTypes.Crewmate);
+ erasePlayerRoles(player.PlayerId, true);
+ Sidekick.sidekick = player;
}
+ Jackal.canCreateSidekick = false;
}
public static void sidekickPromotes() {
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();
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) {
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<PlayerControl>();
+ 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));
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);
}
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<HudManager>.Instance)
DestroyableSingleton<HudManager>.Instance.Chat.AddChat(Guesser.guesser, msg);
if (msg.IndexOf("who", StringComparison.OrdinalIgnoreCase) >= 0)
DestroyableSingleton<Assets.CoreScripts.Telemetry>.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))]
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();
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;
}
}
}
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<RoleInfo> allRoleInfos = new List<RoleInfo>() {
impostor,
cleaner,
warlock,
bountyHunter,
+ witch,
niceMini,
evilMini,
goodGuesser,
jackal,
sidekick,
vulture,
+ pursuer,
+ lawyer,
crewmate,
shifter,
mayor,
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);
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
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;
}
}
__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;
Bait.clearAndReload();
Vulture.clearAndReload();
Medium.clearAndReload();
+ Lawyer.clearAndReload();
+ Pursuer.clearAndReload();
+ Witch.clearAndReload();
}
public static class Jester {
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<PlayerControl> blankedList = new List<PlayerControl>();
+ 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<PlayerControl>();
+ 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<PlayerControl> futureSpelled = new List<PlayerControl>();
+ 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<PlayerControl>();
+ 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
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
- <Version>3.1.2</Version>
+ <Version>3.2.0</Version>
<Description>TheOtherRoles</Description>
<Authors>Eisbison</Authors>
</PropertyGroup>