The **The Other Roles**, is a mod for [Amongs Us](https://store.steampowered.com/app/945360/Among_Us) which adds many new roles, new [Settings](#settings), new [Custom Hats](#custom-hats) and support for [10+ Player Lobbies](#Custom-Servers-and-10+-Players) to the game.\r
Even more roles are coming soon :)\r
\r
-- [Mafia](#mafia)\r
- - Godfather\r
- - Janitor\r
- - Mafioso\r
-- [Morphling](#morphling)\r
-- [Camouflager](#camouflager)\r
-- [Vampire](#vampire)\r
-- [Eraser](#eraser)\r
-- [Trickster](#trickster)\r
-- [Cleaner](#cleaner)\r
-- [Lovers](#lovers)\r
- - Lover\r
- - ImpLover\r
-- [Child](#child)\r
- - Child Impostor\r
- - Child Crewmate\r
-- [Jackal](#jackal)\r
-- [Sidekick](#sidekick)\r
-- [Jester](#jester)\r
-- [Sheriff](#sheriff)\r
-- [Seer](#seer)\r
-- [Engineer](#engineer)\r
-- [Detective](#detective)\r
-- [Lighter](#lighter)\r
-- [Medic](#medic)\r
-- [Mayor](#mayor)\r
-- [Hacker](#hacker)\r
-- [Shifter](#shifter)\r
-- [Time Master](#time-master)\r
-- [Swapper](#swapper)\r
-- [Tracker](#tracker)\r
-- [Snitch](#snitch)\r
-- [Spy](#spy)\r
+| Impostors | Crewmates | Neutral |\r
+|----------|-------------|-----------------|\r
+| [Bad Child](#child) | [Child](#child) | [Jester](#jester) |\r
+| [Camouflager](#camouflager) | [Detective](#detective) | [Jackal](#jackal) |\r
+| [Cleaner](#cleaner) | [Engineer](#engineer) | [Sidekick](#sidekick) |\r
+| [Eraser](#eraser) | [Hacker](#hacker) | |\r
+| [ImpLover](#lovers) | [Lover](#lovers) | |\r
+| [Godfather (Mafia)](#mafia) | [Lighter](#lighter) | |\r
+| [Mafioso (Mafia)](#mafia) | [Mayor](#mayor) | |\r
+| [Janitor (Mafia)](#mafia) | [Medic](#medic) | |\r
+| [Morphling](#morphling) | [Seer](#seer) | |\r
+| [Trickster](#trickster) | [Sheriff](#sheriff) | |\r
+| [Vampire](#vampire) | [Shifter](#shifter) | |\r
+| [Warlock](#warlock) | [Snitch](#snitch) | |\r
+| | [Spy](#spy) | |\r
+| | [Swapper](#swapper) | |\r
+| | [Time Master](#time-master) | |\r
+| | [Tracker](#tracker) | |\r
\r
The [Role Assignment](#role-assignment) sections explains how the roles are being distributed among the players.\r
\r
# Releases\r
| Among Us - Version| Mod Version | Link |\r
|----------|-------------|-----------------|\r
+| 2021.4.14s| v2.4.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.4.0/TheOtherRoles.zip)\r
| 2021.4.14s| v2.3.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.3.0/TheOtherRoles.zip)\r
| 2021.4.14s| v2.2.2| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.2.2/TheOtherRoles.zip)\r
| 2021.4.12s| v2.2.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.2.1/TheOtherRoles.zip)\r
<details>\r
<summary>Click to show the Changelog</summary>\r
\r
+**Version 2.4.0**\r
+- **New Role:** [Warlock](#warlock)\r
+- Added an option that allows ghosts to see the roles and remaining tasks of other players\r
+- Added options to configure Morph & Camo duration\r
+- Added hotkeys to the custom buttons (**Q** for the buttons that are on the same place as the kill button, **F** for the buttons that are above the kill button)\r
+- Fixed an oversight which made StreamerMode only work as host\r
+- Fixed an oversight which required Jackals to finish Tasks, after Sidekick was promoted\r
+- Fixed an oversight which made Sidekicks not promote, if the Jackal disconnected\r
+- Fixed a bug where the Trickster box was invisible\r
+- Fixed a bug where changes to the server ip and port would only be applied if the game was restarted\r
+- Added a way to get the 2 Hidden [Colors](#colors)\r
+\r
**Version 2.3.0**\r
- **New Role:** [Cleaner](#cleaner)\r
- Added 12 new [Colors](#colors)\r
- **Maximum Number Of Meetings:** You can set the maximum number of meetings that can be called in total (every player still has personal maximum of buttons, but if the maximum number of meetings is reached you can't use your meetings even if you have some left, Impostor and Jackal meetings also count) \r
- **Allow Skips On Emergency Meetings:** If set to false, there will not be a skip button in emergency meetings. If a player does not vote, he'll vote himself.\r
- **Hide Player Names:** Hides the names of all players that have role which is unknown to you. Team Lovers/Impostors/Jackal still see the names of their teammates. Impostors can alse see the name of the Spy and everyone can still see the age of the child.\r
+- **Ghosts Can See Roles And Remaining Tasks:** If set to true, ghosts can see the role and the number of remaining tasks of each player.\r
\r
# Custom Hats\r
## Create and submit new hat designs\r
- This is the main texture of your hat. It will usually be rendered in front of the player, if you set the `behind` parameter it will be rendered behind the player.\r
- The name of the texture needs to follow the pattern *hatname.png*, but you can also set some additional parameters in the file name by adding *_parametername* to the file name (before the *.png*).\r
- Parameter `bounce`: This parameter determines whether the hat will bounce while you're walking or not.\r
- - Parameter `adaptive`: If this parameter is set, the Among Us coloring shader will be applied (the shader that replaces some colors with the colors that your character is wearing in the game). The color red (#ff0000) will be replaced with the primary color of your player and the color blue (#0000ff) with the secondary color. Also other colors will be affected and changed, you can have a look at the texture of the [Crewmate Hat](https://static.wikia.nocookie.net/among-us-wiki/images/e/e0/Crewmate_hat.png/revision/latest?cb=20210106045508) to see how this feature should be used.\r
+ - Parameter `adaptive`: If this parameter is set, the Among Us coloring shader will be applied (the shader that replaces some colors with the colors that your character is wearing in the game). The color red (#ff0000) will be replaced with the primary color of your player and the color blue (#0000ff) with the secondary color. Also other colors will be affected and changed, you can have a look at the texture of the [Crewmate Hat](https://static.wikia.nocookie.net/among-us-wiki/images/e/e0/Crewmate_hat.png) to see how this feature should be used.\r
- Parameter `behind`: If this parameter is set, the main texture will be rendered behind the player.\r
- `Back texture (optional)`:\r
- This texture will be rendered behind the player.\r
- **Submission:** If you got a hat design, you can submit it on our [Discord server](https://discord.gg/77RkMJHWsM). We'll look at all the hats and add all the good ones to the game.\r
\r
# Colors\r
-### **Lighter Colors:** \r
-- Pink, Orange, Yellow, White\r
-- Cyan, Lime, Salmon, Mint\r
-- Lavender, Peach, Wasabi, Hot Pink\r
-\r
-\r
-\r
-### **Darker Colors:** \r
-- Red, Blue, Green, Black\r
-- Purple, Brown, Bordeaux, Olive\r
-- Turqoise, Nougat, Gray, Petrol\r
-\r
-\r
+<img src="./Images/Colors.png" alt="drawing" width="300px"/>\r
\r
# Roles\r
\r
|----------|:-------------:|\r
| Morphling Spawn Chance | -\r
| Morphling Cooldown | -\r
+| Morph Duration | Time the Morphling stays morphed\r
-----------------------\r
\r
## Camouflager\r
|----------|:-------------:|\r
| Camouflager Spawn Chance | -\r
| Camouflager Cooldown | -\r
+| Camo Duration | Time players stay camouflaged\r
-----------------------\r
\r
## Vampire\r
**NOTE:**\r
- If a bitten player is still alive when a meeting is being called, he dies at the start of the meeting.\r
- The cooldown is the same as the default kill cooldown (+ the kill delay if the Vampire bites the target).\r
+- If there is a Vampire in the game, there can't be a Warlock\r
\r
### Game Options\r
| Name | Description |\r
| Cleaner Cooldown | Cooldown for cleaning dead bodies\r
-----------------------\r
\r
+\r
+## Warlock\r
+### **Team: Impostors**\r
+The Warlock is an Impostor, that can curse another player (the cursed player doesn't get notified).\\r
+If the cursed person stands next to another player, the Warlock is able to kill that player (no matter how far away he is).\\r
+Performing a kill with the help of a cursed player, will lift the curse and it will result in the Warlock being unable to move for a configurable amount of time.\r
+The Warlock can still perform normal kills, but the two buttons share the same cooldown.\r
+\r
+\\r
+**NOTE:**\r
+- The Warlock can always kill his Impostor mates (and even himselfs) using the "cursed kill"\r
+- If there is a Warlock in the game, there can't be a Vampire\r
+- Performing a normal kill, doesn't lift the curse\r
+\r
+### Game Options\r
+| Name | Description |\r
+|----------|:-------------:|\r
+| Warlock Spawn Chance | -\r
+| Warlock Cooldown | Cooldown for using the Curse and curse Kill\r
+| Warlock Root Time | Time the Warlock is rooted in place after killing using the curse\r
+-----------------------\r
+\r
## Lovers\r
### **Team: Either Crewmates or Impostors**\r
There are always two Lovers which are linked together.\\r
private static CustomButton placeJackInTheBoxButton;
private static CustomButton lightsOutButton;
public static CustomButton cleanerCleanButton;
+ public static CustomButton warlockCurseButton;
public static void setCustomButtonCooldowns() {
engineerRepairButton.MaxTimer = 0f;
placeJackInTheBoxButton.MaxTimer = Trickster.placeBoxCooldown;
lightsOutButton.MaxTimer = Trickster.lightsOutCooldown;
cleanerCleanButton.MaxTimer = Cleaner.cooldown;
+ warlockCurseButton.MaxTimer = Warlock.cooldown;
timeMasterShieldButton.EffectDuration = TimeMaster.shieldDuration;
hackerButton.EffectDuration = Hacker.duration;
() => {},
Engineer.getButtonSprite(),
new Vector3(-1.3f, 0, 0),
- __instance
+ __instance,
+ KeyCode.Q
);
// Janitor Clean
() => { janitorCleanButton.Timer = janitorCleanButton.MaxTimer; },
Janitor.getButtonSprite(),
new Vector3(-1.3f, 0, 0),
- __instance
+ __instance,
+ KeyCode.Q
);
// Sheriff Kill
() => { sheriffKillButton.Timer = sheriffKillButton.MaxTimer;},
__instance.KillButton.renderer.sprite,
new Vector3(-1.3f, 0, 0),
- __instance
+ __instance,
+ KeyCode.Q
);
// Time Master Rewind Time
},
TimeMaster.getButtonSprite(),
new Vector3(-1.3f, 0, 0),
- __instance,
+ __instance,
+ KeyCode.Q,
true,
TimeMaster.shieldDuration,
() => { timeMasterShieldButton.Timer = timeMasterShieldButton.MaxTimer; }
() => {},
Medic.getButtonSprite(),
new Vector3(-1.3f, 0, 0),
- __instance
+ __instance,
+ KeyCode.Q
);
() => { },
Shifter.getButtonSprite(),
new Vector3(-1.3f, 0, 0),
- __instance
+ __instance,
+ KeyCode.Q
);
// Morphling morph
Morphling.sampledTarget = null;
},
Morphling.getSampleSprite(),
- new Vector3(-1.3f, 1.3f, 0f),
+ new Vector3(-1.3f, 1.3f, 0f),
__instance,
+ KeyCode.F,
true,
Morphling.duration,
() => {
camouflagerButton.killButtonManager.TimerText.color = Palette.EnabledColor;
},
Camouflager.getButtonSprite(),
- new Vector3(-1.3f, 1.3f, 0f),
+ new Vector3(-1.3f, 1.3f, 0f),
__instance,
+ KeyCode.F,
true,
Camouflager.duration,
() => { camouflagerButton.Timer = camouflagerButton.MaxTimer; }
Hacker.getButtonSprite(),
new Vector3(-1.3f, 0, 0),
__instance,
+ KeyCode.Q,
true,
0f,
() => {
() => { },
Tracker.getButtonSprite(),
new Vector3(-1.3f, 0, 0),
- __instance
+ __instance,
+ KeyCode.Q
);
vampireKillButton = new CustomButton(
Vampire.getButtonSprite(),
new Vector3(-1.3f, 0, 0),
__instance,
+ KeyCode.Q,
false,
0f,
() => {
Vampire.getGarlicButtonSprite(),
Vector3.zero,
__instance,
+ null,
true
);
() => { jackalSidekickButton.Timer = jackalSidekickButton.MaxTimer;},
Jackal.getSidekickButtonSprite(),
new Vector3(-1.3f, 1.3f, 0f),
- __instance
+ __instance,
+ KeyCode.F
);
// Jackal Kill
() => { jackalKillButton.Timer = jackalKillButton.MaxTimer;},
__instance.KillButton.renderer.sprite,
new Vector3(-1.3f, 0, 0),
- __instance
+ __instance,
+ KeyCode.Q
);
// Sidekick Kill
() => { sidekickKillButton.Timer = sidekickKillButton.MaxTimer;},
__instance.KillButton.renderer.sprite,
new Vector3(-1.3f, 0, 0),
- __instance
+ __instance,
+ KeyCode.Q
);
// Lighter light
Lighter.getButtonSprite(),
new Vector3(-1.3f, 0f, 0f),
__instance,
+ KeyCode.Q,
true,
Lighter.duration,
() => { lighterButton.Timer = lighterButton.MaxTimer; }
() => { eraserButton.Timer = eraserButton.MaxTimer;},
Eraser.getButtonSprite(),
new Vector3(-1.3f, 1.3f, 0f),
- __instance
+ __instance,
+ KeyCode.F
);
placeJackInTheBoxButton = new CustomButton(
() => { placeJackInTheBoxButton.Timer = placeJackInTheBoxButton.MaxTimer;},
Trickster.getPlaceBoxButtonSprite(),
new Vector3(-1.3f, 1.3f, 0f),
- __instance
+ __instance,
+ KeyCode.F
);
lightsOutButton = new CustomButton(
lightsOutButton.killButtonManager.TimerText.color = Palette.EnabledColor;
},
Trickster.getLightsOutButtonSprite(),
- new Vector3(-1.3f, 1.3f, 0f),
+ new Vector3(-1.3f, 1.3f, 0f),
__instance,
+ KeyCode.F,
true,
Trickster.lightsOutDuration,
() => { lightsOutButton.Timer = lightsOutButton.MaxTimer; }
() => { cleanerCleanButton.Timer = cleanerCleanButton.MaxTimer; },
Cleaner.getButtonSprite(),
new Vector3(-1.3f, 1.3f, 0f),
- __instance
+ __instance,
+ KeyCode.F
+ );
+ // Warlock curse
+ warlockCurseButton = new CustomButton(
+ () => {
+ if (Warlock.curseVictim == null) {
+ // Apply Curse
+ Warlock.curseVictim = Warlock.currentTarget;
+ warlockCurseButton.Sprite = Warlock.getCurseKillButtonSprite();
+ warlockCurseButton.Timer = 1f;
+ } else if (Warlock.curseVictim != null && Warlock.curseVictimTarget != null && Helpers.handleMurderAttempt(Warlock.curseVictimTarget)) {
+ // Curse Kill
+ 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);
+
+ 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;
+ PlayerControl.LocalPlayer.NetTransform.Halt(); // Stop current movement so the warlock is not just running straight into the next object
+ HudManager.Instance.StartCoroutine(Effects.Lerp(Warlock.rootTime, new Action<float>((p) => { // Delayed action
+ if (p == 1f) {
+ PlayerControl.LocalPlayer.moveable = true;
+ }
+ })));
+ }
+ }
+ },
+ () => { return Warlock.warlock != null && Warlock.warlock == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; },
+ () => { return ((Warlock.curseVictim == null && Warlock.currentTarget != null) || (Warlock.curseVictim != null && Warlock.curseVictimTarget != null)) && PlayerControl.LocalPlayer.CanMove; },
+ () => {
+ warlockCurseButton.Timer = warlockCurseButton.MaxTimer;
+ warlockCurseButton.Sprite = Warlock.getCurseButtonSprite();
+ Warlock.curseVictim = null;
+ Warlock.curseVictimTarget = null;
+ },
+ Warlock.getCurseButtonSprite(),
+ new Vector3(-1.3f, 1.3f, 0f),
+ __instance,
+ KeyCode.F
);
+
// Set the default (or settings from the previous game) timers/durations when spawning the buttons
setCustomButtonCooldowns();
}
--- /dev/null
+using System;
+using BepInEx;
+using BepInEx.Configuration;
+using BepInEx.IL2CPP;
+using HarmonyLib;
+using UnityEngine;
+using UnhollowerBaseLib;
+
+namespace TheOtherRoles {
+ [HarmonyPatch]
+ public static class ChatCommands {
+ [HarmonyPatch(typeof(ChatController), nameof(ChatController.SendChat))]
+ private static class SendChatPatch {
+ static bool Prefix(ChatController __instance) {
+ string text = __instance.TextArea.text;
+ bool handled = false;
+ if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) {
+ if (text.ToLower().Equals("i'm a panda")) {
+ handled = true;
+ byte colorId = (byte)CustomColors.pickableColors;
+ SaveManager.BodyColor = (byte)colorId;
+ if (PlayerControl.LocalPlayer)
+ PlayerControl.LocalPlayer.CmdCheckColor(colorId);
+ } else if (text.ToLower().Equals("nightsky")) {
+ handled = true;
+ byte colorId = (byte)(CustomColors.pickableColors + 1);
+ SaveManager.BodyColor = (byte)colorId;
+ if (PlayerControl.LocalPlayer)
+ PlayerControl.LocalPlayer.CmdCheckColor(colorId);
+ }
+ }
+ if (handled) {
+ __instance.TextArea.Clear();
+ __instance.quickChatMenu.ResetGlyphs();
+ System.Console.WriteLine("Chat Clear");
+ }
+ return !handled;
+ }
+ }
+ }
+}
\ No newline at end of file
{
[HarmonyPatch]
public static class CredentialsPatch {
- [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))]
+ public static string fullCredentials =
+$@"<color=#FCCE03FF>TheOtherRoles</color> v{TheOtherRolesPlugin.Major}.{TheOtherRolesPlugin.Minor}.{TheOtherRolesPlugin.Patch}:
+- Modded by <color=#FCCE03FF>Eisbison</color>,
+ <color=#FFEB91FF>Thunderstorm584</color> and <color=#FFEB91FF>EndOfFile</color>
+- Balanced with <color=#FFEB91FF>Dhalucard</color>
+- Button design by <color=#FFEB91FF>Bavari</color>";
+ [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))]
private static class VersionShowerPatch
{
static void Postfix(VersionShower __instance) {
string spacer = new String('\n', 8);
- string fullCredentials = $"<color=#FCCE03FF>TheOtherRoles</color> v{TheOtherRolesPlugin.Major}.{TheOtherRolesPlugin.Minor}.{TheOtherRolesPlugin.Patch}:\n- Modded by <color=#FCCE03FF>Eisbison</color> and <color=#FFEB91FF>Thunderstorm584</color>\n- Balanced with <color=#FFEB91FF>Dhalucard</color>\n- Button design by <color=#FFEB91FF>Bavari</color>";
+
if (__instance.text.text.Contains(spacer))
__instance.text.text = __instance.text.text + "\n" + fullCredentials;
else
{
static void Postfix(PingTracker __instance)
{
- __instance.transform.localPosition = new Vector3(__instance.transform.localPosition.x, 2.675f, __instance.transform.localPosition.z);
- if (AmongUsClient.Instance.GameState == InnerNet.InnerNetClient.GameStates.Started)
- __instance.text.text += "<color=#FCCE03FF>TheOtherRoles</color>\nModded by <color=#FCCE03FF>Eisbison</color>";
- else
- __instance.text.text += "\n<color=#FCCE03FF>TheOtherRoles</color>\nModded by <color=#FCCE03FF>Eisbison</color>\nand <color=#FFEB91FF>Thunderstorm584</color>\nBalanced with <color=#FFEB91FF>Dhalucard</color>\nButton design by <color=#FFEB91FF>Bavari</color>";
+ if (AmongUsClient.Instance.GameState == InnerNet.InnerNetClient.GameStates.Started) {
+ __instance.text.text = "<color=#FCCE03FF>TheOtherRoles</color>\nModded by <color=#FCCE03FF>Eisbison</color>\n" + __instance.text.text;
+ __instance.transform.localPosition = new Vector3(2.583f, 2.675f, __instance.transform.localPosition.z);
+ } else {
+ __instance.text.text = $"{fullCredentials}\n{__instance.text.text}";
+ __instance.transform.localPosition = new Vector3(1.25f, 2.675f, __instance.transform.localPosition.z);
+ }
}
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdCheckColor))]
public static class PlayerControlCmdCheckColorPatch {
public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)]byte bodyColor) {
- if (!Helpers.isCustomServer()) return true;
+ if (!Helpers.isCustomServer() || Palette.PlayerColors.Count >= PlayerControl.AllPlayerControls.Count)
+ return true;
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetUncheckedColor, Hazel.SendOption.Reliable, -1);
writer.Write(bodyColor);
[HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.UpdateAvailableColors))]
public static class PlayerTabUpdateAvailableColorsPatch {
public static bool Prefix(PlayerTab __instance) {
- if (!Helpers.isCustomServer()) return true;
+ if (!Helpers.isCustomServer() || Palette.PlayerColors.Count >= PlayerControl.AllPlayerControls.Count)
+ return true;
PlayerControl.SetPlayerMaterialColors(PlayerControl.LocalPlayer.Data.ColorId, __instance.DemoImage);
for (int i = 0; i < Palette.PlayerColors.Length; i++)
return false;
}
}
-}
\ No newline at end of file
+}
public Sprite Sprite;
private HudManager hudManager;
private bool mirror;
+ private KeyCode? hotkey;
- public CustomButton(Action OnClick, Func<bool> HasButton, Func<bool> CouldUse, Action OnMeetingEnds, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, bool HasEffect, float EffectDuration, Action OnEffectEnds, bool mirror = false)
+ public CustomButton(Action OnClick, Func<bool> HasButton, Func<bool> CouldUse, Action OnMeetingEnds, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, KeyCode? hotkey, bool HasEffect, float EffectDuration, Action OnEffectEnds, bool mirror = false)
{
this.hudManager = hudManager;
this.OnClick = OnClick;
this.OnEffectEnds = OnEffectEnds;
this.Sprite = Sprite;
this.mirror = mirror;
+ this.hotkey = hotkey;
Timer = 16.2f;
buttons.Add(this);
killButtonManager = UnityEngine.Object.Instantiate(hudManager.KillButton, hudManager.transform);
PassiveButton button = killButtonManager.GetComponent<PassiveButton>();
button.OnClick.RemoveAllListeners();
- button.OnClick.AddListener((UnityEngine.Events.UnityAction)listener);
+ button.OnClick.AddListener((UnityEngine.Events.UnityAction)onClickEvent);
- void listener()
+ setActive(false);
+ }
+
+ public CustomButton(Action OnClick, Func<bool> HasButton, Func<bool> CouldUse, Action OnMeetingEnds, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, KeyCode? hotkey, bool mirror = false)
+ : this(OnClick, HasButton, CouldUse, OnMeetingEnds, Sprite, PositionOffset, hudManager, hotkey, false, 0f, () => {}, mirror) { }
+
+ void onClickEvent()
+ {
+ if (this.Timer < 0f && HasButton() && CouldUse())
{
- if (this.Timer < 0f && HasButton() && CouldUse())
- {
- killButtonManager.renderer.color = new Color(1f, 1f, 1f, 0.3f);
- this.OnClick();
+ killButtonManager.renderer.color = new Color(1f, 1f, 1f, 0.3f);
+ this.OnClick();
- if (this.HasEffect && !this.isEffectActive) {
- this.Timer = this.EffectDuration;
- killButtonManager.TimerText.color = new Color(0F, 0.8F, 0F);
- this.isEffectActive = true;
- }
+ if (this.HasEffect && !this.isEffectActive) {
+ this.Timer = this.EffectDuration;
+ killButtonManager.TimerText.color = new Color(0F, 0.8F, 0F);
+ this.isEffectActive = true;
}
}
- setActive(false);
}
- public CustomButton(Action OnClick, Func<bool> HasButton, Func<bool> CouldUse, Action OnMeetingEnds, Sprite Sprite, Vector3 PositionOffset, HudManager hudManager, bool mirror = false)
- : this(OnClick, HasButton, CouldUse, OnMeetingEnds, Sprite, PositionOffset, hudManager, false, 0f, () => {}, mirror) { }
-
public static void HudUpdate()
{
buttons.RemoveAll(item => item.killButtonManager == null);
if (Timer >= 0) {
if (HasEffect && isEffectActive)
Timer -= Time.deltaTime;
- else if (!PlayerControl.LocalPlayer.inVent)
+ else if (!PlayerControl.LocalPlayer.inVent && PlayerControl.LocalPlayer.moveable)
Timer -= Time.deltaTime;
}
}
killButtonManager.SetCoolDown(Timer, (HasEffect && isEffectActive) ? EffectDuration : MaxTimer);
+
+ // Trigger OnClickEvent if the hotkey is being pressed down
+ if (hotkey.HasValue && Input.GetKeyDown(hotkey.Value)) onClickEvent();
}
}
\ No newline at end of file
public static CustomOption cleanerSpawnRate;
public static CustomOption cleanerCooldown;
+
+ public static CustomOption warlockSpawnRate;
+ public static CustomOption warlockCooldown;
+ public static CustomOption warlockRootTime;
public static CustomOption maxNumberOfMeetings;
public static CustomOption blockSkippingInEmergencyMeetings;
public static CustomOption noVoteIsSelfVote;
public static CustomOption hidePlayerNames;
+ public static CustomOption showGhostInfo;
+
+ internal static Dictionary<byte, byte[]> blockedRolePairings = new Dictionary<byte, byte[]>();
public static string cs(Color c, string s) {
return string.Format("<color=#{0:X2}{1:X2}{2:X2}{3:X2}>{4}</color>", ToByte(c.r), ToByte(c.g), ToByte(c.b), ToByte(c.a), s);
cleanerSpawnRate = CustomOption.Create(260, cs(Cleaner.color, "Cleaner"), rates, null, true);
cleanerCooldown = CustomOption.Create(261, "Cleaner Cooldown", 30f, 10f, 60f, 2.5f, cleanerSpawnRate);
+ warlockSpawnRate = CustomOption.Create(270, cs(Cleaner.color, "Warlock"), rates, null, true);
+ warlockCooldown = CustomOption.Create(271, "Warlock Cooldown", 30f, 10f, 60f, 2.5f, warlockSpawnRate);
+ warlockRootTime = CustomOption.Create(272, "Warlock Root Time", 5f, 0f, 15f, 1f, warlockSpawnRate);
+
childSpawnRate = CustomOption.Create(180, cs(Child.color, "Child"), rates, null, true);
childGrowingUpDuration = CustomOption.Create(181, "Child Growing Up Duration", 400f, 100f, 1500f, 100f, childSpawnRate);
blockSkippingInEmergencyMeetings = CustomOption.Create(4, "Block Skipping In Emergency Meetings", false);
noVoteIsSelfVote = CustomOption.Create(5, "No Vote Is Self Vote", false, blockSkippingInEmergencyMeetings);
hidePlayerNames = CustomOption.Create(6, "Hide Player Names", false);
+ showGhostInfo = CustomOption.Create(7, "Ghosts Can See Roles And Remaining Tasks", true);
+
+ blockedRolePairings.Add((byte)RoleId.Vampire, new [] { (byte)RoleId.Warlock});
+ blockedRolePairings.Add((byte)RoleId.Warlock, new [] { (byte)RoleId.Vampire});
+
}
}
var hudString = sb.ToString();
int defaultSettingsLines = 19;
- int roleSettingsLines = defaultSettingsLines + 28;
+ int roleSettingsLines = defaultSettingsLines + 29;
int detailedSettingsP1 = roleSettingsLines + 34;
- int detailedSettingsP2 = detailedSettingsP1 + 34;
+ int detailedSettingsP2 = detailedSettingsP1 + 36;
int end1 = hudString.TakeWhile(c => (defaultSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count();
int end2 = hudString.TakeWhile(c => (roleSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count();
int end3 = hudString.TakeWhile(c => (detailedSettingsP1 -= (c == '\n' ? 1 : 0)) > 0).Count();
gap = 4;
index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
hudString = hudString.Insert(index, "\n");
- gap = 12;
+ gap = 13;
index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
hudString = hudString.Insert(index + 1, "\n");
gap = 16;
+++ /dev/null
-using HarmonyLib;
-using static TheOtherRoles.TheOtherRoles;
-using System.Collections;
-using System.Collections.Generic;
-
-namespace TheOtherRoles {
- [HarmonyPatch]
- public static class FakeTasksForEveryone {
- [HarmonyPatch(typeof(GameData), nameof(GameData.RecomputeTaskCounts))]
- private static class GameDataRecomputeTaskCountsPatch {
- private static bool Prefix(GameData __instance) {
- __instance.TotalTasks = 0;
- __instance.CompletedTasks = 0;
- for (int i = 0; i < __instance.AllPlayers.Count; i++) {
- GameData.PlayerInfo playerInfo = __instance.AllPlayers[i]; // PlayerInfo
- if (!playerInfo.Disconnected && playerInfo.Tasks != null && // Disconnected | // Tasks
- playerInfo.Object && // Object -> PlayerControl
- (PlayerControl.GameOptions.GhostsDoTasks || !playerInfo.IsDead) && // GhostsDoTasks | IsDead
- !playerInfo.IsImpostor && // IsImpostor
- !Helpers.hasFakeTasks(playerInfo.Object)
- ) {
-
- for (int j = 0; j < playerInfo.Tasks.Count; j++) {
- __instance.TotalTasks++;
- if (playerInfo.Tasks[j].Complete) { // Complete
- __instance.CompletedTasks++;
- }
- }
- }
- }
- return false;
- }
- }
-
- [HarmonyPatch(typeof(Console), nameof(Console.CanUse))]
- private static class ConsoleCanUsePatch {
- private static bool Prefix(ref float __result, Console __instance, [HarmonyArgument(0)] GameData.PlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) {
- canUse = couldUse = false;
- if (__instance.AllowImpostor) return true;
- if (!Helpers.hasFakeTasks(pc.Object)) return true;
- __result = float.MaxValue;
- return false;
- }
- }
- }
-}
if (AmongUsClient.Instance.AmHost) {
bool blockStart = false;
string message = "";
- foreach (InnerNet.ClientData client in AmongUsClient.Instance.allClients) {
+ foreach (InnerNet.ClientData client in AmongUsClient.Instance.allClients.ToArray()) {
if (client.Character == null) continue;
var dummyComponent = client.Character.GetComponent<DummyBehaviour>();
if (dummyComponent != null && dummyComponent.enabled)
}
}
+ // Lobby code replacement
+ __instance.GameRoomName.text = TheOtherRolesPlugin.StreamerMode.Value ? $"<color={TheOtherRolesPlugin.StreamerModeReplacementColor.Value}>{TheOtherRolesPlugin.StreamerModeReplacementText.Value}</color>" : lobbyCodeText;
+
// Lobby timer
if (!AmongUsClient.Instance.AmHost || !GameData.Instance) return; // Not host or no instance
__instance.PlayerCounter.text = currentText + suffix;
__instance.PlayerCounter.autoSizeTextContainer = true;
- // Lobby code replacement
- __instance.GameRoomName.text = TheOtherRolesPlugin.StreamerMode.Value ? $"<color={TheOtherRolesPlugin.StreamerModeReplacementColor.Value}>{TheOtherRolesPlugin.StreamerModeReplacementText.Value}</color>" : lobbyCodeText;
}
}
return n != StringNames.ServerNA && n != StringNames.ServerEU && n != StringNames.ServerAS;
}
- public static bool hasFakeTasks(PlayerControl player) {
+ public static bool hasFakeTasks(this PlayerControl player) {
return (player == Jester.jester || player == Jackal.jackal || player == Sidekick.sidekick);
}
-
+
+ public static void clearAllTasks(this PlayerControl player) {
+ for (int i = 0; i < player.myTasks.Count; i++) {
+ PlayerTask playerTask = player.myTasks[i];
+ playerTask.OnRemove();
+ UnityEngine.Object.Destroy(playerTask.gameObject);
+ }
+ player.myTasks.Clear();
+ }
+
public static string cs(Color c, string s) {
return string.Format("<color=#{0:X2}{1:X2}{2:X2}{3:X2}>{4}</color>", ToByte(c.r), ToByte(c.g), ToByte(c.b), ToByte(c.a), s);
}
public static System.Collections.Generic.List<JackInTheBox> AllJackInTheBoxes = new System.Collections.Generic.List<JackInTheBox>();
public static int JackInTheBoxLimit = 3;
public static bool boxesConvertedToVents = false;
- private static List<Sprite> boxAnimationSprites;
-
- public static Sprite getBoxSprite() {
- List<Sprite> s = getBoxAnimationSprites();
- if (s.Count > 0) return s.First();
- return null;
- }
-
- public static List<Sprite> getBoxAnimationSprites() {
- if (boxAnimationSprites != null) return boxAnimationSprites;
- boxAnimationSprites = new List<Sprite>();
- for (int i = 1; i < 19; i++) boxAnimationSprites.Add(Helpers.loadSpriteFromResources($"TheOtherRoles.Resources.TricksterAnimation.trickster_box_00{i:00}.png", 175f));
- return boxAnimationSprites;
+ public static Sprite[] boxAnimationSprites = new Sprite[18];
+
+ public static Sprite getBoxAnimationSprite(int index) {
+ if (boxAnimationSprites == null || boxAnimationSprites.Length == 0) return null;
+ index = Mathf.Clamp(index, 0, boxAnimationSprites.Length - 1);
+ if (boxAnimationSprites[index] == null)
+ boxAnimationSprites[index] = (Helpers.loadSpriteFromResources($"TheOtherRoles.Resources.TricksterAnimation.trickster_box_00{(index + 1):00}.png", 175f));
+ return boxAnimationSprites[index];
}
public static void startAnimation(int ventId) {
Vent vent = box.vent;
HudManager.Instance.StartCoroutine(Effects.Lerp(0.6f, new Action<float>((p) => {
- var sprites = getBoxAnimationSprites();
- if (vent != null && vent.myRend != null && sprites != null && sprites.Count > 0) {
- int index = (int)(p * sprites.Count);
- vent.myRend.sprite = sprites[Mathf.Clamp(index, 0, sprites.Count-1)];
- if (p == 1f) vent.myRend.sprite = getBoxSprite();
+ if (vent != null && vent.myRend != null) {
+ vent.myRend.sprite = getBoxAnimationSprite((int)(p * boxAnimationSprites.Length));
+ if (p == 1f) vent.myRend.sprite = getBoxAnimationSprite(0);
}
})));
}
// Create the marker
gameObject.transform.position = position;
var boxRenderer = gameObject.AddComponent<SpriteRenderer>();
- boxRenderer.sprite = getBoxSprite();
+ boxRenderer.sprite = getBoxAnimationSprite(0);
// Create the vent
var referenceVent = UnityEngine.Object.FindObjectOfType<Vent>();
vent.GetComponent<PowerTools.SpriteAnim>()?.Stop();
vent.Id = ShipStatus.Instance.AllVents.Select(x => x.Id).Max() + 1; // Make sure we have a unique id
var ventRenderer = vent.GetComponent<SpriteRenderer>();
- ventRenderer.sprite = getBoxSprite();
+ ventRenderer.sprite = getBoxAnimationSprite(0);
vent.myRend = ventRenderer;
var allVentsList = ShipStatus.Instance.AllVents.ToList();
allVentsList.Add(vent);
public class TheOtherRolesPlugin : BasePlugin
{
public const string Id = "me.eisbison.theotherroles";
- public const string Version = "2.3.0";
+ public const string Version = "2.4.0";
public const byte Major = 2;
- public const byte Minor = 3;
+ public const byte Minor = 4;
public const byte Patch = 0;
public Harmony Harmony { get; } = new Harmony(Id);
public static ConfigEntry<string> Ip { get; set; }
public static ConfigEntry<ushort> Port { get; set; }
- public static DnsRegionInfo CustomRegion;
+ public static IRegionInfo[] defaultRegions;
+
+ public static void UpdateRegions() {
+ ServerManager serverManager = DestroyableSingleton<ServerManager>.Instance;
+ IRegionInfo[] regions = defaultRegions;
+
+ var CustomRegion = new DnsRegionInfo(Ip.Value, "Custom", StringNames.NoTranslation, Ip.Value, Port.Value);
+ regions = regions.Concat(new IRegionInfo[] { CustomRegion.Cast<IRegionInfo>() }).ToArray();
+ ServerManager.DefaultRegions = regions;
+ serverManager.AvailableRegions = regions;
+ }
public override void Load() {
DebugMode = Config.Bind("Custom", "Enable Debug Mode", false);
Ip = Config.Bind("Custom", "Custom Server IP", "127.0.0.1");
Port = Config.Bind("Custom", "Custom Server Port", (ushort)22023);
+ defaultRegions = ServerManager.DefaultRegions;
- CustomRegion = new DnsRegionInfo(Ip.Value, "Custom", StringNames.NoTranslation, Ip.Value, Port.Value);
- ServerManager serverManager = DestroyableSingleton<ServerManager>.Instance;
- IRegionInfo[] regions = ServerManager.DefaultRegions;
-
- regions = regions.Concat(new IRegionInfo[] { CustomRegion.Cast<IRegionInfo>() }).ToArray();
- ServerManager.DefaultRegions = regions;
- serverManager.AvailableRegions = regions;
- serverManager.SaveServers();
+ UpdateRegions();
GameOptionsData.RecommendedImpostors = GameOptionsData.MaxImpostors = Enumerable.Repeat(3, 16).ToArray(); // Max Imp = Recommended Imp = 3
GameOptionsData.MinPlayers = Enumerable.Repeat(4, 15).ToArray(); // Min Players = 4
public static bool blockSkippingInEmergencyMeetings = false;
public static bool noVoteIsSelfVote = false;
public static bool hidePlayerNames = false;
+ public static bool showGhostInfo = true;
// Updating values
public static int meetingsCount = 0;
blockSkippingInEmergencyMeetings = CustomOptionHolder.blockSkippingInEmergencyMeetings.getBool();
noVoteIsSelfVote = CustomOptionHolder.noVoteIsSelfVote.getBool();
hidePlayerNames = CustomOptionHolder.hidePlayerNames.getBool();
+ showGhostInfo = CustomOptionHolder.showGhostInfo.getBool();
}
}
}
\ No newline at end of file
// Update positions
area.Megaphone.transform.localPosition += Vector3.left * 0.1f;
- area.NameText.transform.localPosition = new Vector3(1.2f, 0.2f, area.NameText.transform.localPosition.z);
+ area.NameText.transform.localPosition += new Vector3(0.25f, 0.043f, 0f);
area.transform.localPosition = new Vector3(-3.63f + 2.43f * col, 1.5f - 0.76f * row, -0.9f - 0.01f * row);
}
}
__result = ExileController.Instance.exiled.PlayerName + " was The Trickster.";
else if (Cleaner.cleaner != null && ExileController.Instance.exiled.Object.PlayerId == Cleaner.cleaner.PlayerId)
__result = ExileController.Instance.exiled.PlayerName + " was The Cleaner.";
+ else if (Warlock.warlock != null && ExileController.Instance.exiled.Object.PlayerId == Warlock.warlock.PlayerId)
+ __result = ExileController.Instance.exiled.PlayerName + " was The Warlock.";
}
// Hide number of remaining impostors on Jester win
{
// Helpers
- static PlayerControl setTarget(bool onlyCrewmates = false, bool targetPlayersInVents = false, List<PlayerControl> untargetablePlayers = null) {
+ static PlayerControl setTarget(bool onlyCrewmates = false, bool targetPlayersInVents = false, List<PlayerControl> untargetablePlayers = null, PlayerControl targetingPlayer = null) {
PlayerControl result = null;
float num = GameOptionsData.KillDistances[Mathf.Clamp(PlayerControl.GameOptions.KillDistance, 0, 2)];
if (!ShipStatus.Instance) return result;
+ if (targetingPlayer == null) targetingPlayer = PlayerControl.LocalPlayer;
- Vector2 truePosition = PlayerControl.LocalPlayer.GetTruePosition();
+ Vector2 truePosition = targetingPlayer.GetTruePosition();
Il2CppSystem.Collections.Generic.List<GameData.PlayerInfo> allPlayers = GameData.Instance.AllPlayers;
for (int i = 0; i < allPlayers.Count; i++)
{
GameData.PlayerInfo playerInfo = allPlayers[i];
- if (!playerInfo.Disconnected && playerInfo.PlayerId != PlayerControl.LocalPlayer.PlayerId && !playerInfo.IsDead && (!onlyCrewmates || !playerInfo.IsImpostor))
+ if (!playerInfo.Disconnected && playerInfo.PlayerId != targetingPlayer.PlayerId && !playerInfo.IsDead && (!onlyCrewmates || !playerInfo.IsImpostor))
{
PlayerControl @object = playerInfo.Object;
if(untargetablePlayers != null && untargetablePlayers.Any(x => x == @object)) {
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 || (target == Morphling.morphling && Morphling.morphTarget == Medic.shielded && Morphling.morphTimer > 0f))) {
+ if (Camouflager.camouflageTimer <= 0f && Medic.shielded != null && ((target == Medic.shielded && !isMorphedMorphling) || (isMorphedMorphling && Morphling.morphTarget == Medic.shielded))) {
hasVisibleShield = Medic.showShielded == 0 // Everyone
|| (Medic.showShielded == 1 && (PlayerControl.LocalPlayer == Medic.shielded || PlayerControl.LocalPlayer == Medic.medic)) // Shielded + Medic
|| (Medic.showShielded == 2 && PlayerControl.LocalPlayer == Medic.medic); // Medic only
static void medicSetTarget() {
if (Medic.medic == null || Medic.medic != PlayerControl.LocalPlayer) return;
Medic.currentTarget = setTarget();
- if (!Medic.usedShield) setPlayerOutline(Medic.currentTarget, Medic.color);
+ if (!Medic.usedShield) setPlayerOutline(Medic.currentTarget, Medic.shieldedColor);
}
static void shifterSetTarget() {
}
if(Child.child != null && !Child.isGrownUp()) untargetablePlayers.Add(Child.child); // Exclude Jackal from targeting the Child unless it has grown up
Jackal.currentTarget = setTarget(untargetablePlayers : untargetablePlayers);
- setPlayerOutline(Jackal.currentTarget, Jackal.color);
+ setPlayerOutline(Jackal.currentTarget, Palette.ImpostorRed);
}
static void sidekickSetTarget() {
if(Jackal.jackal != null) untargetablePlayers.Add(Jackal.jackal);
if(Child.child != null && !Child.isGrownUp()) untargetablePlayers.Add(Child.child); // Exclude Sidekick from targeting the Child unless it has grown up
Sidekick.currentTarget = setTarget(untargetablePlayers : untargetablePlayers);
- if (Sidekick.canKill) setPlayerOutline(Sidekick.currentTarget, Sidekick.color);
+ if (Sidekick.canKill) setPlayerOutline(Sidekick.currentTarget, Palette.ImpostorRed);
+ }
+
+ static void sidekickCheckPromotion() {
+ // If LocalPlayer is Sidekick, the Jackal is disconnected and Sidekick promotion is enabled, then trigger promotion
+ if (Sidekick.sidekick == null || Sidekick.sidekick != PlayerControl.LocalPlayer) return;
+ if (Sidekick.sidekick.Data.IsDead == true || !Sidekick.promotesToJackal) return;
+ 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();
+ }
}
static void eraserSetTarget() {
HudManager.Instance.KillButton.SetTarget(target); // Includes setPlayerOutline(target, Palette.ImpstorRed);
}
+ static void warlockSetTarget() {
+ if (Warlock.warlock == null || Warlock.warlock != PlayerControl.LocalPlayer) return;
+ if (Warlock.curseVictim != null && (Warlock.curseVictim.Data.Disconnected || Warlock.curseVictim.Data.IsDead)) {
+ // If the cursed victim is disconnected or dead reset the curse so a new curse can be applied
+ Warlock.resetCurse();
+ }
+ if (Warlock.curseVictim == null) {
+ Warlock.currentTarget = setTarget();
+ setPlayerOutline(Warlock.currentTarget, Warlock.color);
+ } else {
+ Warlock.curseVictimTarget = setTarget(targetingPlayer: Warlock.curseVictim);
+ setPlayerOutline(Warlock.curseVictimTarget, Warlock.color);
+ }
+ }
+
static void trackerUpdate() {
if (Tracker.arrow?.arrow == null) return;
}
}
+ public static void updateGhostInfo() {
+ if (!MapOptions.showGhostInfo) return;
+
+ foreach (PlayerControl p in PlayerControl.AllPlayerControls) {
+ if (p != PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead) continue;
+
+ Transform playerGhostInfoTransform = p.transform.FindChild("GhostInfo");
+ TMPro.TextMeshPro playerGhostInfo = playerGhostInfoTransform != null ? playerGhostInfoTransform.GetComponent<TMPro.TextMeshPro>() : null;
+ if (playerGhostInfo == null) {
+ playerGhostInfo = UnityEngine.Object.Instantiate(p.nameText, p.nameText.transform.parent);
+ playerGhostInfo.transform.localPosition += Vector3.up * 0.25f;
+ playerGhostInfo.fontSize *= 0.75f;
+ playerGhostInfo.gameObject.name = "GhostInfo";
+ }
+
+ PlayerVoteArea playerVoteArea = MeetingHud.Instance?.playerStates?.FirstOrDefault(x => x.TargetPlayerId == p.PlayerId);
+ Transform meetingGhostInfoTransform = playerVoteArea != null ? playerVoteArea.transform.FindChild("GhostInfo") : null;
+ TMPro.TextMeshPro meetingGhostInfo = meetingGhostInfoTransform != null ? meetingGhostInfoTransform.GetComponent<TMPro.TextMeshPro>() : null;
+ if (meetingGhostInfo == null && playerVoteArea != null) {
+ meetingGhostInfo = UnityEngine.Object.Instantiate(playerVoteArea.NameText, playerVoteArea.NameText.transform.parent);
+ meetingGhostInfo.transform.localPosition += Vector3.down * (MeetingHud.Instance.playerStates.Length > 10 ? 0.4f : 0.25f);
+ meetingGhostInfo.fontSize *= 0.75f;
+ meetingGhostInfo.gameObject.name = "GhostInfo";
+ }
+
+ var (tasksCompleted, tasksTotal) = TasksHandler.taskInfo(p.Data);
+ string roleNames = String.Join(", ", RoleInfo.getRoleInfoForPlayer(p).Select(x => Helpers.cs(x.color, x.name)).ToArray());
+ string taskInfo = tasksTotal > 0 ? $"<color=#FAD934FF>({tasksCompleted}/{tasksTotal})</color>" : "";
+ playerGhostInfo.text = $"{roleNames} {taskInfo}".Trim();
+ if (meetingGhostInfo != null) meetingGhostInfo.text = MeetingHud.Instance.state == MeetingHud.VoteStates.Results ? "" : $"{roleNames} {taskInfo}".Trim();
+ }
+ }
+
public static void Postfix(PlayerControl __instance) {
if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return;
// Update Role Description
Helpers.refreshRoleDescription(__instance);
+ // Update Ghost Info
+ updateGhostInfo();
+
// Time Master
bendTimeUpdate();
// Morphling
sidekickSetTarget();
// Impostor
impostorSetTarget();
+ // Warlock
+ warlockSetTarget();
+ // Check for sidekick promotion on Jackal disconnect
+ sidekickCheckPromotion();
}
}
}
if (resetToCrewmate) __instance.Data.IsImpostor = false;
if (resetToDead) __instance.Data.IsDead = true;
+ // Remove fake tasks when player dies
+ if (target.hasFakeTasks())
+ target.clearAllTasks();
+
// Lover suicide trigger on murder
if ((Lovers.lover1 != null && target == Lovers.lover1) || (Lovers.lover2 != null && target == Lovers.lover2)) {
PlayerControl otherLover = target == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1;
// Cleaner Button Sync
if (Cleaner.cleaner != null && PlayerControl.LocalPlayer == Cleaner.cleaner && __instance == Cleaner.cleaner && HudManagerStartPatch.cleanerCleanButton != null)
- HudManagerStartPatch.cleanerCleanButton.Timer = Cleaner.cleaner.killTimer;
+ HudManagerStartPatch.cleanerCleanButton.Timer = Cleaner.cleaner.killTimer;
+
+ // Warlock Button Sync
+ if (Warlock.warlock != null && PlayerControl.LocalPlayer == Warlock.warlock && __instance == Warlock.warlock && HudManagerStartPatch.warlockCurseButton != null) {
+ if(Warlock.warlock.killTimer > HudManagerStartPatch.warlockCurseButton.Timer) {
+ HudManagerStartPatch.warlockCurseButton.Timer = Warlock.warlock.killTimer;
+ }
+ }
// Seer show flash and add dead player position
if (Seer.seer != null && PlayerControl.LocalPlayer == Seer.seer && !Seer.seer.Data.IsDead && Seer.seer != target && Seer.mode <= 1) {
public static void Prefix(KillAnimation __instance, [HarmonyArgument(0)]ref PlayerControl source, [HarmonyArgument(1)]ref PlayerControl target) {
if (Vampire.vampire != null && Vampire.vampire == source && Vampire.bitten != null && Vampire.bitten == target)
source = target;
+
+ if (Warlock.warlock != null && Warlock.warlock == source && Warlock.curseKillTarget != null && Warlock.curseKillTarget == target) {
+ source = target;
+ Warlock.curseKillTarget = null; // Reset here
+ }
}
}
DeadPlayer deadPlayer = new DeadPlayer(__instance, DateTime.UtcNow, DeathReason.Exile, null);
GameHistory.deadPlayers.Add(deadPlayer);
+ // Remove fake tasks when player dies
+ if (__instance.hasFakeTasks())
+ __instance.clearAllTasks();
+
// Lover suicide trigger on exile
if ((Lovers.lover1 != null && __instance == Lovers.lover1) || (Lovers.lover2 != null && __instance == Lovers.lover2)) {
PlayerControl otherLover = __instance == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1;
Eraser,
Spy,
Trickster,
- Cleaner
+ Cleaner,
+ Warlock
}
enum CustomRPC
SetFutureErased,
SetFutureShifted,
PlaceJackInTheBox,
- LightsOut
+ LightsOut,
+ WarlockCurseKill
}
public static class RPCProcedure {
case RoleId.Cleaner:
Cleaner.cleaner = player;
break;
+ case RoleId.Warlock:
+ Warlock.warlock = player;
+ break;
}
}
}
if (player == Eraser.eraser) Eraser.clearAndReload();
if (player == Trickster.trickster) Trickster.clearAndReload();
if (player == Cleaner.cleaner) Cleaner.clearAndReload();
+ if (player == Warlock.warlock) Warlock.clearAndReload();
// Other roles
if (player == Jester.jester) Jester.clearAndReload();
new CustomMessage("Lights are out", Trickster.lightsOutDuration);
}
}
+
+ public static void warlockCurseKill(byte targetId) {
+ foreach (PlayerControl player in PlayerControl.AllPlayerControls) {
+ if (player.PlayerId == targetId) {
+ Warlock.curseKillTarget = player;
+ Warlock.warlock.MurderPlayer(player);
+ return;
+ }
+ }
+ }
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))]
case (byte)CustomRPC.LightsOut:
RPCProcedure.lightsOut();
break;
+ case (byte)CustomRPC.WarlockCurseKill:
+ RPCProcedure.warlockCurseKill(reader.ReadByte());
+ break;
}
}
}
ipField.transform.localPosition = new Vector3(0, -1f, -100f);
ipField.characterLimit = 30;
ipField.AllowSymbols = true;
+ ipField.ForceUppercase = false;
ipField.SetText(TheOtherRolesPlugin.Ip.Value);
__instance.StartCoroutine(Effects.Lerp(0.1f, new Action<float>((p) => {
ipField.outputText.SetText(TheOtherRolesPlugin.Ip.Value);
})));
ipField.ClearOnFocus = false;
- ipField.OnFocusLost = ipField.OnEnter = ipField.OnChange = new Button.ButtonClickedEvent();
- ipField.OnChange.AddListener((UnityAction)ipEvent);
+ ipField.OnEnter = ipField.OnChange = new Button.ButtonClickedEvent();
+ ipField.OnFocusLost = new Button.ButtonClickedEvent();
+ ipField.OnChange.AddListener((UnityAction)onEnterOrIpChange);
+ ipField.OnFocusLost.AddListener((UnityAction)onFocusLost);
- void ipEvent() {
+ void onEnterOrIpChange() {
TheOtherRolesPlugin.Ip.Value = ipField.text;
- TheOtherRolesPlugin.CustomRegion.DefaultIp = ipField.text;
+ }
+
+ void onFocusLost() {
+ TheOtherRolesPlugin.UpdateRegions();
+ __instance.ChooseOption(ServerManager.DefaultRegions[ServerManager.DefaultRegions.Length - 1]);
}
}
if (portField == null || portField.gameObject == null) {
portField.ClearOnFocus = false;
- portField.OnFocusLost = portField.OnEnter = portField.OnChange = new Button.ButtonClickedEvent();
- portField.OnChange.AddListener((UnityAction)portEvent);
+ portField.OnEnter = portField.OnChange = new Button.ButtonClickedEvent();
+ portField.OnFocusLost = new Button.ButtonClickedEvent();
+ portField.OnChange.AddListener((UnityAction)onEnterOrPortFieldChange);
+ portField.OnFocusLost.AddListener((UnityAction)onFocusLost);
- void portEvent() {
+ void onEnterOrPortFieldChange() {
ushort port = 0;
if (ushort.TryParse(portField.text, out port)) {
TheOtherRolesPlugin.Port.Value = port;
- TheOtherRolesPlugin.CustomRegion.Port = port;
portField.outputText.color = Color.white;
} else {
portField.outputText.color = Color.red;
}
}
+
+ void onFocusLost() {
+ TheOtherRolesPlugin.UpdateRegions();
+ __instance.ChooseOption(ServerManager.DefaultRegions[ServerManager.DefaultRegions.Length - 1]);
+ }
}
}
}
impSettings.Add((byte)RoleId.Eraser, CustomOptionHolder.eraserSpawnRate.getSelection());
impSettings.Add((byte)RoleId.Trickster, CustomOptionHolder.tricksterSpawnRate.getSelection());
impSettings.Add((byte)RoleId.Cleaner, CustomOptionHolder.cleanerSpawnRate.getSelection());
+ impSettings.Add((byte)RoleId.Warlock, CustomOptionHolder.warlockSpawnRate.getSelection());
crewSettings.Add((byte)RoleId.Jester, CustomOptionHolder.jesterSpawnRate.getSelection());
crewSettings.Add((byte)RoleId.Mayor, CustomOptionHolder.mayorSpawnRate.getSelection());
List<byte> crewTickets = new List<byte>();
List<byte> impTickets = new List<byte>();
- foreach (KeyValuePair<byte, int> entry in crewSettings) {
+ for (int i = 0; i < crewSettings.Count; i++) {
+ var entry = crewSettings.ElementAt(i);
if (entry.Value == 0) { // Never
} else if (entry.Value == 10) { // Always
if (crewmates.Count > 0 && maxCrewmateRoles > 0) {
setRoleToRandomPlayer(entry.Key, crewmates);
maxCrewmateRoles--;
+ if(CustomOptionHolder.blockedRolePairings.ContainsKey(entry.Key)) {
+ foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[entry.Key]) {
+ if(impSettings.ContainsKey(blockedRoleId)) {
+ impSettings[blockedRoleId] = 0; // Set Chance to Never for that role
+ }
+ if(crewSettings.ContainsKey(blockedRoleId)) {
+ crewSettings[blockedRoleId] = 0; // Set Chance to Never for that role
+ }
+ }
+ }
}
} else { // Other
- for (int i = 0; i < entry.Value; i++) crewTickets.Add(entry.Key);
+ for (int j = 0; j < entry.Value; j++) crewTickets.Add(entry.Key);
}
}
- foreach (KeyValuePair<byte, int> entry in impSettings) {
+ for (int i = 0; i < impSettings.Count; i++) {
+ var entry = impSettings.ElementAt(i);
if (entry.Value == 0) { // Never
} else if (entry.Value == 10) { // Always
if (impostors.Count > 0 && maxImpostorRoles > 0) {
setRoleToRandomPlayer(entry.Key, impostors);
+ if(CustomOptionHolder.blockedRolePairings.ContainsKey(entry.Key)) {
+ foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[entry.Key]) {
+ if(impSettings.ContainsKey(blockedRoleId)) {
+ impSettings[blockedRoleId] = 0; // Set Chance to Never for that role
+ }
+ if(crewSettings.ContainsKey(blockedRoleId)) {
+ crewSettings[blockedRoleId] = 0; // Set Chance to Never for that role
+ }
+ }
+ }
maxImpostorRoles--;
}
} else { // Other
- for (int i = 0; i < entry.Value; i++) impTickets.Add(entry.Key);
+ for (int j = 0; j < entry.Value; j++) impTickets.Add(entry.Key);
}
}
byte roleId = crewTickets[index];
crewTickets.RemoveAll(x => x == roleId);
setRoleToRandomPlayer(roleId, crewmates);
+
+ if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) {
+ foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) {
+ crewTickets.RemoveAll(x => x == blockedRoleId);
+ impTickets.RemoveAll(x => x == blockedRoleId);
+ }
+ }
}
}
byte roleId = impTickets[index];
impTickets.RemoveAll(x => x == roleId);
setRoleToRandomPlayer(roleId, impostors);
+
+ if (CustomOptionHolder.blockedRolePairings.ContainsKey(roleId)) {
+ foreach(var blockedRoleId in CustomOptionHolder.blockedRolePairings[roleId]) {
+ crewTickets.RemoveAll(x => x == blockedRoleId);
+ impTickets.RemoveAll(x => x == blockedRoleId);
+ }
+ }
}
}
}
"Kill everyone and leave no traces",
"Clean up dead bodies"));
}
+ if (Warlock.warlock != null && p == Warlock.warlock) {
+ infos.Add(new RoleInfo("Warlock",
+ Warlock.color,
+ "Curse other players and kill everyone",
+ "Curse and kill everyone"));
+ }
if (Detective.detective != null && p == Detective.detective) {
infos.Add(new RoleInfo("Detective",
Detective.color,
if (Hacker.hacker != null && p == Hacker.hacker) {
infos.Add(new RoleInfo("Hacker",
Hacker.color,
- "Hacke to find the <color=#FF1919FF>Impostors</color>",
- "Hacke to find the Impostors"));
+ "Hack to find the <color=#FF1919FF>Impostors</color>",
+ "Hack to find the Impostors"));
}
if (Child.child != null && p == Child.child) {
infos.Add(new RoleInfo(p.Data.IsImpostor ? "Bad Child" : "Good Child",
return infos;
}
}
-}
\ No newline at end of file
+}
--- /dev/null
+using HarmonyLib;
+using static TheOtherRoles.TheOtherRoles;
+using System.Collections;
+using System.Collections.Generic;
+using System;
+
+namespace TheOtherRoles {
+ [HarmonyPatch]
+ public static class TasksHandler {
+
+ public static Tuple<int, int> taskInfo(GameData.PlayerInfo playerInfo) {
+ int TotalTasks = 0;
+ int CompletedTasks = 0;
+ if (!playerInfo.Disconnected && playerInfo.Tasks != null &&
+ playerInfo.Object &&
+ (PlayerControl.GameOptions.GhostsDoTasks || !playerInfo.IsDead) &&
+ !playerInfo.IsImpostor &&
+ !Helpers.hasFakeTasks(playerInfo.Object)
+ ) {
+
+ for (int j = 0; j < playerInfo.Tasks.Count; j++) {
+ TotalTasks++;
+ if (playerInfo.Tasks[j].Complete) {
+ CompletedTasks++;
+ }
+ }
+ }
+ return Tuple.Create(CompletedTasks, TotalTasks);
+ }
+
+ [HarmonyPatch(typeof(GameData), nameof(GameData.RecomputeTaskCounts))]
+ private static class GameDataRecomputeTaskCountsPatch {
+ private static bool Prefix(GameData __instance) {
+ __instance.TotalTasks = 0;
+ __instance.CompletedTasks = 0;
+ for (int i = 0; i < __instance.AllPlayers.Count; i++) {
+ GameData.PlayerInfo playerInfo = __instance.AllPlayers[i];
+ var (playerCompleted, playerTotal) = taskInfo(playerInfo);
+ __instance.TotalTasks += playerTotal;
+ __instance.CompletedTasks += playerCompleted;
+ }
+ return false;
+ }
+ }
+ }
+}
Spy.clearAndReload();
Trickster.clearAndReload();
Cleaner.clearAndReload();
+ Warlock.clearAndReload();
}
public static class Jester {
}
public static void removeCurrentJackal() {
- if (!formerJackals.Contains(jackal)) formerJackals.Add(jackal);
+ if (jackal != null && !formerJackals.Contains(jackal)) formerJackals.Add(jackal);
jackal = null;
currentTarget = null;
fakeSidekick = null;
cooldown = CustomOptionHolder.janitorCooldown.getFloat();
}
}
+
+ public static class Warlock {
+
+ public static PlayerControl warlock;
+ public static Color color = Palette.ImpostorRed;
+
+ public static PlayerControl currentTarget;
+ public static PlayerControl curseVictim;
+ public static PlayerControl curseVictimTarget;
+ public static PlayerControl curseKillTarget;
+
+ public static float cooldown = 30f;
+ public static float rootTime = 5f;
+
+ private static Sprite curseButtonSprite;
+ private static Sprite curseKillButtonSprite;
+
+ public static Sprite getCurseButtonSprite() {
+ if (curseButtonSprite) return curseButtonSprite;
+ curseButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.CurseButton.png", 115f);
+ return curseButtonSprite;
+ }
+
+ public static Sprite getCurseKillButtonSprite() {
+ if (curseKillButtonSprite) return curseKillButtonSprite;
+ curseKillButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.CurseKillButton.png", 115f);
+ return curseKillButtonSprite;
+ }
+
+ public static void clearAndReload() {
+ warlock = null;
+ currentTarget = null;
+ curseVictim = null;
+ curseVictimTarget = null;
+ curseKillTarget = null;
+ cooldown = CustomOptionHolder.warlockCooldown.getFloat();
+ rootTime = CustomOptionHolder.warlockRootTime.getFloat();
+ }
+
+ public static void resetCurse() {
+ HudManagerStartPatch.warlockCurseButton.Timer = HudManagerStartPatch.warlockCurseButton.MaxTimer;
+ HudManagerStartPatch.warlockCurseButton.Sprite = Warlock.getCurseButtonSprite();
+ HudManagerStartPatch.warlockCurseButton.killButtonManager.TimerText.color = Palette.EnabledColor;
+ currentTarget = null;
+ curseVictim = null;
+ curseVictimTarget = null;
+ curseKillTarget = null;
+ }
+
+ }
+
}
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
- <Version>2.3.0</Version>
+ <Version>2.4.0</Version>
<Description>TheOtherRoles</Description>
<Authors>Eisbison</Authors>
</PropertyGroup>
Morphling.morphling.nameText.text = hidePlayerName(PlayerControl.LocalPlayer, Morphling.morphling) ? "" : Morphling.morphTarget.Data.PlayerName;
Morphling.morphling.myRend.material.SetColor("_BackColor", Palette.ShadowColors[Morphling.morphTarget.Data.ColorId]);
Morphling.morphling.myRend.material.SetColor("_BodyColor", Palette.PlayerColors[Morphling.morphTarget.Data.ColorId]);
- Morphling.morphling.myRend.material.SetFloat("_Outline", Morphling.morphTarget.myRend.material.GetFloat("_Outline"));
- Morphling.morphling.myRend.material.SetColor("_OutlineColor", Morphling.morphTarget.myRend.material.GetColor("_OutlineColor"));
Morphling.morphling.HatRenderer.SetHat(Morphling.morphTarget.Data.HatId, Morphling.morphTarget.Data.ColorId);
Morphling.morphling.nameText.transform.localPosition = new Vector3(0f, (Morphling.morphTarget.Data.HatId == 0U) ? 0.7f : 1.05f, -0.5f);
if (Snitch.snitch == null || Snitch.snitch.Data.IsDead) return;
- int numberOfTasks = 0;
- GameData.PlayerInfo playerInfo = Snitch.snitch.Data;
- if (!playerInfo.Disconnected && playerInfo.Tasks != null) {
- for (int i = 0; i < playerInfo.Tasks.Count; i++) {
- if (!playerInfo.Tasks[i].Complete)
- numberOfTasks++;
- }
- }
+ var (playerCompleted, playerTotal) = TasksHandler.taskInfo(Snitch.snitch.Data);
+ int numberOfTasks = playerTotal - playerCompleted;
if (PlayerControl.LocalPlayer.Data.IsImpostor && numberOfTasks <= Snitch.taskCountForImpostors) {
if (Snitch.localArrows.Count == 0) Snitch.localArrows.Add(new Arrow(Color.blue));
}
}
+
+ [HarmonyPatch(typeof(Console), nameof(Console.CanUse))]
+ public static class ConsoleCanUsePatch {
+ public static bool Prefix(ref float __result, Console __instance, [HarmonyArgument(0)] GameData.PlayerInfo pc, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse) {
+ canUse = couldUse = false;
+ if (Swapper.swapper != null && Swapper.swapper == PlayerControl.LocalPlayer)
+ return !__instance.TaskTypes.Any(x => x == TaskTypes.FixLights || x == TaskTypes.FixComms);
+ if (__instance.AllowImpostor) return true;
+ if (!Helpers.hasFakeTasks(pc.Object)) return true;
+ __result = float.MaxValue;
+ return false;
+ }
+ }
+
+
[HarmonyPatch(typeof(TuneRadioMinigame), nameof(TuneRadioMinigame.Begin))]
class CommsMinigameBeginPatch {
static void Postfix(TuneRadioMinigame __instance) {