diff --git a/Images/TOR_Roles.png b/Images/TOR_Roles.png new file mode 100644 index 000000000..8d3a134fd Binary files /dev/null and b/Images/TOR_Roles.png differ diff --git a/Images/TOR_roles.jpg b/Images/TOR_roles.jpg deleted file mode 100644 index fcbe686cf..000000000 Binary files a/Images/TOR_roles.jpg and /dev/null differ diff --git a/README.md b/README.md index 06697fc76..0440f943c 100644 --- a/README.md +++ b/README.md @@ -5,42 +5,42 @@ This mod is not affiliated with Among Us or Innersloth LLC, and the content contained therein is not endorsed or otherwise sponsored by Innersloth LLC. Portions of the materials contained herein are property of Innersloth LLC. © Innersloth LLC.

[![Discord](./Images/TOR_server.png)](https://discord.gg/77RkMJHWsM) -![eisbison infographic](./Images/TOR_roles.jpg) +![eisbison infographic](./Images/TOR_Roles.png) # The Other Roles -The **The Other Roles**, is a mod for [Among 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. -Even more roles are coming soon :) +The **The Other Roles**, is a mod for [Among Us](https://store.steampowered.com/app/945360/Among_Us) which adds many new roles, new [Settings](#settings) and new [Custom Hats](#custom-hats) to the game. +Even more roles are coming soon. :) -| Impostors | Crewmates | Neutral | Secondary | +| Impostors | Crewmates | Neutral | Modifier | |----------|-------------|-----------------|----------------| -| [Evil Mini](#mini) | [Nice Mini](#mini) | [Arsonist](#arsonist) | [Lover](#lovers) | -| [Evil Guesser](#guesser) | [Nice Guesser](#guesser) | [Jester](#jester) | | -| [Bounty Hunter](#bounty-hunter) | [Detective](#detective) | [Jackal](#jackal) | | -| [Camouflager](#camouflager) | [Engineer](#engineer) | [Sidekick](#sidekick) | | -| [Cleaner](#cleaner) | [Hacker](#hacker) | [Vulture](#vulture) | | -| [Eraser](#eraser) | [Lighter](#lighter) | [Lawyer](#lawyer) | | -| [Godfather (Mafia)](#mafia) | [Mayor](#mayor) | | | -| [Mafioso (Mafia)](#mafia) | [Medic](#medic) | | | -| [Janitor (Mafia)](#mafia) | [Security Guard](#security-guard) | | | -| [Morphling](#morphling) | [Seer](#seer) | | | -| [Trickster](#trickster) | [Sheriff](#sheriff) -| [Vampire](#vampire) | [Deputy](#deputy) | | -| [Warlock](#warlock) | [Shifter](#shifter) | | | -| [Witch](#witch) | [Snitch](#snitch) | | | +| [Godfather (Mafia)](#mafia) | [Shifter](#shifter) | [Jester](#jester) | [Bloody](#bloody) | +| [Mafioso (Mafia)](#mafia) | [Mayor](#mayor) | [Arsonist](#arsonist) | [Anti Teleport](#anti-teleport) | +| [Janitor (Mafia)](#mafia) | [Engineer](#engineer) | [Jackal](#jackal) | [Tie Breaker](#tie-breaker) | +| [Morphling](#morphling) | [Sheriff](#sheriff) | [Sidekick](#sidekick) | [Bait](#bait) | +| [Camouflager](#camouflager) | [Deputy](#deputy) | [Vulture](#vulture) | [Lovers](#lovers) | +| [Vampire](#vampire) | [Lighter](#lighter) | [Lawyer](#lawyer) | [Sunglasses](#sunglasses) | +| [Eraser](#eraser) | [Detective](#detective) | | [Mini](#mini) | +| [Trickster](#trickster) | [Time Master](#time-master) | | [VIP](#vip) | +| [Cleaner](#cleaner) | [Medic](#medic) | | [Invert](#invert) | +| [Warlock](#warlock) | [Swapper](#swapper) | +| [Bounty Hunter](#bounty-hunter) | [Seer](#seer) | | +| [Witch](#witch) | [Hacker](#hacker) | | | +| [Ninja](#ninja) | [Tracker](#tracker) | | | +| [Evil Guesser](#guesser) | [Snitch](#snitch) | | | | | [Spy](#spy) | | | -| | [Swapper](#swapper) | | | -| | [Time Master](#time-master) | | | -| | [Tracker](#tracker) | | | -| | [Bait](#bait) | | -| | [Medium](#medium) | | +| | [Portalmaker](#portalmaker) | | | +| | [Security Guard](#security-guard) | | | +| | [Medium](#medium) | | | +| | [Nice Guesser](#guesser) | | The [Role Assignment](#role-assignment) sections explains how the roles are being distributed among the players. # Releases | Among Us - Version| Mod Version | Link | |----------|-------------|-----------------| +| 2022.3.29| v4.0.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.0.0/TheOtherRoles.zip) | 2022.3.29s| v3.4.5| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v3.4.5/TheOtherRoles.zip) | 2022.2.23s| v3.4.4| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v3.4.4/TheOtherRoles.zip) | 2021.12.15s| v3.4.3| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v3.4.3/TheOtherRoles.zip) @@ -101,6 +101,25 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
Click to show the Changelog +**Version 4.0.0** +- Added new role [Ninja](#ninja) thanks [gendelo3](https://github.com/gendelo3) +- Added new role [Portalmaker](#portalmaker) thanks [gendelo3](https://github.com/gendelo3) +- Added option Shield Last Round First Kill (until the first meeting) +- Added option that medic shield will be set instantly, but shows up after meeting +- Added additions to Swapper (charges mechanic, confirm swap button, UI & color changes in meeting) +- Added option Mayor can always see the vote color (after finishing x-amount of tasks) +- Added possibility to enter a name +- Added map in meeting with last position and list of tasks +- Added Bait, Mini, Lover, Bloody, Sunglasses, Anti Teleport, Tiebreaker & Invert modifier +- Changed lobby/settings ui +- Changed that a sidekicked Spy/Impostor is still shown red to Impostor +- Changed the position of the Guesser to The Neutral Roles (Guesser still remains Imp/Crew when setting up roles) +- Reworked the Lawyer +- Removed Bait, Mini & Lover role +- Fixed a bug with the color of the scream robe +- Fixed a bug where the Jack In The Box size was not properly on Airship +- Fixed a bug where "No vote is self vote" did not work + **Version 3.4.5** - Update to Among Us version 2022.3.29s - Added horse-mode button in the main menu (bottom right) @@ -110,7 +129,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein - Fixed a bug where the killer doesn't teleport to the body - Changed the light source to prevent vanilla bugs - Removed dlekS (for now) :c - + **Version 3.4.4** - Fixed a bug where games were not finished properly on offical servers (special thanks to miniduikboot & 6pak) @@ -595,16 +614,21 @@ docker run -d -p 22023:22023/udp --env IMPOSTOR_AntiCheat__Enabled=false --env I # Settings The mod adds a few new settings to Among Us (in addition to the role settings): - **Streamer Mode:** You can activate the streamer mode in the Among Us settings. It hides the lobby code, the custom server ip and the custom server port. You can set a custom lobby code replacement text, by changing the *Streamer Mode Replacement Text* in the `BepInEx\config\me.eisbison.theotherroles.cfg` file. -- **Number of Impostors:** The number of Impostor count be set inside a lobby -- **Map:** The map can be changed inside a lobby -- **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) +- **Number of Crewmates:** The number of Crewmate roles can be set inside a lobby. +- **Number of Neutrals:** The number of Neutral roles can be set inside a lobby. +- **Number of Impostors:** The number of Impostor roles can be set inside a lobby. +- **Number of Modifiers:** The number of Modifiers can be set inside a lobby. +- **Map:** The map can be changed inside a lobby. +- **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). - **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, they'll vote themself. - **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 also see the name of the Spy and everyone can still see the age of the mini. -- **Allow Parallel MedBay Scans:** Allows players to perform their MedBay scans at the same time +- **Allow Parallel MedBay Scans:** Allows players to perform their MedBay scans at the same time. +- **Shield Last Game First Kill** The first killed player of the previous round will be shielded for all players visible until the first meeting. +- **Play On A Random Map** If enabled it allows you to set a rotation of all current maps, except ehT dlekS - **Ghosts Can See Roles** - **Ghosts Can See Votes** - **Ghosts Can See The Number Of Remaining Tasks** -- **Dleks:** You are now able to select the Dleks map. +- **The map is accessable during a meeting and will show your last location when a body gets reported/meeting gets called** - **Task Counts:** You are now able to select more tasks. - **Role Summary:** When a game ends there will be a list of all players and their roles and their task progress - **Darker/Lighter:** Displays color type of each player in meetings @@ -695,7 +719,7 @@ The Janitor is an Impostor who cannot kill, but they can hide dead bodies instea ## Morphling ### **Team: Impostors** -The Morphling is an Impostor which can additionally scan the appearance of a player. After an arbitrary time they can take on that appearance for 10s. +The Morphling is an Impostor which can additionally scan the appearance of a player. After an arbitrary time they can take on that appearance for 10s.\ \ **NOTE:** - They shrink to the size of the Mini when they copy its look. @@ -715,8 +739,8 @@ The Morphling is an Impostor which can additionally scan the appearance of a pla ## Camouflager ### **Team: Impostors** -The Camouflager is an Impostor which can additionally activate a camouflage mode. -The camouflage mode lasts for 10s and while it is active, all player names/pets/hats +The Camouflager is an Impostor which can additionally activate a camouflage mode.\ +The camouflage mode lasts for 10s and while it is active, all player names/pets/hats\ are hidden and all players have the same color.\ \ **NOTE:** @@ -760,8 +784,8 @@ The Eraser is an Impostor that can erase the role of every player.\ The targeted players will lose their role after the meeting right before a player is exiled.\ After every erase, the cooldown increases by 10 seconds.\ The erase will be performed, even if the Eraser or their target die before the next meeting.\ -By default the Eraser can erase everyone but the Spy and other Impostors. Depending on the options -they can also erase them (Impostors will lose their special Impostor ability). +By default the Eraser can erase everyone but the Spy and other Impostors.\ +Depending on the options they can also erase them (Impostors will lose their special Impostor ability).\ \ **NOTE:** - The Shifter shift will always be triggered before the Erase (hence either the new role of the Shifter will be erased or the Shifter saves the role of their target, depending on whom the Eraser erased) @@ -780,10 +804,9 @@ ex-Lover surviving, as the partnership was erased before. Also a Jester win woul ## Trickster ### **Team: Impostors** -The Trickster is an Impostor that can place 3 jack-in-the-boxes that are invisible at first to other players. \ -If the Trickster has placed all of their boxes they will be converted into a vent network usable only by the Trickster themself, but the boxes are revealed to the others. \ +The Trickster is an Impostor that can place 3 jack-in-the-boxes that are invisible at first to other players.\ +If the Trickster has placed all of their boxes they will be converted into a vent network usable only by the Trickster themself, but the boxes are revealed to the others.\ If the boxes are converted to a vent network, the Trickster gains a new ability "Lights out" to limit the visibility of Non-Impostors, that cannot be fixed by other players. Lights are automatically restored after a while.\ - \ **NOTE:** - Impostors will get a text indicator at the bottom of the screen to notify them if the lights are out due to the Trickster ability, as there is no sabotage arrows or task to sabotage text to otherwise notify them about it. @@ -800,7 +823,6 @@ If the boxes are converted to a vent network, the Trickster gains a new ability ## Cleaner ### **Team: Impostors** The Cleaner is an Impostor who has the ability to clean up dead bodies.\ - \ **NOTE:** - The Kill and Clean cooldown are shared, preventing them from immediately cleaning their own kills. @@ -813,14 +835,12 @@ The Cleaner is an Impostor who has the ability to clean up dead bodies.\ | Cleaner Cooldown | Cooldown for cleaning dead bodies ----------------------- - ## Warlock ### **Team: Impostors** The Warlock is an Impostor, that can curse another player (the cursed player doesn't get notified).\ If the cursed person stands next to another player, the Warlock is able to kill that player (no matter how far away they are).\ 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.\ The Warlock can still perform normal kills, but the two buttons share the same cooldown.\ - \ **NOTE:** - The Warlock can always kill their Impostor mates (and even themself) using the "cursed kill" @@ -835,7 +855,6 @@ The Warlock can still perform normal kills, but the two buttons share the same c | Warlock Root Time | Time the Warlock is rooted in place after killing using the curse ----------------------- - ## Bounty Hunter ### **Team: Impostors** \ @@ -869,12 +888,10 @@ Similar to the Vampire, shields and blanks will be checked twice (at the end of 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).\ If the Witch dies before the meeting starts or if the Witch is being guessed during the meeting, the spellbound players will be highlighted but they'll survive in any case.\ Depending on the options you can choose whether voting the Witch out will save all the spellbound players or not.\ - \ **NOTE:** - The spellbound players will die before the voted player dies (which might trigger e.g. trigger an Impostor win condition, even if the Witch is the one being voted) - ### Game Options | Name | Description | |----------|:-------------:| @@ -887,6 +904,29 @@ Depending on the options you can choose whether voting the Witch out will save a | Voting The Witch Saves All The Targets | If set to true, all the cursed targets will survive at the end of the meeting ----------------------- +## Ninja +### **Team: Impostors** +The Ninja is an Impostor who has the ability to kill another player all over the map.\ +You can mark a player with your ability and by using the ability again, you jump to the position of the marked player and kill it.\ +Depending on the options you know where your marked player is.\ +If the Ninja uses its ability, it will leave a trace (leaves) for a configurable amount of time where it activated the ability and additionally where it killed the before marked player.\ +\ +**NOTE:** +- The Ninja has a 5 second cooldown after marking a player +- The trace has a darker (black) or lighter (white) color depending on the players color that will fade into green +- The mark on the marked player will reset after a meeting or after using the ability to kill the marked player. Performing a normal kill will **NOT** reset the mark +- If the Ninja tries to kill a shielded player (e.g. Medic shield, Shield last game first kill ), the kill will not be performed +- If the Ninja tries to kill the Time Master while the shield is active, the Ninja won't teleport to the players position, but the Time Master shield will still be activated + +### Game Options +| Name | Description | +|----------|:-------------:| +| Ninja Spawn Chance | - +| Ninja Mark Cooldown | - +| Ninja Knows Location Of Target | - +| Trace Duration | - +| Time Till Trace Color Has Faded | - +----------------------- ## Guesser ### **Team: Crewmates or Impostors** @@ -918,126 +958,198 @@ Depending on the options, the Guesser can't guess the shielded player and depend ----------------------- -## Lovers -### **Team: Lovers (and secondary team)** -There are always two Lovers which are linked together.\ -Their primary goal is it to stay alive together until the end of the game.\ -If one Lover dies (and the option is activated), the other Lover suicides.\ -You can select if Lovers are able to have a second role (could be a Neutral, Crewmate or Impostor Role)\ -You can specify the chance of one Lover being an Impostor.\ -The Lovers never know the role of their partner, they only see who their partner is.\ -The Lovers win, if they are both alive when the game ends. They can also win with their original team (e.g. a dead Impostor Lover can win with the Impostors, an Arsonist Lover can still achieve an Arsonist win)\ -If one of the Lovers is a killer (i.e. Jackal/Sidekick/Impostor), they can achieve a "Lovers solo win" where only the Lovers win.\ -If there is no killer among the Lovers (e.g. an Arsonist Lover + Crewmate Lover) and they are both alive when the game ends, they win together with the Crewmates.\ -If there's a team Impostor/Jackal Lover in the game, the tasks of a Crewmate Lover won't be counted (for a task win) as long as they're alive. If the Lover dies, their tasks will also be counted.\ -You can enable an exclusive chat only for Lovers\ -\ -**NOTE:** -- In a 2 Cremates vs 2 Impostors (or 2 members of team Jackal) and the Lovers are not in the same team, the game is not automatically over since the Lovers can still achieve a solo win. E.g. if there are the following roles Impostor + ImpLover + Lover + Crewmate left, the game will not end and the next kill will decide if the Impostors or Lovers win. -- The Lovers can change if the Shifter takes the role of a Lovers +## Jester +### **Team: Neutral** +The Jester does not have any tasks. They win the game as a solo, if they get voted out during a meeting. ### Game Options | Name | Description | |----------|:-------------:| -| Lovers Spawn Chance | - -| Chance That One Lover Is Impostor | - -| Both Lovers Die | Whether the second Lover suicides, if the first one dies -| Lovers Can Have Another Role | If set to true, the Lovers can have a second role -| Enable Lover Chat | - +| Jester Spawn Chance | - +| Jester Can Call Emergency Meeting | Option to disable the emergency button for the Jester ----------------------- +## Arsonist +### **Team: Neutral** +The Arsonist does not have any tasks, they have to win the game as a solo.\ +The Arsonist can douse other players by pressing the douse button and remaining next to the player for a few seconds.\ +If the player that the Arsonist douses walks out of range, the cooldown will reset to 0.\ +After dousing everyone alive the Arsonist can ignite all the players which results in an Arsonist win. -## Sheriff -### **Team: Crewmates** -The Sheriff has the ability to kill Impostors. -If they try to kill a Crewmate, they die instead. +### Game Options +| Name | Description | +|----------|:-------------:| +| Arsonist Spawn Chance | - +| Arsonist Countdown | - +| Arsonist Douse Duration | The time it takes to douse a player +----------------------- + +## Jackal +### **Team: Jackal** +The Jackal is part of an extra team, that tries to eliminate all the other players.\ +The Jackal has no tasks and can kill Impostors, Crewmates and Neutrals.\ +The Jackal (if allowed by the options) can select another player to be their Sidekick. +Creating a Sidekick removes all tasks of the Sidekick and adds them to the team Jackal. The Sidekick loses their current role (except if they're a Lover, then they play in two teams). +The "Create Sidekick Action" may only be used once per Jackal or once per game (depending on the options). +The Jackal can also promote Impostors to be their Sidekick, but depending on the options the Impostor will either really turn into the Sidekick and leave the team Impostors or they will just look like the Sidekick to the Jackal and remain as they were.\ +Also if a Spy or Impostor gets sidekicked, they still will appear red to the Impostors. + +The team Jackal enables multiple new outcomes of the game, listing some examples here: +- The Impostors could be eliminated and then the crew plays against the team Jackal. +- The Crew could be eliminated, then the Team Jackal fight against the Impostors (The Crew can still make a task win in this scenario) + +The priority of the win conditions is the following: +1. Crewmate Mini lose by vote +2. Jester wins by vote +3. Arsonist win +4. Team Impostor wins by sabotage +5. Team Crew wins by tasks (also possible if the whole Crew is dead) +6. Lovers among the last three players win +7. Team Jackal wins by outnumbering (When the team Jackal contains an equal or greater amount of players than the Crew and there are 0 Impostors left and team Jackal contains no Lover) +8. Team Impostor wins by outnumbering (When the team Impostors contains an equal or greater amount of players than the Crew and there are 0 players of the team Jackal left and team Impostors contains no Lover) +9. Team Crew wins by outnumbering (When there is no player of the team Jackal and the team Impostors left) **NOTE:** -- If the Sheriff shoots the person the Medic shielded, the Sheriff and the shielded person **both remain unharmed**. -- If the Sheriff shoots a Mini Impostor, the Sheriff dies if the Mini is still growing up. If it's 18, the Mini Impostor dies. +- The Jackal (and their Sidekick) may be killed by a Sheriff. +- A Jackal cannot target the Mini, while it's growing up. After that they can kill it or select it as its Sidekick. +- The Crew can still win, even if all of their members are dead, if they finish their tasks fast enough (That's why converting the last Crewmate with tasks left into a Sidekick results in a task win for the crew.) + +If both Impostors and Jackals are in the game, the game continues even if all Crewmates are dead. Crewmates may still win in this case by completing their tasks. Jackal and Impostor have to kill each other. ### Game Options -| Name | Description | +| Name | Description |----------|:-------------:| -| Sheriff Spawn Chance | - -| Sheriff Cooldown | - -| Sheriff Can Kill Neutrals | - -| Sheriff Has A Deputy | Deputy can not be in game without Sheriff +| Jackal Spawn Chance | - | +| Jackal/Sidekick Kill Cooldown | Kill cooldown | +| Jackal Create Sidekick Cooldown | Cooldown before a Sidekick can be created | +| Jackal can use vents | Yes/No | +| Jackal can create a Sidekick | Yes/No | +| Jackals promoted from Sidekick can create a Sidekick | Yes/No (to prevent the Jackal team from growing) | +| Jackals can make an Impostor to their Sidekick | Yes/No (to prevent a Jackal from turning an Impostor into a Sidekick, if they use the ability on an Impostor they see the Impostor as Sidekick, but the Impostor isn't converted to Sidekick. If this option is set to "No" Jackal and Sidekick can kill each other.) | +| Jackal and Sidekick have Impostor vision | - | ----------------------- -## Deputy -### **Team: Crewmates** -The Deputy has the ability to handcuff player. -Handcuffs will be hidden until the handcuffed player try to use a disabled button/hotkey. -Handcuffs disable: -- Kill -- Abilities -- Vent -- Report - +## Sidekick +### **Team: Jackal** +Gets assigned to a player during the game by the "Create Sidekick Action" of the Jackal and joins the Jackal in their quest to eliminate all other players.\ +Upon the death of the Jackal (depending on the options), they might get promoted to Jackal themself and potentially even assign a Sidekick of their own.\ +\ **NOTE:** -- Duration starts after the handcuffs become visible. -- Deputy can not be in game without Sheriff. +- A player that converts into a Sidekick loses their previous role and tasks (if they had one). +- The Sidekick may be killed by a Sheriff. +- The Sidekick cannot target the Mini, while it's growing up. ### Game Options -| Name | Description | +| Name | Description |----------|:-------------:| -| Deputy Number Of Handcuffs | - -| Handcuff Cooldown| - -| Handcuff Duration | - -| Sheriff And Deputy Know Each Other | - -| Deputy Gets Promoted To Sheriff | "Off", "On (Immediately)" or "On (After Meeting)" -| Deputy Keeps Handcuffs When Promoted |- +| Jackal/Sidekick Kill Cooldown | Uses the same kill cooldown setting as the Jackal | +| Sidekick gets promoted to Jackal on Jackal death | Yes/No | +| Sidekick can kill | Yes/No | +| Sidekick can use vents | Yes/No | ----------------------- -## Jester +## Vulture ### **Team: Neutral** -The Jester does not have any tasks. They win the game as a solo, if they get voted out during a meeting. + +The Vulture does not have any tasks, they have to win the game as a solo.\ +The Vulture is a neutral role that must eat a specified number of corpses (depending on the options) in order to win.\ +Depending on the options, when a player dies, the Vulture gets an arrow pointing to the corpse.\ +If there is a Vulture in the game, there can't be a Cleaner. ### Game Options | Name | Description | |----------|:-------------:| -| Jester Spawn Chance | - -| Jester Can Call Emergency Meeting | Option to disable the emergency button for the Jester +| Vulture Spawn Chance | - +| Vulture Countdown | - +| Number Of Corpses Needed To Be Eaten | Corpes needed to be eaten to win the game +| Vulture Can Use Vents | - +| Show Arrows Pointing Towards The Corpes | - ----------------------- -## Arsonist +## Lawyer ### **Team: Neutral** -The Arsonist does not have any tasks, they have to win the game as a solo.\ -The Arsonist can douse other players by pressing the douse button and remaining next to the player for a few seconds.\ -If the player that the Arsonist douses walks out of range, the cooldown will reset to 0.\ -After dousing everyone alive the Arsonist can ignite all the players which results in an Arsonist win. +The Lawyer is a neutral role that has a client.\ +The client might be an Impostor or Jackal which is no Lover.\ +Depending on the options, the client can also be a Jester.\ +The Lawyer needs their client to win in order to win the game.\ +Their client doesn't know that it is their client.\ +If their client gets voted out, the Lawyer dies with the client.\ +If their client dies, the Lawyer changes their role and becomes the [Pursuer](#pursuer), which has a different goal to win the game.\ +\ +How the Lawyer wins: +- Lawyer dead/alive, client alive and client wins: The Lawyer wins together with the team of the client. +- If their client is Jester and the Jester gets voted out, the Lawyer wins together with the Jester. + +**NOTE:** +- If the client disconnects, the Lawyer will also turn into the Pursuer +- The Lawyer needs to figure out the role of their client depending on the options ### Game Options | Name | Description | |----------|:-------------:| -| Arsonist Spawn Chance | - -| Arsonist Countdown | - -| Arsonist Douse Duration | The time it takes to douse a player +| Lawyer Spawn Chance | - +| Lawyer Target Can Be The Jester | - +| 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 +| Lawyer Knows Target Role | - +| Pursuer Blank Cooldown | - +| Pursuer Number Of Blanks | - ----------------------- -## Seer +## Pursuer +### **Team: Neutral** +The Pursuer is still a neutral role, but has a different goal to win the game; they have to be alive when the game ends and the Crew wins.\ +In order to achieve this goal, the Pursuer has an ability called "Blank", where they 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 their target, and their cooldowns will be triggered as usual.\ +If the killer fires the "Blank", shields (e.g. Medic shield or Time Master shield) will not 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, their tasks won't be counted anymore. + +----------------------- + +## Shifter ### **Team: Crewmates** -The Seer has two abilities (one can activate one of them or both in the options). -The Seer sees the souls of players that died a round earlier, the souls slowly fade away. -The Seer gets a blue flash on their screen, if a player dies somewhere on the map. +The Shifter can take over the role of another Crewmate, the other player will transform into a Crewmate.\ +The Shift will always be performed at the end of the next meeting right before a player is exiled. The target needs to be chosen during the round.\ +Even if the Shifter or the target dies before the meeting, the Shift will still be performed.\ +Swapping roles with an Impostor or Neutral fails and the Shifter commits suicide after the next meeting (there won't be any body).\ +The Shifter aims to save roles from leaving the game, by e.g. taking over a Sheriff or Medic that is known to the Impostors.\ +This works especially well against the Eraser, but also gives the Eraser the possibility to act like a Shifter.\ +The **special interactions** with the Shifter are noted in the chapters of the respective roles.\ +\ +**NOTE:** +- The Shifter shift will always be triggered before the Erase (hence either the new role of the Shifter will be erased or the Shifter saves the role of their target, depending on whom the Eraser erased) +- If the Shifter takes over a role, their new cooldowns will start at the maximum cooldown of the ability +- One time use abilities (e.g. shielding a player or Engineer sabotage fix) can only used by one player in the game (i.e. the Shifter +can only use them, if the previous player did not use them before) + +### Game Options +| Name | Description +|----------|:-------------:| +| Shifter Spawn Chance | - +| Shifter Shifts Modifiers | Sets if Lovers and/or Medic Shield will be shifted +----------------------- + +## Mayor +### **Team: Crewmates** +The Mayor leads the Crewmates by having a vote that counts twice.\ +The Mayor can always use their meeting, even if the maximum number of meetings was reached.\ +The Mayor can see the vote colors after completing a configurable amount of tasks, depending on the options. ### Game Options | Name | Description | |----------|:-------------:| -| Seer Spawn Chance | - -| Seer Mode | Options: Show death flash and souls, show death flash, show souls -| Seer Limit Soul Duration | Toggle if souls should turn invisible after a while -| Seer Soul Duration | Sets how long it will take the souls to turn invisible after a meeting +| Mayor Spawn Chance | - +| Mayor Can See Vote Colors | - +| Mayor Completed Tasks Needed To See Vote Colors | - ----------------------- ## Engineer ### **Team: Crewmates** The Engineer (if alive) can fix a certain amount of sabotages per game from anywhere on the map.\ The Engineer can use vents.\ -If the Engineer is inside a vent, depending on the options the members of the team Jackal/Impostors will see a blue outline around all vents on the map (in order to warn them). -Because of the vents the Engineer might not be able to start some tasks using the "Use" button, you can double-click on the tasks instead.\ -\ +If the Engineer is inside a vent, depending on the options the members of the team Jackal/Impostors will see a blue outline around all vents on the map (in order to warn them).\ +Because of the vents the Engineer might not be able to start some tasks using the "Use" button, you can double-click on the tasks instead. + **NOTE:** - The kill button of Impostors activates if they stand next to a vent where the Engineer is. They can also kill them there. No other action (e.g. Morphling sample, Shifter shift, ...) can affect players inside vents. @@ -1050,26 +1162,47 @@ Because of the vents the Engineer might not be able to start some tasks using th | Jackal and Sidekick See Vents Highlighted | - ----------------------- -## Detective +## Sheriff ### **Team: Crewmates** -The Detective can see footprints that other players leave behind. -The Detective's other feature shows when they report a corpse: they receive clues about the killer's identity. The type of information they get is based on the time it took them to find the corpse. +The Sheriff has the ability to kill Impostors.\ +If they try to kill a Crewmate, they die instead.\ \ **NOTE:** -- When people change their colors (because of a morph or camouflage), all the footprints also change their colors (also the ones that were already on the ground). If the effects are over, all footprints switch back to the original color. -- The Detective does not see footprints of players that sit in vents -- More information about the [colors](#colors) -- During the meetings you can see, whether a player wears a darker or a lighter color, represented by (D) or (L) in the names. +- If the Sheriff shoots the person the Medic shielded, the Sheriff and the shielded person **both remain unharmed**. +- If the Sheriff shoots a Mini Impostor, the Sheriff dies if the Mini is still growing up. If it's 18, the Mini Impostor dies. ### Game Options | Name | Description | |----------|:-------------:| -| Detective Spawn Chance | - -| Anonymous Footprints | If set to true, all footprints will have the same color. Otherwise they will have the color of the respective player. -| Footprint Interval | The interval between two footprints -| Footprint Duration | Sets how long the footprints remain visible. -| Time Where Detective Reports Will Have Name | The amount of time that the Detective will have to report the body since death to get the killer's name. | -| Time Where Detective Reports Will Have Color Type| The amount of time that the Detective will have to report the body since death to get the killer's color type. | +| Sheriff Spawn Chance | - +| Sheriff Cooldown | - +| Sheriff Can Kill Neutrals | - +| Sheriff Has A Deputy | Deputy can not be in game without Sheriff +----------------------- + +## Deputy +### **Team: Crewmates** +The Deputy has the ability to handcuff player.\ +Handcuffs will be hidden until the handcuffed player try to use a disabled button/hotkey.\ +Handcuffs disable: +- Kill +- Abilities +- Vent +- Report\ +\ +**NOTE:** +- Duration starts after the handcuffs become visible. +- Deputy can not be in game without Sheriff. + +### Game Options +| Name | Description | +|----------|:-------------:| +| Deputy Number Of Handcuffs | - +| Handcuff Cooldown| - +| Handcuff Duration | - +| Sheriff And Deputy Know Each Other | - +| Deputy Gets Promoted To Sheriff | "Off", "On (Immediately)" or "On (After Meeting)" +| Deputy Keeps Handcuffs When Promoted |- ----------------------- ## Lighter @@ -1086,80 +1219,125 @@ The Lighter can turn on their Lighter every now and then, which increases their | Lighter Duration | - ----------------------- -## Mini -### **Team: Crewmates or Impostors** -The Mini can be a Crewmate (67% chance) or an Impostor (33% chance).\ -The Mini's character is smaller and hence visible to everyone in the game.\ -The Mini cannot be killed until it turns 18 years old, however it can be voted out.\ -**Impostor Mini:** - - While growing up the kill cooldown is doubled. When it's fully grown up its kill cooldown is 2/3 of the default one. - - If it gets thrown out of the ship, everything is fine. - -**Crewmate Mini:** - - The Crewmate Mini aims to play out the strength its invincibility in the early game. - - If it gets thrown out of the ship before it turns 18, everyone loses. So think twice before you vote out a Mini. +## Detective +### **Team: Crewmates** +The Detective can see footprints that other players leave behind.\ +The Detective's other feature shows when they report a corpse: they receive clues about the killer's identity. The type of information they get is based on the time it took them to find the corpse. **NOTE:** -- If the Sheriff tries to kill the Mini before it's fully grown, they die, no matter if the Mini is a Crewmate or Impostor -- The Sheriff can kill the Impostor Mini, but only if it's fully grown up +- When people change their colors (because of a morph or camouflage), all the footprints also change their colors (also the ones that were already on the ground). If the effects are over, all footprints switch back to the original color. +- The Detective does not see footprints of players that sit in vents +- More information about the [colors](#colors) ### Game Options | Name | Description | |----------|:-------------:| -| Mini Spawn Chance | - -| Mini | Mini Growing Up Duration +| Detective Spawn Chance | - +| Anonymous Footprints | If set to true, all footprints will have the same color. Otherwise they will have the color of the respective player. +| Footprint Interval | The interval between two footprints +| Footprint Duration | Sets how long the footprints remain visible. +| Time Where Detective Reports Will Have Name | The amount of time that the Detective will have to report the body since death to get the killer's name. | +| Time Where Detective Reports Will Have Color Type| The amount of time that the Detective will have to report the body since death to get the killer's color type. | ----------------------- -## Medic +## Time Master ### **Team: Crewmates** -The Medic can shield (highlighted by an outline around the player) one player per game, which makes the player unkillable.\ -The shielded player can still be voted out and might also be an Impostor.\ -If set in the options, the shielded player and/or the Medic will get a red flash on their screen if someone (Impostor, Sheriff, ...) tried to murder them. +The Time Master has a time shield which they can activate. The time shield remains active for a configurable amount of time.\ +If a player tries to kill the Time Master while the time shield is active, the kill won't happen and the +time will rewind for a set amount of time.\ +The kill cooldown of the killer won't be reset, so the Time Master +has to make sure that the game won't result in the same situation.\ +The Time Master won't be affected by the rewind. + +**NOTE:** +- Only the movement is affected by the rewind. +- A Vampire bite will trigger the rewind. If the Time Master misses shielding the bite, they can still shield the kill which happens a few seconds later. +- If the Time Master was bitten and has their shield active before when a meeting is called, they survive but the time won't be rewound. +- If the Time Master has a Medic shield, they won't rewind. +- The shield itself ends immediately when triggered. So the Time Master can be attacked again as soon as the rewind ends. + +### Game Options +| Name | Description | +|----------|:-------------:| +| Time Master Spawn Chance | - | +| Time Master Cooldown | - | +| Rewind Duration | How much time to rewind | +| Time Master Shield Duration | +----------------------- + +## Medic +### **Team: Crewmates** +The Medic can shield (highlighted by an outline around the player) one player per game, which makes the player unkillable.\ +The shielded player can still be voted out and might also be an Impostor.\ +If set in the options, the shielded player and/or the Medic will get a red flash on their screen if someone (Impostor, Sheriff, ...) tried to murder them.\ If the Medic dies, the shield disappears with them.\ The Sheriff will not die if they try to kill a shielded Crewmate and won't perform a kill if they try to kill a shielded Impostor.\ Depending on the options, guesses from the Guesser will be blocked by the shield and the shielded player/medic might be notified.\ The Medic's other feature shows when they report a corpse: they will see how long ago the player died. -\ + **NOTE:** - If the shielded player is a Lover and the other Lover dies, they nevertheless kill themselves. - If the Shifter has a shield or their target has a Shield, the shielded player switches. - Shields set after the next meeting, will be set before a possible shift is being performed. - ### Game Options | Name | Description | Options | |----------|:-------------:|:-------------:| | Medic Spawn Chance | - | - | Show Shielded Player | Sets who sees if a player has a shield | "Everyone", "Shielded + Medic", "Medic" | Shielded Player Sees Murder Attempt| Whether a shielded player sees if someone tries to kill them | True/false | -| Shield Will Be Set After Next Meeting | - | True/false +| Shield Will Be Activated | Sets when the shield will be active | "Instantly", "Instantly, Visible After Meeting", "After Meeting" | Medic Sees Murder Attempt On Shielded Player | - | If anyone tries to harm the shielded player (Impostor, Sheriff, Guesser, ...), the Medic will see a red flash ----------------------- -## Mayor +## Swapper ### **Team: Crewmates** -The Mayor leads the Crewmates by having a vote that counts twice.\ -The Mayor can always use their meeting, even if the maximum number of meetings was reached. +During meetings the Swapper can exchange votes that two people get (i.e. all votes +that player A got will be given to player B and vice versa).\ +Because of the Swapper's strength in meetings, they might not start emergency meetings and can't fix lights and comms.\ +The Swapper now has initial swap charges and can recharge those charges after completing a configurable amount of tasks.\ +\ +**NOTE:** +- The remaining charges will be displayed in brackets next to the players role while not in a meeting +- In a meeting the charges will appear next to the Confirm Swap button + +### Game Options +| Name | Description +|----------|:-------------:| +| Swapper Spawn Chance | - +| Swapper can call emergency meeting | Option to disable the emergency button for the Swapper +| Swapper can only swap others | Sets whether the Swapper can swap themself or not +| Initial Swap Charges | - +| Number Of Tasks Needed For Recharging | - +----------------------- + +## Seer +### **Team: Crewmates** +The Seer has two abilities (one can activate one of them or both in the options).\ +The Seer sees the souls of players that died a round earlier, the souls slowly fade away.\ +The Seer gets a blue flash on their screen, if a player dies somewhere on the map. ### Game Options | Name | Description | |----------|:-------------:| -| Mayor Spawn Chance | - +| Seer Spawn Chance | - +| Seer Mode | Options: Show death flash and souls, show death flash, show souls +| Seer Limit Soul Duration | Toggle if souls should turn invisible after a while +| Seer Soul Duration | Sets how long it will take the souls to turn invisible after a meeting ----------------------- ## Hacker ### **Team: Crewmates** If the Hacker activates the "Hacker mode", the Hacker gets more information than others from the admin table and vitals for a set duration.\ -Otherwise they see the same information as everyone else. +Otherwise they see the same information as everyone else.\ **Admin table:** The Hacker can see the colors (or color types) of the players on the table.\ **Vitals**: The Hacker can see how long dead players have been dead for.\ The Hacker can access his mobile gadgets (vitals & admin table), with a maximum of charges (uses) and a configurable amount of tasks needed to recharge.\ -While accessing those mobile gadgets, the Hacker is not able to move.\ -\ +While accessing those mobile gadgets, the Hacker is not able to move. + **NOTE:** - If the Morphling morphs or the Camouflager camouflages, the colors on the admin table change accordingly - More information about the [colors](#colors) -- During the meetings you can see, whether a player wears a darker or a lighter color, represented by (D) or (L) in the names. ### Game Options | Name | Description | @@ -1173,75 +1351,11 @@ While accessing those mobile gadgets, the Hacker is not able to move.\ | Can't Move During Cam Duration | - ----------------------- - -## Shifter -### **Team: Crewmates** -The Shifter can take over the role of another Crewmate, the other player will transform into a Crewmate.\ -The Shift will always be performed at the end of the next meeting right before a player is exiled. The target needs to be chosen during the round.\ -Even if the Shifter or the target dies before the meeting, the Shift will still be performed.\ -Swapping roles with an Impostor or Neutral fails and the Shifter commits suicide after the next meeting (there won't be any body).\ -The Shifter aims to save roles from leaving the game, by e.g. taking over a Sheriff or Medic that is known to the Impostors.\ -This works especially well against the Eraser, but also gives the Eraser the possibility to act like a Shifter.\ -The **special interactions** with the Shifter are noted in the chapters of the respective roles.\ -\ -**NOTE:** -- The Shifter shift will always be triggered before the Erase (hence either the new role of the Shifter will be erased or the Shifter saves the role of their target, depending on whom the Eraser erased) -- If the Shifter takes over a role, their new cooldowns will start at the maximum cooldown of the ability -- One time use abilities (e.g. shielding a player or Engineer sabotage fix) can only used by one player in the game (i.e. the Shifter -can only use them, if the previous player did not use them before) - -### Game Options -| Name | Description -|----------|:-------------:| -| Shifter Spawn Chance | - -| Shifter Shifts Modifiers | Sets if Lovers and/or Medic Shield will be shifted ------------------------ - -## Time Master -### **Team: Crewmates** -The Time Master has a time shield which they can activate. The time shield remains active for a configurable amount of time.\ -If a player tries to kill the Time Master while the time shield is active, the kill won't happen and the -time will rewind for a set amount of time.\ -The kill cooldown of the killer won't be reset, so the Time Master -has to make sure that the game won't result in the same situation.\ -The Time Master won't be affected by the rewind.\ -\ -**NOTE:** -- Only the movement is affected by the rewind. -- A Vampire bite will trigger the rewind. If the Time Master misses shielding the bite, they can still shield the kill which happens a few seconds later. -- If the Time Master was bitten and has their shield active before when a meeting is called, they survive but the time won't be rewound. -- If the Time Master has a Medic shield, they won't rewind. -- The shield itself ends immediately when triggered. So the Time Master can be attacked again as soon as the rewind ends. - -### Game Options -| Name | Description | -|----------|:-------------:| -| Time Master Spawn Chance | - | -| Time Master Cooldown | - | -| Rewind Duration | How much time to rewind | -| Time Master Shield Duration | ------------------------ - -## Swapper -### **Team: Crewmates** -During meetings the Swapper can exchange votes that two people get (i.e. all votes -that player A got will be given to player B and vice versa).\ -Because of the Swapper's strength in meetings, they might not start emergency meetings -and can't fix lights and comms. - -### Game Options -| Name | Description -|----------|:-------------:| -| Swapper Spawn Chance | - -| Swapper can call emergency meeting | Option to disable the emergency button for the Swapper -| Swapper can only swap others | Sets whether the Swapper can swap themself or not ------------------------ - ## Tracker ### **Team: Crewmates** -The Tracker can select one player to track. Depending on the options the Tracker can track a different person after each meeting or the Tracker tracks the same person for the whole game. -An arrow points to the last tracked position of the player. -The arrow updates its position every few seconds (configurable). +The Tracker can select one player to track. Depending on the options the Tracker can track a different person after each meeting or the Tracker tracks the same person for the whole game.\ +An arrow points to the last tracked position of the player.\ +The arrow updates its position every few seconds (configurable).\ Depending on the options, the Tracker has another ability: They can track all corpses on the map for a set amount of time. They will keep tracking corpses, even if they were cleaned or eaten by the Vulture. ### Game Options @@ -1257,10 +1371,9 @@ Depending on the options, the Tracker has another ability: They can track all co ## Snitch ### **Team: Crewmates** -When the Snitch finishes all the tasks, arrows will appear (only visible to the Snitch) that point to the Impostors (depending on the options also to members of team Jackal). +When the Snitch finishes all the tasks, arrows will appear (only visible to the Snitch) that point to the Impostors (depending on the options also to members of team Jackal).\ When the Snitch has one task left (configurable) the Snitch will be revealed to the Impostors (depending on the options also to members of team Jackal) with an arrow pointing to the Snitch. - ### Game Options | Name | Description |----------|:-------------:| @@ -1271,71 +1384,6 @@ When the Snitch has one task left (configurable) the Snitch will be revealed to | Snitch can't be guessed after finishing all their tasks | - ----------------------- -## Jackal -### **Team: Jackal** -The Jackal is part of an extra team, that tries to eliminate all the other players.\ -The Jackal has no tasks and can kill Impostors, Crewmates and Neutrals.\ -The Jackal (if allowed by the options) can select another player to be their Sidekick. -Creating a Sidekick removes all tasks of the Sidekick and adds them to the team Jackal. The Sidekick loses their current role (except if they're a Lover, then they play in two teams). -The "Create Sidekick Action" may only be used once per Jackal or once per game (depending on the options). -The Jackal can also promote Impostors to be their Sidekick but, depending on the options the Impostor will either really turn into the Sidekick and leave the team Impostors or they will just look like the Sidekick to the Jackal and remain as they were.\ -\ -The team Jackal enables multiple new outcomes of the game, listing some examples here: -- The Impostors could be eliminated and then the crew plays against the team Jackal. -- The Crew could be eliminated, then the Team Jackal fight against the Impostors (The Crew can still make a task win in this scenario) - -The priority of the win conditions is the following: -1. Crewmate Mini lose by vote -2. Jester wins by vote -3. Arsonist win -4. Team Impostor wins by sabotage -5. Team Crew wins by tasks (also possible if the whole Crew is dead) -6. Lovers among the last three players win -7. Team Jackal wins by outnumbering (When the team Jackal contains an equal or greater amount of players than the Crew and there are 0 Impostors left and team Jackal contains no Lover) -8. Team Impostor wins by outnumbering (When the team Impostors contains an equal or greater amount of players than the Crew and there are 0 players of the team Jackal left and team Impostors contains no Lover) -9. Team Crew wins by outnumbering (When there is no player of the team Jackal and the team Impostors left) - -**NOTE:** -- The Jackal (and their Sidekick) may be killed by a Sheriff. -- A Jackal cannot target the Mini, while it's growing up. After that they can kill it or select it as its Sidekick. -- The Crew can still win, even if all of their members are dead, if they finish their tasks fast enough (That's why converting the last Crewmate with tasks left into a Sidekick results in a task win for the crew.) - -If both Impostors and Jackals are in the game the game, continues even if all Crewmates are dead. Crewmates may still win in this case by completing their tasks. Jackal and Impostor have to kill each other. - - - -### Game Options -| Name | Description -|----------|:-------------:| -| Jackal Spawn Chance | - | -| Jackal/Sidekick Kill Cooldown | Kill cooldown | -| Jackal Create Sidekick Cooldown | Cooldown before a Sidekick can be created | -| Jackal can use vents | Yes/No | -| Jackal can create a Sidekick | Yes/No | -| Jackals promoted from Sidekick can create a Sidekick | Yes/No (to prevent the Jackal team from growing) | -| Jackals can make an Impostor to their Sidekick | Yes/No (to prevent a Jackal from turning an Impostor into a Sidekick, if they use the ability on an Impostor they see the Impostor as Sidekick, but the Impostor isn't converted to Sidekick. If this option is set to "No" Jackal and Sidekick can kill each other.) | -| Jackal and Sidekick have Impostor vision | - | ------------------------ - -## Sidekick -### **Team: Jackal** -Gets assigned to a player during the game by the "Create Sidekick Action" of the Jackal and joins the Jackal in their quest to eliminate all other players.\ -Upon the death of the Jackal (depending on the options), they might get promoted to Jackal themself and potentially even assign a Sidekick of their own. -\ -**NOTE:** -- A player that converts into a Sidekick loses their previous role and tasks (if they had one), except the Lover role. -- The Sidekick may be killed by a Sheriff. -- The Sidekick cannot target the Mini, while it's growing up. - -### Game Options -| Name | Description -|----------|:-------------:| -| Jackal/Sidekick Kill Cooldown | Uses the same kill cooldown setting as the Jackal | -| Sidekick gets promoted to Jackal on Jackal death | Yes/No | -| Sidekick can kill | Yes/No | -| Sidekick can use vents | Yes/No | ------------------------ - ## Spy ### **Team: Crewmates** The Spy is a Crewmate, which has no special abilities.\ @@ -1345,6 +1393,9 @@ There are two possibilities (depending on the set options): - The Impostors can kill the Spy but they can also kill their Impostor partner (if they mistake another Impostor for the Spy) You can set whether the Sheriff can kill the Spy or not (in order to keep the lie alive). +**NOTE:** +- If the Spy gets sidekicked, it still will appear red to the Impostors. + ### Game Options | Name | Description |----------|:-------------:| @@ -1355,13 +1406,37 @@ You can set whether the Sheriff can kill the Spy or not (in order to keep the li | Spy Has Impostor Vision | Give the Spy the same vision as the Impostors have ----------------------- +## Portalmaker +### **Team: Crewmates** +The Portalmaker is a Crewmate that can place two portals on the map.\ +These two portals are connected to each other.\ +Those portals will be visible after the next meeting and can be used by everyone.\ +Additionally to that, the Portalmaker gets information about who used the portals and when in the chat during each meeting, depending on the options. + +**NOTE:** +- The extra button to use a portal will appear after the Portalmaker set his portals and a meeting/body report was called. +- While one player uses a portal, it is blocked for any other player until the player got teleported. +- All ghosts can still use the portals, but won't block any living player from using it and the Portalmaker won't get any information about it in chat. +- If a morphed person uses a portal it will show the morphed name/color depending on the options. +- If a comouflaged person uses a portal it will show "A comouflaged person used the portal." + +### Game Options +| Name | Description +|----------|:-------------:| +| Portalmaker Spawn Chance | - +| Portalmaker Cooldown | - +| Use Portal Cooldown | - +| Portalmaker Log Only Shows Color Type | - +| Log Shows Time | - +----------------------- + ## Security Guard ### **Team: Crewmates** The Security Guard is a Crewmate that has a certain number of screws that they can use for either sealing vents or for placing new cameras.\ Placing a new camera and sealing vents takes a configurable amount of screws. The total number of screws that a Security Guard has can also be configured.\ The new camera will be visible after the next meeting and accessible by everyone.\ -The vents will be sealed after the next meeting, players can't enter or exit sealed vents, but they can still "move to them" underground.\ -\ +The vents will be sealed after the next meeting, players can't enter or exit sealed vents, but they can still "move to them" underground. + **NOTE:** - Trickster boxes can't be sealed @@ -1385,27 +1460,10 @@ The vents will be sealed after the next meeting, players can't enter or exit sea | Can't Move During Cam Duration | - ----------------------- -## Bait -### **Team: Crewmates** - -The Bait is a Crewmate that if killed, forces the killer to self report the body (you can configure a delay in the options). -Additionally, the Bait can see if someone is inside a vent (depending on the options the exact vent gets -an outline or all vents do). - -### Game Options -| Name | Description -|----------|:-------------:| -| Bait Spawn Chance | - -| Bait Highlight All Vents | If set to true, all vents will be highlighted if a player is inside of one of them. If set to false, only the vents where players are siting in will be highlighted. -| Bait Report Delay | - -| Warn The Killer With A Flash | - ------------------------ - ## Medium ### **Team: Crewmates** -The medium is a crewmate who can ask the souls of dead players for information. Like the Seer, it sees the places where the players have died (after the next meeting) and can question them. It then gets random information about the soul or the killer in the chat. The souls only stay for one round, i.e. until the next meeting. Depending on the options, the souls can only be questioned once and then disappear. -During the meetings you can see, whether a player wears a darker or a lighter color, represented by (D) or (L) in the names. +The medium is a crewmate who can ask the souls of dead players for information. Like the Seer, it sees the places where the players have died (after the next meeting) and can question them. It then gets random information about the soul or the killer in the chat. The souls only stay for one round, i.e. until the next meeting. Depending on the options, the souls can only be questioned once and then disappear.\ Questions: What is your Role? @@ -1422,58 +1480,163 @@ What is your killers role? (mini exluded) | Medium Each Soul Can Only Be Questioned Once | If set to true, souls can only be questioned once and then disappear ----------------------- -## Vulture -### **Team: Neutral** +# Modifier +A modifier is an addition to your Impostor/Neutral/Crewmate role. -The Vulture does not have any tasks, they have to win the game as a solo.\ -The Vulture is a neutral role that must eat a specified number of corpses (depending on the options) in order to win.\ -Depending on the options, when a player dies, the Vulture gets an arrow pointing to the corpse. -If there is a Vulture in the game, there can't be a Cleaner. +## Bloody +### Bloody + +If killed, the Bloody Modifier will leave a trail for x-seconds on their killer. The trail will have the color of the killed person. Impostor, +Neutral or Crewmate roles can have this Modifier. ### Game Options | Name | Description | |----------|:-------------:| -| Vulture Spawn Chance | - -| Vulture Countdown | - -| Number Of Corpses Needed To Be Eaten | Corpes needed to be eaten to win the game -| Vulture Can Use Vents | - -| Show Arrows Pointing Towards The Corpes | - +| Bloody Spawn Chance | - +| Bloody Quantity | - +| Trail duration | - ----------------------- -## Lawyer -### **Team: Neutral** -The Lawyer is a neutral role that has a client. -The client might be an Impostor or Jackal which is no Lover. -The Lawyer needs their client to win in order to win the game. -If their client dies or gets voted out, the Lawyer changes their 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, as it is not allowed to betray their client. +## Anti Teleport -The Lawyer can win in multiple ways: -- Lawyer dead, client alive and client team won: The Lawyer wins together with the team of the client -- Lawyer and client alive and client team won: The Lawyer wins with the team of the client. The client **doesn't** win (even if their Impostor/Team Jackal mate wins), the Lawyer steals their win. Hence the client should keep the Lawyer alive for some time, to get some help during the meetings, but has to eliminate them soon enough to not get their win stolen. +The Anti Teleport Modifier prevents the player from getting teleported to the Meeting Table if a body gets reported or an Emergency Meeting is called.\ +The player will start the round where the previous one ended (Emergency Meeting Call/Body Report). + +### Game Options +| Name | Description | +|----------|:-------------:| +| Anti Teleport Spawn Chance | - +| Anti Teleport Quantity | - +----------------------- + +## Tie Breaker + +If the Voting ends in a tie, the Tie Breaker takes place and the player with the Tie Breaker Modifier gets an extra vote thats not visible to anyone.\ +Everyone will know if the Tie Breaker was involved in the Meeting or not. + +### Game Options +| Name | Description | +|----------|:-------------:| +| Tie Breaker Spawn Chance | - +----------------------- + +## Bait + +The Bait forces the killer to self report the body (you can configure a delay in the options).\ +There can be more than one Bait. + +### Game Options +| Name | Description +|----------|:-------------:| +| Bait Spawn Chance | - +| Bait Quantity | - +| Bait Report Delay Min | - +| Bait Report Delay Max | - +| Warn The Killer With A Flash | - +----------------------- + +## Lovers + +There are always two Lovers which are linked together.\ +Their primary goal is it to stay alive together until the end of the game.\ +If one Lover dies (and the option is activated), the other Lover suicides.\ +You can specify the chance of one Lover being an Impostor.\ +The Lovers never know the role of their partner, they only see who their partner is.\ +The Lovers win, if they are both alive when the game ends. They can also win with their original team (e.g. a dead Impostor Lover can win with the Impostors, an Arsonist Lover can still achieve an Arsonist win).\ +If one of the Lovers is a killer (i.e. Jackal/Sidekick/Impostor), they can achieve a "Lovers solo win" where only the Lovers win.\ +If there is no killer among the Lovers (e.g. an Arsonist Lover + Crewmate Lover) and they are both alive when the game ends, they win together with the Crewmates.\ +If there's an Impostor/Jackal + Crewmate Lover in the game, the tasks of a Crewmate Lover won't be counted (for a task win) as long as they're alive.\ +If the Lover dies, their tasks will also be counted.\ +You can enable an exclusive chat only for Lovers. **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 someone is their Lawyer, but won't know who. +In a 2 Crewmates vs 2 Impostors (or 2 members of team Jackal) and the Lovers are not in the same team, the game is not automatically over since the Lovers can still achieve a solo win. E.g. if there are the following roles Impostor + ImpLover + Lover + Crewmate left, the game will not end and the next kill will decide if the Impostors or Lovers win. ### Game Options | Name | Description | |----------|:-------------:| -| Lawyer Target Knows | The target knows that it is the target (marked with "§", if the 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 -| Lawyer Knows Target Role | - -| Pursuer Blank Cooldown | - -| Pursuer Number Of Blanks | - +| Lovers Spawn Chance | - +| Chance That One Lover Is Impostor | - +| Both Lovers Die | Whether the second Lover suicides, if the first one dies +| Enable Lover Chat | - ----------------------- -## Pursuer -### **Team: Neutral** -The Pursuer is still a neutral role, but has a different goal to win the game; they have to be alive when the game ends (no matter who causes the win). -In order to achieve this goal, the Pursuer has an ability called "Blank", where they 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 their target, and their cooldowns will be triggered as usual. -If the killer fires the "Blank", shields (e.g. Medic shield or Time Master shield) will not 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, their tasks won't be counted anymore. +## Sunglasses + +The Sunglasses will lower the Crewmate's vision by small percentage. The percentage is configurable in the options.\ +The vision will also be affected when lights out. + +**NOTE:** +- Sunglasses only affects Crewmates. +- If you have the Sunglasses modifier and get sidekicked, you lose the modifier. + +### Game Options +| Name | Description +|----------|:-------------:| +| Sunglasses Spawn Chance | - +| Sunglasses Quantity | - +| Vision with sunglasses | - +----------------------- + +## Mini + +The Mini's character is smaller and hence visible to everyone in the game.\ +The Mini cannot be killed until it turns 18 years old, however it can be voted out. + +**Impostor/Jackal Mini:** +- While growing up the kill cooldown is doubled. When it's fully grown up its kill cooldown is 2/3 of the default one. +- If it gets thrown out of the ship, everything is fine. + +**Crewmate Mini:** +- The Crewmate Mini aims to play out the strength its invincibility in the early game. +- If it gets thrown out of the ship before it turns 18, everyone loses. So think twice before you vote out a Mini. + +**Neutral Mini:** +- The cooldown is not effected, except for the Team Jackal/Sidekick. +- If it gets thrown out of the ship, everything is fine except for the Jester. +- If the Jester Mini gets voted out the game will end in a Jester win. + +**NOTE:** +- If the Sheriff tries to kill the Mini before it's fully grown, nothing happens. +- The Sheriff can kill the Impostor/Neutral Mini, but only if it's fully grown up. + +### Game Options +| Name | Description | +|----------|:-------------:| +| Mini Spawn Chance | - +| Mini | Mini Growing Up Duration +----------------------- + +## VIP + +An Impostor, Jackal or Crewmate can be affected by the VIP (Very Important Player) Modifier.\ +The VIP will show everyone when he dies with a flash similar to the Seer Flash.\ +If the option Show Team Color is On, then everyone will get a flash in the color of the team the player was part of. + +Teams: +- Impostor = Red +- Neutral = Blue +- Crewmate = White + +### Game Options +| Name | Description +|----------|:-------------:| +| VIP Spawn Chance | - +| VIP Quantity | - +| Show Team Color | - +----------------------- + +## Invert + +The Invert Modifier inverts your controls (no matter if keyboard or mouse).\ +The Invert can affect all teams (Impostor, Neutral, Crewmate). + +### Game Options +| Name | Description +|----------|:-------------:| +| Invert Spawn Chance | - +| Invert Quantity | - +----------------------- # 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. diff --git a/TheOtherRoles/Buttons.cs b/TheOtherRoles/Buttons.cs index 73df41878..f17189cd4 100644 --- a/TheOtherRoles/Buttons.cs +++ b/TheOtherRoles/Buttons.cs @@ -14,22 +14,24 @@ static class HudManagerStartPatch { private static CustomButton engineerRepairButton; private static CustomButton janitorCleanButton; - private static CustomButton sheriffKillButton; + public static CustomButton sheriffKillButton; private static CustomButton deputyHandcuffButton; private static CustomButton timeMasterShieldButton; private static CustomButton medicShieldButton; private static CustomButton shifterShiftButton; private static CustomButton morphlingButton; private static CustomButton camouflagerButton; + private static CustomButton portalmakerPlacePortalButton; + private static CustomButton usePortalButton; private static CustomButton hackerButton; private static CustomButton hackerVitalsButton; private static CustomButton hackerAdminTableButton; private static CustomButton trackerTrackPlayerButton; private static CustomButton trackerTrackCorpsesButton; - private static CustomButton vampireKillButton; + public static CustomButton vampireKillButton; private static CustomButton garlicButton; - private static CustomButton jackalKillButton; - private static CustomButton sidekickKillButton; + public static CustomButton jackalKillButton; + public static CustomButton sidekickKillButton; private static CustomButton jackalSidekickButton; private static CustomButton lighterButton; private static CustomButton eraserButton; @@ -44,6 +46,7 @@ static class HudManagerStartPatch public static CustomButton mediumButton; public static CustomButton pursuerButton; public static CustomButton witchSpellButton; + public static CustomButton ninjaButton; public static Dictionary> deputyHandcuffedButtons = null; @@ -64,6 +67,8 @@ public static void setCustomButtonCooldowns() { shifterShiftButton.MaxTimer = 0f; morphlingButton.MaxTimer = Morphling.cooldown; camouflagerButton.MaxTimer = Camouflager.cooldown; + portalmakerPlacePortalButton.MaxTimer = Portalmaker.cooldown; + usePortalButton.MaxTimer = Portalmaker.usePortalCooldown; hackerButton.MaxTimer = Hacker.cooldown; hackerVitalsButton.MaxTimer = Hacker.cooldown; hackerAdminTableButton.MaxTimer = Hacker.cooldown; @@ -86,7 +91,8 @@ public static void setCustomButtonCooldowns() { mediumButton.MaxTimer = Medium.cooldown; pursuerButton.MaxTimer = Pursuer.cooldown; trackerTrackCorpsesButton.MaxTimer = Tracker.corpsesTrackingCooldown; - witchSpellButton.MaxTimer = Witch.cooldown; + witchSpellButton.MaxTimer = Witch.cooldown; + ninjaButton.MaxTimer = Ninja.cooldown; timeMasterShieldButton.EffectDuration = TimeMaster.shieldDuration; hackerButton.EffectDuration = Hacker.duration; @@ -365,6 +371,7 @@ public static void Postfix(HudManager __instance) RPCProcedure.setFutureShielded(Medic.currentTarget.PlayerId); else RPCProcedure.medicSetShielded(Medic.currentTarget.PlayerId); + Medic.meetingAfterShielding = false; }, () => { return Medic.medic != null && Medic.medic == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; }, () => { return !Medic.usedShield && Medic.currentTarget && PlayerControl.LocalPlayer.CanMove; }, @@ -713,7 +720,65 @@ public static void Postfix(HudManager __instance) true ); - + portalmakerPlacePortalButton = new CustomButton( + () => { + portalmakerPlacePortalButton.Timer = portalmakerPlacePortalButton.MaxTimer; + + var pos = PlayerControl.LocalPlayer.transform.position; + byte[] buff = new byte[sizeof(float) * 2]; + Buffer.BlockCopy(BitConverter.GetBytes(pos.x), 0, buff, 0 * sizeof(float), sizeof(float)); + Buffer.BlockCopy(BitConverter.GetBytes(pos.y), 0, buff, 1 * sizeof(float), sizeof(float)); + + MessageWriter writer = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.PlacePortal, Hazel.SendOption.Reliable); + writer.WriteBytesAndSize(buff); + writer.EndMessage(); + RPCProcedure.placePortal(buff); + }, + () => { return Portalmaker.portalmaker != null && Portalmaker.portalmaker == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead && Portal.secondPortal == null; }, + () => { return PlayerControl.LocalPlayer.CanMove && Portal.secondPortal == null; }, + () => { portalmakerPlacePortalButton.Timer = portalmakerPlacePortalButton.MaxTimer; }, + Portalmaker.getPlacePortalButtonSprite(), + new Vector3(-1.8f, -0.06f, 0), + __instance, + KeyCode.F + ); + + usePortalButton = new CustomButton( + () => { + bool didTeleport = false; + Vector2 exit = Portal.findExit(PlayerControl.LocalPlayer.transform.position); + Vector2 entry = Portal.findEntry(PlayerControl.LocalPlayer.transform.position); + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(entry); // TODO: check for bans on servers + + if (!PlayerControl.LocalPlayer.Data.IsDead) { // Ghosts can portal too, but non-blocking and only with a local animation + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UsePortal, Hazel.SendOption.Reliable, -1); + writer.Write((byte)PlayerControl.LocalPlayer.PlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + RPCProcedure.usePortal(PlayerControl.LocalPlayer.PlayerId); + usePortalButton.Timer = usePortalButton.MaxTimer; + HudManager.Instance.StartCoroutine(Effects.Lerp(Portal.teleportDuration, new Action((p) => { // Delayed action + PlayerControl.LocalPlayer.moveable = false; + PlayerControl.LocalPlayer.NetTransform.Halt(); + if (p >= 0.5f && p <= 0.53f && !didTeleport) { + PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(exit); + didTeleport = true; + } + if (p == 1f) { + PlayerControl.LocalPlayer.moveable = true; + } + }))); + }, + () => { return Portal.bothPlacedAndEnabled; }, + () => { return PlayerControl.LocalPlayer.CanMove && Portal.locationNearEntry(PlayerControl.LocalPlayer.transform.position) && !Portal.isTeleporting; }, + () => { usePortalButton.Timer = usePortalButton.MaxTimer; }, + Portalmaker.getUsePortalButtonSprite(), + new Vector3(0.9f, -0.06f, 0), + __instance, + KeyCode.H, + mirror: true + ); + // Jackal Sidekick Button jackalSidekickButton = new CustomButton( () => { @@ -1288,10 +1353,14 @@ public static void Postfix(HudManager __instance) RPCProcedure.setFutureSpelled(Witch.currentTarget.PlayerId); } if (attempt == MurderAttemptResult.BlankKill || attempt == MurderAttemptResult.PerformKill) { - witchSpellButton.MaxTimer += Witch.cooldownAddition; + Witch.currentCooldownAddition += Witch.cooldownAddition; + witchSpellButton.MaxTimer = Witch.cooldown + Witch.currentCooldownAddition; + Patches.PlayerControlFixedUpdatePatch.miniCooldownUpdate(); // Modifies the MaxTimer if the witch is the mini witchSpellButton.Timer = witchSpellButton.MaxTimer; - if (Witch.triggerBothCooldowns) - Witch.witch.killTimer = PlayerControl.GameOptions.KillCooldown; + if (Witch.triggerBothCooldowns) { + float multiplier = (Mini.mini != null && PlayerControl.LocalPlayer == Mini.mini) ? (Mini.isGrownUp() ? 0.66f : 2f) : 1f; + Witch.witch.killTimer = PlayerControl.GameOptions.KillCooldown * multiplier; + } } else { witchSpellButton.Timer = 0f; } @@ -1299,9 +1368,76 @@ public static void Postfix(HudManager __instance) } ); - // Set the default (or settings from the previous game) timers/durations when spawning the buttons + // Ninja mark and assassinate button + ninjaButton = new CustomButton( + () => { + if (Ninja.ninjaMarked != null) { + // Murder attempt with teleport + MurderAttemptResult attempt = Helpers.checkMuderAttempt(Ninja.ninja, Ninja.ninjaMarked); + if (attempt == MurderAttemptResult.PerformKill) { + // Create first trace before killing + var pos = PlayerControl.LocalPlayer.transform.position; + byte[] buff = new byte[sizeof(float) * 2]; + Buffer.BlockCopy(BitConverter.GetBytes(pos.x), 0, buff, 0 * sizeof(float), sizeof(float)); + Buffer.BlockCopy(BitConverter.GetBytes(pos.y), 0, buff, 1 * sizeof(float), sizeof(float)); + + MessageWriter writer = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.PlaceNinjaTrace, Hazel.SendOption.Reliable); + writer.WriteBytesAndSize(buff); + writer.EndMessage(); + RPCProcedure.placeNinjaTrace(buff); + + // Perform Kill + MessageWriter writer2 = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedMurderPlayer, Hazel.SendOption.Reliable, -1); + writer2.Write(PlayerControl.LocalPlayer.PlayerId); + writer2.Write(Ninja.ninjaMarked.PlayerId); + writer2.Write(byte.MaxValue); + AmongUsClient.Instance.FinishRpcImmediately(writer2); + RPCProcedure.uncheckedMurderPlayer(PlayerControl.LocalPlayer.PlayerId, Ninja.ninjaMarked.PlayerId, byte.MaxValue); + + // Create Second trace after killing + pos = Ninja.ninjaMarked.transform.position; + buff = new byte[sizeof(float) * 2]; + Buffer.BlockCopy(BitConverter.GetBytes(pos.x), 0, buff, 0 * sizeof(float), sizeof(float)); + Buffer.BlockCopy(BitConverter.GetBytes(pos.y), 0, buff, 1 * sizeof(float), sizeof(float)); + + MessageWriter writer3 = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.PlaceNinjaTrace, Hazel.SendOption.Reliable); + writer3.WriteBytesAndSize(buff); + writer3.EndMessage(); + RPCProcedure.placeNinjaTrace(buff); + } + + if (attempt == MurderAttemptResult.BlankKill || attempt == MurderAttemptResult.PerformKill) { + ninjaButton.Timer = ninjaButton.MaxTimer; + Ninja.ninja.killTimer = PlayerControl.GameOptions.KillCooldown; + } else if (attempt == MurderAttemptResult.SuppressKill) { + ninjaButton.Timer = 0f; + } + Ninja.ninjaMarked = null; + return; + } + if (Ninja.currentTarget != null) { + Ninja.ninjaMarked = Ninja.currentTarget; + ninjaButton.Timer = 5f; + } + }, + () => { return Ninja.ninja != null && Ninja.ninja == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; }, + () => { // CouldUse + ninjaButton.Sprite = Ninja.ninjaMarked != null ? Ninja.getKillButtonSprite() : Ninja.getMarkButtonSprite(); + return (Ninja.currentTarget != null || Ninja.ninjaMarked != null) && PlayerControl.LocalPlayer.CanMove; + }, + () => { // on meeting ends + ninjaButton.Timer = ninjaButton.MaxTimer; + Ninja.ninjaMarked = null; + }, + Ninja.getMarkButtonSprite(), + new Vector3(-1.8f, -0.06f, 0), + __instance, + KeyCode.F + ); + + // Set the default (or settings from the previous game) timers / durations when spawning the buttons setCustomButtonCooldowns(); deputyHandcuffedButtons = null; } } -} \ No newline at end of file +} diff --git a/TheOtherRoles/CustomOptionHolder.cs b/TheOtherRoles/CustomOptionHolder.cs index 72aa25080..9bbf8ac95 100644 --- a/TheOtherRoles/CustomOptionHolder.cs +++ b/TheOtherRoles/CustomOptionHolder.cs @@ -8,10 +8,12 @@ using System.Reflection; using System.Text; using static TheOtherRoles.TheOtherRoles; +using Types = TheOtherRoles.CustomOption.CustomOptionType; namespace TheOtherRoles { public class CustomOptionHolder { public static string[] rates = new string[]{"0%", "10%", "20%", "30%", "40%", "50%", "60%", "70%", "80%", "90%", "100%"}; + public static string[] ratesModifier = new string[]{"1", "2", "3"}; public static string[] presets = new string[]{"Preset 1", "Preset 2", "Preset 3", "Preset 4", "Preset 5"}; public static CustomOption presetSelection; @@ -22,6 +24,8 @@ public class CustomOptionHolder { public static CustomOption neutralRolesCountMax; public static CustomOption impostorRolesCountMin; public static CustomOption impostorRolesCountMax; + public static CustomOption modifiersCountMin; + public static CustomOption modifiersCountMax; public static CustomOption mafiaSpawnRate; public static CustomOption janitorCooldown; @@ -42,16 +46,6 @@ public class CustomOptionHolder { public static CustomOption eraserSpawnRate; public static CustomOption eraserCooldown; public static CustomOption eraserCanEraseAnyone; - - public static CustomOption miniSpawnRate; - public static CustomOption miniGrowingUpDuration; - - public static CustomOption loversSpawnRate; - public static CustomOption loversImpLoverRate; - public static CustomOption loversBothDie; - public static CustomOption loversCanHaveAnotherRole; - public static CustomOption loversEnableChat; - public static CustomOption guesserSpawnRate; public static CustomOption guesserIsImpGuesserRate; public static CustomOption guesserNumberOfShots; @@ -97,10 +91,24 @@ public class CustomOptionHolder { public static CustomOption witchTriggerBothCooldowns; public static CustomOption witchVoteSavesTargets; + public static CustomOption ninjaSpawnRate; + public static CustomOption ninjaCooldown; + public static CustomOption ninjaKnowsTargetLocation; + public static CustomOption ninjaTraceTime; + public static CustomOption ninjaTraceColorTime; + public static CustomOption shifterSpawnRate; public static CustomOption shifterShiftsModifiers; public static CustomOption mayorSpawnRate; + public static CustomOption mayorCanSeeVoteColors; + public static CustomOption mayorTasksNeededToSeeVoteColors; + + public static CustomOption portalmakerSpawnRate; + public static CustomOption portalmakerCooldown; + public static CustomOption portalmakerUsePortalCooldown; + public static CustomOption portalmakerLogOnlyColorType; + public static CustomOption portalmakerLogHasTime; public static CustomOption engineerSpawnRate; public static CustomOption engineerNumberOfFixes; @@ -140,12 +148,15 @@ public class CustomOptionHolder { public static CustomOption medicSpawnRate; public static CustomOption medicShowShielded; public static CustomOption medicShowAttemptToShielded; - public static CustomOption medicSetShieldAfterMeeting; + public static CustomOption medicSetOrShowShieldAfterMeeting; public static CustomOption medicShowAttemptToMedic; + public static CustomOption medicSetShieldAfterMeeting; public static CustomOption swapperSpawnRate; public static CustomOption swapperCanCallEmergency; public static CustomOption swapperCanOnlySwapOthers; + public static CustomOption swapperSwapsNumber; + public static CustomOption swapperRechargeTasksNumber; public static CustomOption seerSpawnRate; public static CustomOption seerMode; @@ -200,11 +211,6 @@ public class CustomOptionHolder { public static CustomOption securityGuardCamRechargeTasksNumber; public static CustomOption securityGuardNoMove; - public static CustomOption baitSpawnRate; - public static CustomOption baitHighlightAllVents; - public static CustomOption baitReportDelay; - public static CustomOption baitShowKillFlash; - public static CustomOption vultureSpawnRate; public static CustomOption vultureCooldown; public static CustomOption vultureNumberToWin; @@ -217,7 +223,7 @@ public class CustomOptionHolder { public static CustomOption mediumOneTimeUse; public static CustomOption lawyerSpawnRate; - public static CustomOption lawyerTargetKnows; + public static CustomOption lawyerTargetCanBeJester; public static CustomOption lawyerWinsAfterMeetings; public static CustomOption lawyerNeededMeetings; public static CustomOption lawyerVision; @@ -225,11 +231,47 @@ public class CustomOptionHolder { public static CustomOption pursuerCooldown; public static CustomOption pursuerBlanksNumber; + public static CustomOption modifierBait; + public static CustomOption modifierBaitQuantity; + public static CustomOption modifierBaitReportDelayMin; + public static CustomOption modifierBaitReportDelayMax; + public static CustomOption modifierBaitShowKillFlash; + + public static CustomOption modifierLover; + public static CustomOption modifierLoverImpLoverRate; + public static CustomOption modifierLoverBothDie; + public static CustomOption modifierLoverEnableChat; + + public static CustomOption modifierBloody; + public static CustomOption modifierBloodyQuantity; + public static CustomOption modifierBloodyDuration; + + public static CustomOption modifierAntiTeleport; + public static CustomOption modifierAntiTeleportQuantity; + + public static CustomOption modifierTieBreaker; + + public static CustomOption modifierSunglasses; + public static CustomOption modifierSunglassesQuantity; + public static CustomOption modifierSunglassesVision; + + public static CustomOption modifierMini; + public static CustomOption modifierMiniGrowingUpDuration; + + public static CustomOption modifierVip; + public static CustomOption modifierVipQuantity; + public static CustomOption modifierVipShowColor; + + public static CustomOption modifierInvert; + public static CustomOption modifierInvertQuantity; + public static CustomOption modifierInvertDuration; + public static CustomOption maxNumberOfMeetings; public static CustomOption blockSkippingInEmergencyMeetings; public static CustomOption noVoteIsSelfVote; public static CustomOption hidePlayerNames; public static CustomOption allowParallelMedBayScans; + public static CustomOption shieldFirstKill; public static CustomOption dynamicMap; public static CustomOption dynamicMapEnableSkeld; @@ -251,231 +293,275 @@ private static byte ToByte(float f) { public static void Load() { + // Role Options - presetSelection = CustomOption.Create(0, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Preset"), presets, null, true); - activateRoles = CustomOption.Create(1, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Enable Mod Roles And Block Vanilla Roles"), true, null, true); + presetSelection = CustomOption.Create(0, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Preset"), presets, null, true); + activateRoles = CustomOption.Create(1, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Enable Mod Roles And Block Vanilla Roles"), true, null, true); // Using new id's for the options to not break compatibilty with older versions - crewmateRolesCountMin = CustomOption.Create(300, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Crewmate Roles"), 0f, 0f, 15f, 1f, null, true); - crewmateRolesCountMax = CustomOption.Create(301, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Crewmate Roles"), 0f, 0f, 15f, 1f); - neutralRolesCountMin = CustomOption.Create(302, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Neutral Roles"), 0f, 0f, 15f, 1f); - neutralRolesCountMax = CustomOption.Create(303, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Neutral Roles"), 0f, 0f, 15f, 1f); - impostorRolesCountMin = CustomOption.Create(304, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Impostor Roles"), 0f, 0f, 3f, 1f); - impostorRolesCountMax = CustomOption.Create(305, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Impostor Roles"), 0f, 0f, 3f, 1f); - - mafiaSpawnRate = CustomOption.Create(10, cs(Janitor.color, "Mafia"), rates, null, true); - janitorCooldown = CustomOption.Create(11, "Janitor Cooldown", 30f, 10f, 60f, 2.5f, mafiaSpawnRate); - - morphlingSpawnRate = CustomOption.Create(20, cs(Morphling.color, "Morphling"), rates, null, true); - morphlingCooldown = CustomOption.Create(21, "Morphling Cooldown", 30f, 10f, 60f, 2.5f, morphlingSpawnRate); - morphlingDuration = CustomOption.Create(22, "Morph Duration", 10f, 1f, 20f, 0.5f, morphlingSpawnRate); - - camouflagerSpawnRate = CustomOption.Create(30, cs(Camouflager.color, "Camouflager"), rates, null, true); - camouflagerCooldown = CustomOption.Create(31, "Camouflager Cooldown", 30f, 10f, 60f, 2.5f, camouflagerSpawnRate); - camouflagerDuration = CustomOption.Create(32, "Camo Duration", 10f, 1f, 20f, 0.5f, camouflagerSpawnRate); - - vampireSpawnRate = CustomOption.Create(40, cs(Vampire.color, "Vampire"), rates, null, true); - vampireKillDelay = CustomOption.Create(41, "Vampire Kill Delay", 10f, 1f, 20f, 1f, vampireSpawnRate); - vampireCooldown = CustomOption.Create(42, "Vampire Cooldown", 30f, 10f, 60f, 2.5f, vampireSpawnRate); - vampireCanKillNearGarlics = CustomOption.Create(43, "Vampire Can Kill Near Garlics", true, vampireSpawnRate); - - eraserSpawnRate = CustomOption.Create(230, cs(Eraser.color, "Eraser"), rates, null, true); - eraserCooldown = CustomOption.Create(231, "Eraser Cooldown", 30f, 10f, 120f, 5f, eraserSpawnRate); - eraserCanEraseAnyone = CustomOption.Create(232, "Eraser Can Erase Anyone", false, eraserSpawnRate); - - tricksterSpawnRate = CustomOption.Create(250, cs(Trickster.color, "Trickster"), rates, null, true); - tricksterPlaceBoxCooldown = CustomOption.Create(251, "Trickster Box Cooldown", 10f, 2.5f, 30f, 2.5f, tricksterSpawnRate); - tricksterLightsOutCooldown = CustomOption.Create(252, "Trickster Lights Out Cooldown", 30f, 10f, 60f, 5f, tricksterSpawnRate); - tricksterLightsOutDuration = CustomOption.Create(253, "Trickster Lights Out Duration", 15f, 5f, 60f, 2.5f, tricksterSpawnRate); - - 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); - - bountyHunterSpawnRate = CustomOption.Create(320, cs(BountyHunter.color, "Bounty Hunter"), rates, null, true); - bountyHunterBountyDuration = CustomOption.Create(321, "Duration After Which Bounty Changes", 60f, 10f, 180f, 10f, bountyHunterSpawnRate); - bountyHunterReducedCooldown = CustomOption.Create(322, "Cooldown After Killing Bounty", 2.5f, 0f, 30f, 2.5f, bountyHunterSpawnRate); - bountyHunterPunishmentTime = CustomOption.Create(323, "Additional Cooldown After Killing Others", 20f, 0f, 60f, 2.5f, bountyHunterSpawnRate); - 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(370, cs(Witch.color, "Witch"), rates, null, true); - witchCooldown = CustomOption.Create(371, "Witch Spell Casting Cooldown", 30f, 10f, 120f, 5f, witchSpawnRate); - witchAdditionalCooldown = CustomOption.Create(372, "Witch Additional Cooldown", 10f, 0f, 60f, 5f, witchSpawnRate); - witchCanSpellAnyone = CustomOption.Create(373, "Witch Can Spell Anyone", false, witchSpawnRate); - witchSpellCastingDuration = CustomOption.Create(374, "Spell Casting Duration", 1f, 0f, 10f, 1f, witchSpawnRate); - witchTriggerBothCooldowns = CustomOption.Create(375, "Trigger Both Cooldowns", true, witchSpawnRate); - witchVoteSavesTargets = CustomOption.Create(376, "Voting The Witch Saves All The Targets", 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); - - loversSpawnRate = CustomOption.Create(50, cs(Lovers.color, "Lovers"), rates, null, true); - loversImpLoverRate = CustomOption.Create(51, "Chance That One Lover Is Impostor", rates, loversSpawnRate); - loversBothDie = CustomOption.Create(52, "Both Lovers Die", true, loversSpawnRate); - loversCanHaveAnotherRole = CustomOption.Create(53, "Lovers Can Have Another Role", true, loversSpawnRate); - loversEnableChat = CustomOption.Create(54, "Enable Lover Chat", true, loversSpawnRate); - - guesserSpawnRate = CustomOption.Create(310, cs(Guesser.color, "Guesser"), rates, null, true); - guesserIsImpGuesserRate = CustomOption.Create(311, "Chance That The Guesser Is An Impostor", rates, guesserSpawnRate); - guesserNumberOfShots = CustomOption.Create(312, "Guesser Number Of Shots", 2f, 1f, 15f, 1f, guesserSpawnRate); - guesserHasMultipleShotsPerMeeting = CustomOption.Create(313, "Guesser Can Shoot Multiple Times Per Meeting", false, guesserSpawnRate); - guesserShowInfoInGhostChat = CustomOption.Create(314, "Guesses Visible In Ghost Chat", true, guesserSpawnRate); - guesserKillsThroughShield = CustomOption.Create(315, "Guesses Ignore The Medic Shield", true, guesserSpawnRate); - guesserEvilCanKillSpy = CustomOption.Create(316, "Evil Guesser Can Guess The Spy", true, guesserSpawnRate); - guesserSpawnBothRate = CustomOption.Create(317, "Both Guesser Spawn Rate", rates, guesserSpawnRate); - guesserCantGuessSnitchIfTaksDone = CustomOption.Create(318, "Guesser Can't Guess Snitch When Tasks Completed", true, guesserSpawnRate); - - jesterSpawnRate = CustomOption.Create(60, cs(Jester.color, "Jester"), rates, null, true); - jesterCanCallEmergency = CustomOption.Create(61, "Jester Can Call Emergency Meeting", true, jesterSpawnRate); - jesterHasImpostorVision = CustomOption.Create(62, "Jester Has Impostor Vision", false, jesterSpawnRate); - - arsonistSpawnRate = CustomOption.Create(290, cs(Arsonist.color, "Arsonist"), rates, null, true); - arsonistCooldown = CustomOption.Create(291, "Arsonist Cooldown", 12.5f, 2.5f, 60f, 2.5f, arsonistSpawnRate); - arsonistDuration = CustomOption.Create(292, "Arsonist Douse Duration", 3f, 1f, 10f, 1f, arsonistSpawnRate); - - jackalSpawnRate = CustomOption.Create(220, cs(Jackal.color, "Jackal"), rates, null, true); - jackalKillCooldown = CustomOption.Create(221, "Jackal/Sidekick Kill Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate); - jackalCreateSidekickCooldown = CustomOption.Create(222, "Jackal Create Sidekick Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate); - jackalCanUseVents = CustomOption.Create(223, "Jackal Can Use Vents", true, jackalSpawnRate); - jackalCanCreateSidekick = CustomOption.Create(224, "Jackal Can Create A Sidekick", false, jackalSpawnRate); - sidekickPromotesToJackal = CustomOption.Create(225, "Sidekick Gets Promoted To Jackal On Jackal Death", false, jackalSpawnRate); - sidekickCanKill = CustomOption.Create(226, "Sidekick Can Kill", false, jackalSpawnRate); - sidekickCanUseVents = CustomOption.Create(227, "Sidekick Can Use Vents", true, jackalSpawnRate); - jackalPromotedFromSidekickCanCreateSidekick = CustomOption.Create(228, "Jackals Promoted From Sidekick Can Create A Sidekick", true, jackalSpawnRate); - jackalCanCreateSidekickFromImpostor = CustomOption.Create(229, "Jackals Can Make An Impostor To His Sidekick", true, jackalSpawnRate); - jackalAndSidekickHaveImpostorVision = CustomOption.Create(430, "Jackal And Sidekick Have Impostor Vision", false, jackalSpawnRate); - - vultureSpawnRate = CustomOption.Create(340, cs(Vulture.color, "Vulture"), rates, null, true); - vultureCooldown = CustomOption.Create(341, "Vulture Cooldown", 15f, 10f, 60f, 2.5f, vultureSpawnRate); - vultureNumberToWin = CustomOption.Create(342, "Number Of Corpses Needed To Be Eaten", 4f, 1f, 10f, 1f, vultureSpawnRate); - vultureCanUseVents = CustomOption.Create(343, "Vulture Can Use Vents", true, vultureSpawnRate); - vultureShowArrows = CustomOption.Create(344, "Show Arrows Pointing Towards The Corpses", 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); - lawyerKnowsRole = CustomOption.Create(355, "Lawyer Knows Target Role", false, lawyerSpawnRate); - pursuerCooldown = CustomOption.Create(356, "Pursuer Blank Cooldown", 30f, 5f, 60f, 2.5f, lawyerSpawnRate); - pursuerBlanksNumber = CustomOption.Create(357, "Pursuer Number Of Blanks", 5f, 1f, 20f, 1f, lawyerSpawnRate); - - shifterSpawnRate = CustomOption.Create(70, cs(Shifter.color, "Shifter"), rates, null, true); - shifterShiftsModifiers = CustomOption.Create(71, "Shifter Shifts Modifiers", false, shifterSpawnRate); - - mayorSpawnRate = CustomOption.Create(80, cs(Mayor.color, "Mayor"), rates, null, true); - - engineerSpawnRate = CustomOption.Create(90, cs(Engineer.color, "Engineer"), rates, null, true); - engineerNumberOfFixes = CustomOption.Create(91, "Number Of Sabotage Fixes", 1f, 1f, 3f, 1f, engineerSpawnRate); - engineerHighlightForImpostors = CustomOption.Create(92, "Impostors See Vents Highlighted", true, engineerSpawnRate); - engineerHighlightForTeamJackal = CustomOption.Create(93, "Jackal and Sidekick See Vents Highlighted ", true, engineerSpawnRate); - - sheriffSpawnRate = CustomOption.Create(100, cs(Sheriff.color, "Sheriff"), rates, null, true); - sheriffCooldown = CustomOption.Create(101, "Sheriff Cooldown", 30f, 10f, 60f, 2.5f, sheriffSpawnRate); - sheriffCanKillNeutrals = CustomOption.Create(102, "Sheriff Can Kill Neutrals", false, sheriffSpawnRate); - deputySpawnRate = CustomOption.Create(103, "Sheriff Has A Deputy", rates, sheriffSpawnRate); - deputyNumberOfHandcuffs = CustomOption.Create(104, "Deputy Number Of Handcuffs", 3f, 1f, 10f, 1f, deputySpawnRate); - deputyHandcuffCooldown = CustomOption.Create(105, "Handcuff Cooldown", 30f, 10f, 60f, 2.5f, deputySpawnRate); - deputyHandcuffDuration = CustomOption.Create(106, "Handcuff Duration", 15f, 5f, 60f, 2.5f, deputySpawnRate); - deputyKnowsSheriff = CustomOption.Create(107, "Sheriff And Deputy Know Each Other ", true, deputySpawnRate); - deputyGetsPromoted = CustomOption.Create(108, "Deputy Gets Promoted To Sheriff", new string[] { "Off", "On (Immediately)", "On (After Meeting)" }, deputySpawnRate); - deputyKeepsHandcuffs = CustomOption.Create(109, "Deputy Keeps Handcuffs When Promoted", true, deputyGetsPromoted); - - lighterSpawnRate = CustomOption.Create(110, cs(Lighter.color, "Lighter"), rates, null, true); - lighterModeLightsOnVision = CustomOption.Create(111, "Lighter Mode Vision On Lights On", 2f, 0.25f, 5f, 0.25f, lighterSpawnRate); - lighterModeLightsOffVision = CustomOption.Create(112, "Lighter Mode Vision On Lights Off", 0.75f, 0.25f, 5f, 0.25f, lighterSpawnRate); - lighterCooldown = CustomOption.Create(113, "Lighter Cooldown", 30f, 5f, 120f, 5f, lighterSpawnRate); - lighterDuration = CustomOption.Create(114, "Lighter Duration", 5f, 2.5f, 60f, 2.5f, lighterSpawnRate); - - detectiveSpawnRate = CustomOption.Create(120, cs(Detective.color, "Detective"), rates, null, true); - detectiveAnonymousFootprints = CustomOption.Create(121, "Anonymous Footprints", false, detectiveSpawnRate); - detectiveFootprintIntervall = CustomOption.Create(122, "Footprint Intervall", 0.5f, 0.25f, 10f, 0.25f, detectiveSpawnRate); - detectiveFootprintDuration = CustomOption.Create(123, "Footprint Duration", 5f, 0.25f, 10f, 0.25f, detectiveSpawnRate); - detectiveReportNameDuration = CustomOption.Create(124, "Time Where Detective Reports Will Have Name", 0, 0, 60, 2.5f, detectiveSpawnRate); - detectiveReportColorDuration = CustomOption.Create(125, "Time Where Detective Reports Will Have Color Type", 20, 0, 120, 2.5f, detectiveSpawnRate); - - timeMasterSpawnRate = CustomOption.Create(130, cs(TimeMaster.color, "Time Master"), rates, null, true); - timeMasterCooldown = CustomOption.Create(131, "Time Master Cooldown", 30f, 10f, 120f, 2.5f, timeMasterSpawnRate); - timeMasterRewindTime = CustomOption.Create(132, "Rewind Time", 3f, 1f, 10f, 1f, timeMasterSpawnRate); - timeMasterShieldDuration = CustomOption.Create(133, "Time Master Shield Duration", 3f, 1f, 20f, 1f, timeMasterSpawnRate); - - medicSpawnRate = CustomOption.Create(140, cs(Medic.color, "Medic"), rates, null, true); - medicShowShielded = CustomOption.Create(143, "Show Shielded Player", new string[] {"Everyone", "Shielded + Medic", "Medic"}, medicSpawnRate); - medicShowAttemptToShielded = CustomOption.Create(144, "Shielded Player Sees Murder Attempt", false, medicSpawnRate); - medicSetShieldAfterMeeting = CustomOption.Create(145, "Shield Will Be Set After The Next Meeting", false, medicSpawnRate); - medicShowAttemptToMedic = CustomOption.Create(146, "Medic Sees Murder Attempt On Shielded Player", false, medicSpawnRate); - - swapperSpawnRate = CustomOption.Create(150, cs(Swapper.color, "Swapper"), rates, null, true); - swapperCanCallEmergency = CustomOption.Create(151, "Swapper can call emergency meeting", false, swapperSpawnRate); - swapperCanOnlySwapOthers = CustomOption.Create(152, "Swapper can only swap others", false, swapperSpawnRate); - - seerSpawnRate = CustomOption.Create(160, cs(Seer.color, "Seer"), rates, null, true); - seerMode = CustomOption.Create(161, "Seer Mode", new string[]{ "Show Death Flash + Souls", "Show Death Flash", "Show Souls"}, seerSpawnRate); - seerLimitSoulDuration = CustomOption.Create(163, "Seer Limit Soul Duration", false, seerSpawnRate); - seerSoulDuration = CustomOption.Create(162, "Seer Soul Duration", 15f, 0f, 120f, 5f, seerLimitSoulDuration); + crewmateRolesCountMin = CustomOption.Create(300, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Crewmate Roles"), 0f, 0f, 15f, 1f, null, true); + crewmateRolesCountMax = CustomOption.Create(301, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Crewmate Roles"), 0f, 0f, 15f, 1f); + neutralRolesCountMin = CustomOption.Create(302, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Neutral Roles"), 0f, 0f, 15f, 1f); + neutralRolesCountMax = CustomOption.Create(303, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Neutral Roles"), 0f, 0f, 15f, 1f); + impostorRolesCountMin = CustomOption.Create(304, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Impostor Roles"), 0f, 0f, 3f, 1f); + impostorRolesCountMax = CustomOption.Create(305, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Impostor Roles"), 0f, 0f, 3f, 1f); + modifiersCountMin = CustomOption.Create(306, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Minimum Modifiers"), 0f, 0f, 15f, 1f); + modifiersCountMax = CustomOption.Create(307, Types.General, cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Maximum Modifiers"), 0f, 0f, 15f, 1f); + + mafiaSpawnRate = CustomOption.Create(10, Types.Impostor, cs(Janitor.color, "Mafia"), rates, null, true); + janitorCooldown = CustomOption.Create(11, Types.Impostor, "Janitor Cooldown", 30f, 10f, 60f, 2.5f, mafiaSpawnRate); + + morphlingSpawnRate = CustomOption.Create(20, Types.Impostor, cs(Morphling.color, "Morphling"), rates, null, true); + morphlingCooldown = CustomOption.Create(21, Types.Impostor, "Morphling Cooldown", 30f, 10f, 60f, 2.5f, morphlingSpawnRate); + morphlingDuration = CustomOption.Create(22, Types.Impostor, "Morph Duration", 10f, 1f, 20f, 0.5f, morphlingSpawnRate); + + camouflagerSpawnRate = CustomOption.Create(30, Types.Impostor, cs(Camouflager.color, "Camouflager"), rates, null, true); + camouflagerCooldown = CustomOption.Create(31, Types.Impostor, "Camouflager Cooldown", 30f, 10f, 60f, 2.5f, camouflagerSpawnRate); + camouflagerDuration = CustomOption.Create(32, Types.Impostor, "Camo Duration", 10f, 1f, 20f, 0.5f, camouflagerSpawnRate); + + vampireSpawnRate = CustomOption.Create(40, Types.Impostor, cs(Vampire.color, "Vampire"), rates, null, true); + vampireKillDelay = CustomOption.Create(41, Types.Impostor, "Vampire Kill Delay", 10f, 1f, 20f, 1f, vampireSpawnRate); + vampireCooldown = CustomOption.Create(42, Types.Impostor, "Vampire Cooldown", 30f, 10f, 60f, 2.5f, vampireSpawnRate); + vampireCanKillNearGarlics = CustomOption.Create(43, Types.Impostor, "Vampire Can Kill Near Garlics", true, vampireSpawnRate); + + eraserSpawnRate = CustomOption.Create(230, Types.Impostor, cs(Eraser.color, "Eraser"), rates, null, true); + eraserCooldown = CustomOption.Create(231, Types.Impostor, "Eraser Cooldown", 30f, 10f, 120f, 5f, eraserSpawnRate); + eraserCanEraseAnyone = CustomOption.Create(232, Types.Impostor, "Eraser Can Erase Anyone", false, eraserSpawnRate); + + tricksterSpawnRate = CustomOption.Create(250, Types.Impostor, cs(Trickster.color, "Trickster"), rates, null, true); + tricksterPlaceBoxCooldown = CustomOption.Create(251, Types.Impostor, "Trickster Box Cooldown", 10f, 2.5f, 30f, 2.5f, tricksterSpawnRate); + tricksterLightsOutCooldown = CustomOption.Create(252, Types.Impostor, "Trickster Lights Out Cooldown", 30f, 10f, 60f, 5f, tricksterSpawnRate); + tricksterLightsOutDuration = CustomOption.Create(253, Types.Impostor, "Trickster Lights Out Duration", 15f, 5f, 60f, 2.5f, tricksterSpawnRate); + + cleanerSpawnRate = CustomOption.Create(260, Types.Impostor, cs(Cleaner.color, "Cleaner"), rates, null, true); + cleanerCooldown = CustomOption.Create(261, Types.Impostor, "Cleaner Cooldown", 30f, 10f, 60f, 2.5f, cleanerSpawnRate); + + warlockSpawnRate = CustomOption.Create(270, Types.Impostor, cs(Cleaner.color, "Warlock"), rates, null, true); + warlockCooldown = CustomOption.Create(271, Types.Impostor, "Warlock Cooldown", 30f, 10f, 60f, 2.5f, warlockSpawnRate); + warlockRootTime = CustomOption.Create(272, Types.Impostor, "Warlock Root Time", 5f, 0f, 15f, 1f, warlockSpawnRate); + + bountyHunterSpawnRate = CustomOption.Create(320, Types.Impostor, cs(BountyHunter.color, "Bounty Hunter"), rates, null, true); + bountyHunterBountyDuration = CustomOption.Create(321, Types.Impostor, "Duration After Which Bounty Changes", 60f, 10f, 180f, 10f, bountyHunterSpawnRate); + bountyHunterReducedCooldown = CustomOption.Create(322, Types.Impostor, "Cooldown After Killing Bounty", 2.5f, 0f, 30f, 2.5f, bountyHunterSpawnRate); + bountyHunterPunishmentTime = CustomOption.Create(323, Types.Impostor, "Additional Cooldown After Killing Others", 20f, 0f, 60f, 2.5f, bountyHunterSpawnRate); + bountyHunterShowArrow = CustomOption.Create(324, Types.Impostor, "Show Arrow Pointing Towards The Bounty", true, bountyHunterSpawnRate); + bountyHunterArrowUpdateIntervall = CustomOption.Create(325, Types.Impostor, "Arrow Update Intervall", 15f, 2.5f, 60f, 2.5f, bountyHunterShowArrow); + + witchSpawnRate = CustomOption.Create(370, Types.Impostor, cs(Witch.color, "Witch"), rates, null, true); + witchCooldown = CustomOption.Create(371, Types.Impostor, "Witch Spell Casting Cooldown", 30f, 10f, 120f, 5f, witchSpawnRate); + witchAdditionalCooldown = CustomOption.Create(372, Types.Impostor, "Witch Additional Cooldown", 10f, 0f, 60f, 5f, witchSpawnRate); + witchCanSpellAnyone = CustomOption.Create(373, Types.Impostor, "Witch Can Spell Anyone", false, witchSpawnRate); + witchSpellCastingDuration = CustomOption.Create(374, Types.Impostor, "Spell Casting Duration", 1f, 0f, 10f, 1f, witchSpawnRate); + witchTriggerBothCooldowns = CustomOption.Create(375, Types.Impostor, "Trigger Both Cooldowns", true, witchSpawnRate); + witchVoteSavesTargets = CustomOption.Create(376, Types.Impostor, "Voting The Witch Saves All The Targets", true, witchSpawnRate); + + ninjaSpawnRate = CustomOption.Create(380, Types.Impostor, cs(Ninja.color, "Ninja"), rates, null, true); + ninjaCooldown = CustomOption.Create(381, Types.Impostor, "Ninja Mark Cooldown", 30f, 10f, 120f, 5f, ninjaSpawnRate); + ninjaKnowsTargetLocation = CustomOption.Create(382, Types.Impostor, "Ninja Knows Location Of Target", true, ninjaSpawnRate); + ninjaTraceTime = CustomOption.Create(383, Types.Impostor, "Trace Duration", 5f, 1f, 20f, 0.5f, ninjaSpawnRate); + ninjaTraceColorTime = CustomOption.Create(384, Types.Impostor, "Time Till Trace Color Has Faded", 2f, 0f, 20f, 0.5f, ninjaSpawnRate); + guesserSpawnRate = CustomOption.Create(310, Types.Neutral, cs(Guesser.color, "Guesser"), rates, null, true); + guesserIsImpGuesserRate = CustomOption.Create(311, Types.Neutral, "Chance That The Guesser Is An Impostor", rates, guesserSpawnRate); + guesserNumberOfShots = CustomOption.Create(312, Types.Neutral, "Guesser Number Of Shots", 2f, 1f, 15f, 1f, guesserSpawnRate); + guesserHasMultipleShotsPerMeeting = CustomOption.Create(313, Types.Neutral, "Guesser Can Shoot Multiple Times Per Meeting", false, guesserSpawnRate); + guesserShowInfoInGhostChat = CustomOption.Create(314, Types.Neutral, "Guesses Visible In Ghost Chat", true, guesserSpawnRate); + guesserKillsThroughShield = CustomOption.Create(315, Types.Neutral, "Guesses Ignore The Medic Shield", true, guesserSpawnRate); + guesserEvilCanKillSpy = CustomOption.Create(316, Types.Neutral, "Evil Guesser Can Guess The Spy", true, guesserSpawnRate); + guesserSpawnBothRate = CustomOption.Create(317, Types.Neutral, "Both Guesser Spawn Rate", rates, guesserSpawnRate); + guesserCantGuessSnitchIfTaksDone = CustomOption.Create(318, Types.Neutral, "Guesser Can't Guess Snitch When Tasks Completed", true, guesserSpawnRate); + + jesterSpawnRate = CustomOption.Create(60, Types.Neutral, cs(Jester.color, "Jester"), rates, null, true); + jesterCanCallEmergency = CustomOption.Create(61, Types.Neutral, "Jester Can Call Emergency Meeting", true, jesterSpawnRate); + jesterHasImpostorVision = CustomOption.Create(62, Types.Neutral, "Jester Has Impostor Vision", false, jesterSpawnRate); + + arsonistSpawnRate = CustomOption.Create(290, Types.Neutral, cs(Arsonist.color, "Arsonist"), rates, null, true); + arsonistCooldown = CustomOption.Create(291, Types.Neutral, "Arsonist Cooldown", 12.5f, 2.5f, 60f, 2.5f, arsonistSpawnRate); + arsonistDuration = CustomOption.Create(292, Types.Neutral, "Arsonist Douse Duration", 3f, 1f, 10f, 1f, arsonistSpawnRate); + + jackalSpawnRate = CustomOption.Create(220, Types.Neutral, cs(Jackal.color, "Jackal"), rates, null, true); + jackalKillCooldown = CustomOption.Create(221, Types.Neutral, "Jackal/Sidekick Kill Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate); + jackalCreateSidekickCooldown = CustomOption.Create(222, Types.Neutral, "Jackal Create Sidekick Cooldown", 30f, 10f, 60f, 2.5f, jackalSpawnRate); + jackalCanUseVents = CustomOption.Create(223, Types.Neutral, "Jackal Can Use Vents", true, jackalSpawnRate); + jackalCanCreateSidekick = CustomOption.Create(224, Types.Neutral, "Jackal Can Create A Sidekick", false, jackalSpawnRate); + sidekickPromotesToJackal = CustomOption.Create(225, Types.Neutral, "Sidekick Gets Promoted To Jackal On Jackal Death", false, jackalCanCreateSidekick); + sidekickCanKill = CustomOption.Create(226, Types.Neutral, "Sidekick Can Kill", false, jackalCanCreateSidekick); + sidekickCanUseVents = CustomOption.Create(227, Types.Neutral, "Sidekick Can Use Vents", true, jackalCanCreateSidekick); + jackalPromotedFromSidekickCanCreateSidekick = CustomOption.Create(228, Types.Neutral, "Jackals Promoted From Sidekick Can Create A Sidekick", true, sidekickPromotesToJackal); + jackalCanCreateSidekickFromImpostor = CustomOption.Create(229, Types.Neutral, "Jackals Can Make An Impostor To His Sidekick", true, jackalCanCreateSidekick); + jackalAndSidekickHaveImpostorVision = CustomOption.Create(430, Types.Neutral, "Jackal And Sidekick Have Impostor Vision", false, jackalSpawnRate); + + vultureSpawnRate = CustomOption.Create(340, Types.Neutral, cs(Vulture.color, "Vulture"), rates, null, true); + vultureCooldown = CustomOption.Create(341, Types.Neutral, "Vulture Cooldown", 15f, 10f, 60f, 2.5f, vultureSpawnRate); + vultureNumberToWin = CustomOption.Create(342, Types.Neutral, "Number Of Corpses Needed To Be Eaten", 4f, 1f, 10f, 1f, vultureSpawnRate); + vultureCanUseVents = CustomOption.Create(343, Types.Neutral, "Vulture Can Use Vents", true, vultureSpawnRate); + vultureShowArrows = CustomOption.Create(344, Types.Neutral, "Show Arrows Pointing Towards The Corpses", true, vultureSpawnRate); + + lawyerSpawnRate = CustomOption.Create(350, Types.Neutral, cs(Lawyer.color, "Lawyer"), rates, null, true); + lawyerTargetCanBeJester = CustomOption.Create(351, Types.Neutral, "Lawyer Target Can Be The Jester", false, lawyerSpawnRate); + lawyerWinsAfterMeetings = CustomOption.Create(352, Types.Neutral, "Lawyer Wins After Meetings", false, lawyerSpawnRate); + lawyerNeededMeetings = CustomOption.Create(353, Types.Neutral, "Lawyer Needed Meetings To Win", 5f, 1f, 15f, 1f, lawyerWinsAfterMeetings); + lawyerVision = CustomOption.Create(354, Types.Neutral, "Lawyer Vision", 1f, 0.25f, 3f, 0.25f, lawyerSpawnRate); + lawyerKnowsRole = CustomOption.Create(355, Types.Neutral, "Lawyer Knows Target Role", false, lawyerSpawnRate); + pursuerCooldown = CustomOption.Create(356, Types.Neutral, "Pursuer Blank Cooldown", 30f, 5f, 60f, 2.5f, lawyerSpawnRate); + pursuerBlanksNumber = CustomOption.Create(357, Types.Neutral, "Pursuer Number Of Blanks", 5f, 1f, 20f, 1f, lawyerSpawnRate); + + shifterSpawnRate = CustomOption.Create(70, Types.Crewmate, cs(Shifter.color, "Shifter"), rates, null, true); + shifterShiftsModifiers = CustomOption.Create(71, Types.Crewmate, "Shifter Shifts Modifiers", false, shifterSpawnRate); + + mayorSpawnRate = CustomOption.Create(80, Types.Crewmate, cs(Mayor.color, "Mayor"), rates, null, true); + + mayorCanSeeVoteColors = CustomOption.Create(81, Types.Crewmate, "Mayor Can See Vote Colors", false, mayorSpawnRate); + mayorTasksNeededToSeeVoteColors = CustomOption.Create(82, Types.Crewmate, "Completed Tasks Needed To See Vote Colors", 5f, 0f, 20f, 1f, mayorCanSeeVoteColors); + + engineerSpawnRate = CustomOption.Create(90, Types.Crewmate, cs(Engineer.color, "Engineer"), rates, null, true); + engineerNumberOfFixes = CustomOption.Create(91, Types.Crewmate, "Number Of Sabotage Fixes", 1f, 1f, 3f, 1f, engineerSpawnRate); + engineerHighlightForImpostors = CustomOption.Create(92, Types.Crewmate, "Impostors See Vents Highlighted", true, engineerSpawnRate); + engineerHighlightForTeamJackal = CustomOption.Create(93, Types.Crewmate, "Jackal and Sidekick See Vents Highlighted ", true, engineerSpawnRate); + + sheriffSpawnRate = CustomOption.Create(100, Types.Crewmate, cs(Sheriff.color, "Sheriff"), rates, null, true); + sheriffCooldown = CustomOption.Create(101, Types.Crewmate, "Sheriff Cooldown", 30f, 10f, 60f, 2.5f, sheriffSpawnRate); + sheriffCanKillNeutrals = CustomOption.Create(102, Types.Crewmate, "Sheriff Can Kill Neutrals", false, sheriffSpawnRate); + deputySpawnRate = CustomOption.Create(103, Types.Crewmate, "Sheriff Has A Deputy", rates, sheriffSpawnRate); + deputyNumberOfHandcuffs = CustomOption.Create(104, Types.Crewmate, "Deputy Number Of Handcuffs", 3f, 1f, 10f, 1f, deputySpawnRate); + deputyHandcuffCooldown = CustomOption.Create(105, Types.Crewmate, "Handcuff Cooldown", 30f, 10f, 60f, 2.5f, deputySpawnRate); + deputyHandcuffDuration = CustomOption.Create(106, Types.Crewmate, "Handcuff Duration", 15f, 5f, 60f, 2.5f, deputySpawnRate); + deputyKnowsSheriff = CustomOption.Create(107, Types.Crewmate, "Sheriff And Deputy Know Each Other ", true, deputySpawnRate); + deputyGetsPromoted = CustomOption.Create(108, Types.Crewmate, "Deputy Gets Promoted To Sheriff", new string[] { "Off", "On (Immediately)", "On (After Meeting)" }, deputySpawnRate); + deputyKeepsHandcuffs = CustomOption.Create(109, Types.Crewmate, "Deputy Keeps Handcuffs When Promoted", true, deputyGetsPromoted); + + lighterSpawnRate = CustomOption.Create(110, Types.Crewmate, cs(Lighter.color, "Lighter"), rates, null, true); + lighterModeLightsOnVision = CustomOption.Create(111, Types.Crewmate, "Lighter Mode Vision On Lights On", 2f, 0.25f, 5f, 0.25f, lighterSpawnRate); + lighterModeLightsOffVision = CustomOption.Create(112, Types.Crewmate, "Lighter Mode Vision On Lights Off", 0.75f, 0.25f, 5f, 0.25f, lighterSpawnRate); + lighterCooldown = CustomOption.Create(113, Types.Crewmate, "Lighter Cooldown", 30f, 5f, 120f, 5f, lighterSpawnRate); + lighterDuration = CustomOption.Create(114, Types.Crewmate, "Lighter Duration", 5f, 2.5f, 60f, 2.5f, lighterSpawnRate); + + detectiveSpawnRate = CustomOption.Create(120, Types.Crewmate, cs(Detective.color, "Detective"), rates, null, true); + detectiveAnonymousFootprints = CustomOption.Create(121, Types.Crewmate, "Anonymous Footprints", false, detectiveSpawnRate); + detectiveFootprintIntervall = CustomOption.Create(122, Types.Crewmate, "Footprint Intervall", 0.5f, 0.25f, 10f, 0.25f, detectiveSpawnRate); + detectiveFootprintDuration = CustomOption.Create(123, Types.Crewmate, "Footprint Duration", 5f, 0.25f, 10f, 0.25f, detectiveSpawnRate); + detectiveReportNameDuration = CustomOption.Create(124, Types.Crewmate, "Time Where Detective Reports Will Have Name", 0, 0, 60, 2.5f, detectiveSpawnRate); + detectiveReportColorDuration = CustomOption.Create(125, Types.Crewmate, "Time Where Detective Reports Will Have Color Type", 20, 0, 120, 2.5f, detectiveSpawnRate); + + timeMasterSpawnRate = CustomOption.Create(130, Types.Crewmate, cs(TimeMaster.color, "Time Master"), rates, null, true); + timeMasterCooldown = CustomOption.Create(131, Types.Crewmate, "Time Master Cooldown", 30f, 10f, 120f, 2.5f, timeMasterSpawnRate); + timeMasterRewindTime = CustomOption.Create(132, Types.Crewmate, "Rewind Time", 3f, 1f, 10f, 1f, timeMasterSpawnRate); + timeMasterShieldDuration = CustomOption.Create(133, Types.Crewmate, "Time Master Shield Duration", 3f, 1f, 20f, 1f, timeMasterSpawnRate); + + medicSpawnRate = CustomOption.Create(140, Types.Crewmate, cs(Medic.color, "Medic"), rates, null, true); + medicShowShielded = CustomOption.Create(143, Types.Crewmate, "Show Shielded Player", new string[] {"Everyone", "Shielded + Medic", "Medic"}, medicSpawnRate); + medicShowAttemptToShielded = CustomOption.Create(144, Types.Crewmate, "Shielded Player Sees Murder Attempt", false, medicSpawnRate); + medicSetOrShowShieldAfterMeeting = CustomOption.Create(145, Types.Crewmate, "Shield Will Be Activated", new string[] { "Instantly", "Instantly, Visible\nAfter Meeting", "After Meeting" }, medicSpawnRate); + + medicShowAttemptToMedic = CustomOption.Create(146, Types.Crewmate, "Medic Sees Murder Attempt On Shielded Player", false, medicSpawnRate); + + swapperSpawnRate = CustomOption.Create(150, Types.Crewmate, cs(Swapper.color, "Swapper"), rates, null, true); + swapperCanCallEmergency = CustomOption.Create(151, Types.Crewmate, "Swapper Can Call Emergency Meeting", false, swapperSpawnRate); + swapperCanOnlySwapOthers = CustomOption.Create(152, Types.Crewmate, "Swapper Can Only Swap Others", false, swapperSpawnRate); + + swapperSwapsNumber = CustomOption.Create(153, Types.Crewmate, "Initial Swap Charges", 1f, 0f, 5f, 1f, swapperSpawnRate); + swapperRechargeTasksNumber = CustomOption.Create(154, Types.Crewmate, "Number Of Tasks Needed For Recharging", 2f, 1f, 10f, 1f, swapperSpawnRate); + + + seerSpawnRate = CustomOption.Create(160, Types.Crewmate, cs(Seer.color, "Seer"), rates, null, true); + seerMode = CustomOption.Create(161, Types.Crewmate, "Seer Mode", new string[]{ "Show Death Flash + Souls", "Show Death Flash", "Show Souls"}, seerSpawnRate); + seerLimitSoulDuration = CustomOption.Create(163, Types.Crewmate, "Seer Limit Soul Duration", false, seerSpawnRate); + seerSoulDuration = CustomOption.Create(162, Types.Crewmate, "Seer Soul Duration", 15f, 0f, 120f, 5f, seerLimitSoulDuration); - hackerSpawnRate = CustomOption.Create(170, cs(Hacker.color, "Hacker"), rates, null, true); - hackerCooldown = CustomOption.Create(171, "Hacker Cooldown", 30f, 5f, 60f, 5f, hackerSpawnRate); - hackerHackeringDuration = CustomOption.Create(172, "Hacker Duration", 10f, 2.5f, 60f, 2.5f, hackerSpawnRate); - hackerOnlyColorType = CustomOption.Create(173, "Hacker Only Sees Color Type", false, hackerSpawnRate); - hackerToolsNumber = CustomOption.Create(174, "Max Mobile Gadget Charges", 5f, 1f, 30f, 1f, hackerSpawnRate); - hackerRechargeTasksNumber = CustomOption.Create(175, "Number Of Tasks Needed For Recharging", 2f, 1f, 5f, 1f, hackerSpawnRate); - hackerNoMove = CustomOption.Create(176, "Cant Move During Mobile Gadget Duration", true, hackerSpawnRate); - - trackerSpawnRate = CustomOption.Create(200, cs(Tracker.color, "Tracker"), rates, null, true); - trackerUpdateIntervall = CustomOption.Create(201, "Tracker Update Intervall", 5f, 1f, 30f, 1f, trackerSpawnRate); - trackerResetTargetAfterMeeting = CustomOption.Create(202, "Tracker Reset Target After Meeting", false, trackerSpawnRate); - trackerCanTrackCorpses = CustomOption.Create(203, "Tracker Can Track Corpses", true, trackerSpawnRate); - trackerCorpsesTrackingCooldown = CustomOption.Create(204, "Corpses Tracking Cooldown", 30f, 5f, 120f, 5f, trackerCanTrackCorpses); - trackerCorpsesTrackingDuration = CustomOption.Create(205, "Corpses Tracking Duration", 5f, 2.5f, 30f, 2.5f, trackerCanTrackCorpses); + hackerSpawnRate = CustomOption.Create(170, Types.Crewmate, cs(Hacker.color, "Hacker"), rates, null, true); + hackerCooldown = CustomOption.Create(171, Types.Crewmate, "Hacker Cooldown", 30f, 5f, 60f, 5f, hackerSpawnRate); + hackerHackeringDuration = CustomOption.Create(172, Types.Crewmate, "Hacker Duration", 10f, 2.5f, 60f, 2.5f, hackerSpawnRate); + hackerOnlyColorType = CustomOption.Create(173, Types.Crewmate, "Hacker Only Sees Color Type", false, hackerSpawnRate); + hackerToolsNumber = CustomOption.Create(174, Types.Crewmate, "Max Mobile Gadget Charges", 5f, 1f, 30f, 1f, hackerSpawnRate); + hackerRechargeTasksNumber = CustomOption.Create(175, Types.Crewmate, "Number Of Tasks Needed For Recharging", 2f, 1f, 5f, 1f, hackerSpawnRate); + hackerNoMove = CustomOption.Create(176, Types.Crewmate, "Cant Move During Mobile Gadget Duration", true, hackerSpawnRate); + + trackerSpawnRate = CustomOption.Create(200, Types.Crewmate, cs(Tracker.color, "Tracker"), rates, null, true); + trackerUpdateIntervall = CustomOption.Create(201, Types.Crewmate, "Tracker Update Intervall", 5f, 1f, 30f, 1f, trackerSpawnRate); + trackerResetTargetAfterMeeting = CustomOption.Create(202, Types.Crewmate, "Tracker Reset Target After Meeting", false, trackerSpawnRate); + trackerCanTrackCorpses = CustomOption.Create(203, Types.Crewmate, "Tracker Can Track Corpses", true, trackerSpawnRate); + trackerCorpsesTrackingCooldown = CustomOption.Create(204, Types.Crewmate, "Corpses Tracking Cooldown", 30f, 5f, 120f, 5f, trackerCanTrackCorpses); + trackerCorpsesTrackingDuration = CustomOption.Create(205, Types.Crewmate, "Corpses Tracking Duration", 5f, 2.5f, 30f, 2.5f, trackerCanTrackCorpses); - snitchSpawnRate = CustomOption.Create(210, cs(Snitch.color, "Snitch"), rates, null, true); - snitchLeftTasksForReveal = CustomOption.Create(211, "Task Count Where The Snitch Will Be Revealed", 1f, 0f, 5f, 1f, snitchSpawnRate); - snitchIncludeTeamJackal = CustomOption.Create(212, "Include Team Jackal", false, snitchSpawnRate); - snitchTeamJackalUseDifferentArrowColor = CustomOption.Create(213, "Use Different Arrow Color For Team Jackal", true, snitchIncludeTeamJackal); - - spySpawnRate = CustomOption.Create(240, cs(Spy.color, "Spy"), rates, null, true); - spyCanDieToSheriff = CustomOption.Create(241, "Spy Can Die To Sheriff", false, spySpawnRate); - spyImpostorsCanKillAnyone = CustomOption.Create(242, "Impostors Can Kill Anyone If There Is A Spy", true, spySpawnRate); - spyCanEnterVents = CustomOption.Create(243, "Spy Can Enter Vents", false, spySpawnRate); - spyHasImpostorVision = CustomOption.Create(244, "Spy Has Impostor Vision", false, spySpawnRate); - - securityGuardSpawnRate = CustomOption.Create(280, cs(SecurityGuard.color, "Security Guard"), rates, null, true); - securityGuardCooldown = CustomOption.Create(281, "Security Guard Cooldown", 30f, 10f, 60f, 2.5f, securityGuardSpawnRate); - securityGuardTotalScrews = CustomOption.Create(282, "Security Guard Number Of Screws", 7f, 1f, 15f, 1f, securityGuardSpawnRate); - securityGuardCamPrice = CustomOption.Create(283, "Number Of Screws Per Cam", 2f, 1f, 15f, 1f, securityGuardSpawnRate); - securityGuardVentPrice = CustomOption.Create(284, "Number Of Screws Per Vent", 1f, 1f, 15f, 1f, securityGuardSpawnRate); - securityGuardCamDuration = CustomOption.Create(285, "Security Guard Duration", 10f, 2.5f, 60f, 2.5f, securityGuardSpawnRate); - securityGuardCamMaxCharges = CustomOption.Create(286, "Gadged Max Charges", 5f, 1f, 30f, 1f, securityGuardSpawnRate); - securityGuardCamRechargeTasksNumber = CustomOption.Create(287, "Number Of Tasks Needed For Recharging", 3f, 1f, 10f, 1f, securityGuardSpawnRate); - securityGuardNoMove = CustomOption.Create(288, "Cant Move During Cam Duration", true, securityGuardSpawnRate); - - baitSpawnRate = CustomOption.Create(330, cs(Bait.color, "Bait"), rates, null, true); - baitHighlightAllVents = CustomOption.Create(331, "Highlight All Vents If A Vent Is Occupied", false, baitSpawnRate); - baitReportDelay = CustomOption.Create(332, "Bait Report Delay", 0f, 0f, 10f, 1f, baitSpawnRate); - baitShowKillFlash = CustomOption.Create(333, "Warn The Killer With A Flash", true, baitSpawnRate); - - mediumSpawnRate = CustomOption.Create(360, cs(Medium.color, "Medium"), rates, null, true); - mediumCooldown = CustomOption.Create(361, "Medium Questioning Cooldown", 30f, 5f, 120f, 5f, mediumSpawnRate); - mediumDuration = CustomOption.Create(362, "Medium Questioning Duration", 3f, 0f, 15f, 1f, mediumSpawnRate); - mediumOneTimeUse = CustomOption.Create(363, "Each Soul Can Only Be Questioned Once", false, mediumSpawnRate); + snitchSpawnRate = CustomOption.Create(210, Types.Crewmate, cs(Snitch.color, "Snitch"), rates, null, true); + snitchLeftTasksForReveal = CustomOption.Create(211, Types.Crewmate, "Task Count Where The Snitch Will Be Revealed", 1f, 0f, 5f, 1f, snitchSpawnRate); + snitchIncludeTeamJackal = CustomOption.Create(212, Types.Crewmate, "Include Team Jackal", false, snitchSpawnRate); + snitchTeamJackalUseDifferentArrowColor = CustomOption.Create(213, Types.Crewmate, "Use Different Arrow Color For Team Jackal", true, snitchIncludeTeamJackal); + + spySpawnRate = CustomOption.Create(240, Types.Crewmate, cs(Spy.color, "Spy"), rates, null, true); + spyCanDieToSheriff = CustomOption.Create(241, Types.Crewmate, "Spy Can Die To Sheriff", false, spySpawnRate); + spyImpostorsCanKillAnyone = CustomOption.Create(242, Types.Crewmate, "Impostors Can Kill Anyone If There Is A Spy", true, spySpawnRate); + spyCanEnterVents = CustomOption.Create(243, Types.Crewmate, "Spy Can Enter Vents", false, spySpawnRate); + spyHasImpostorVision = CustomOption.Create(244, Types.Crewmate, "Spy Has Impostor Vision", false, spySpawnRate); + + portalmakerSpawnRate = CustomOption.Create(390, Types.Crewmate, cs(Portalmaker.color, "Portalmaker"), rates, null, true); + portalmakerCooldown = CustomOption.Create(391, Types.Crewmate, "Portalmaker Cooldown", 30f, 10f, 60f, 2.5f, portalmakerSpawnRate); + portalmakerUsePortalCooldown = CustomOption.Create(392, Types.Crewmate, "Use Portal Cooldown", 30f, 10f, 60f, 2.5f, portalmakerSpawnRate); + portalmakerLogOnlyColorType = CustomOption.Create(393, Types.Crewmate, "Portalmaker Log Only Shows Color Type", true, portalmakerSpawnRate); + portalmakerLogHasTime = CustomOption.Create(394, Types.Crewmate, "Log Shows Time", true, portalmakerSpawnRate); + securityGuardSpawnRate = CustomOption.Create(280, Types.Crewmate, cs(SecurityGuard.color, "Security Guard"), rates, null, true); + securityGuardCooldown = CustomOption.Create(281, Types.Crewmate, "Security Guard Cooldown", 30f, 10f, 60f, 2.5f, securityGuardSpawnRate); + securityGuardTotalScrews = CustomOption.Create(282, Types.Crewmate, "Security Guard Number Of Screws", 7f, 1f, 15f, 1f, securityGuardSpawnRate); + securityGuardCamPrice = CustomOption.Create(283, Types.Crewmate, "Number Of Screws Per Cam", 2f, 1f, 15f, 1f, securityGuardSpawnRate); + securityGuardVentPrice = CustomOption.Create(284, Types.Crewmate, "Number Of Screws Per Vent", 1f, 1f, 15f, 1f, securityGuardSpawnRate); + securityGuardCamDuration = CustomOption.Create(285, Types.Crewmate, "Security Guard Duration", 10f, 2.5f, 60f, 2.5f, securityGuardSpawnRate); + securityGuardCamMaxCharges = CustomOption.Create(286, Types.Crewmate, "Gadged Max Charges", 5f, 1f, 30f, 1f, securityGuardSpawnRate); + securityGuardCamRechargeTasksNumber = CustomOption.Create(287, Types.Crewmate, "Number Of Tasks Needed For Recharging", 3f, 1f, 10f, 1f, securityGuardSpawnRate); + securityGuardNoMove = CustomOption.Create(288, Types.Crewmate, "Cant Move During Cam Duration", true, securityGuardSpawnRate); + + mediumSpawnRate = CustomOption.Create(360, Types.Crewmate, cs(Medium.color, "Medium"), rates, null, true); + mediumCooldown = CustomOption.Create(361, Types.Crewmate, "Medium Questioning Cooldown", 30f, 5f, 120f, 5f, mediumSpawnRate); + mediumDuration = CustomOption.Create(362, Types.Crewmate, "Medium Questioning Duration", 3f, 0f, 15f, 1f, mediumSpawnRate); + mediumOneTimeUse = CustomOption.Create(363, Types.Crewmate, "Each Soul Can Only Be Questioned Once", false, mediumSpawnRate); + + // Modifier + modifierBloody = CustomOption.Create(1000, Types.Modifier, cs(Color.yellow, "Bloody"), rates, null, true); + modifierBloodyQuantity = CustomOption.Create(1001, Types.Modifier, cs(Color.yellow, "Bloody Quantity"), ratesModifier, modifierBloody); + modifierBloodyDuration = CustomOption.Create(1002, Types.Modifier, "Trail Duration", 10f, 3f, 60f, 1f, modifierBloody); + + modifierAntiTeleport = CustomOption.Create(1010, Types.Modifier, cs(Color.yellow, "Anti Teleport"), rates, null, true); + modifierAntiTeleportQuantity = CustomOption.Create(1011, Types.Modifier, cs(Color.yellow, "Anti Teleport Quantity"), ratesModifier, modifierAntiTeleport); + + modifierTieBreaker = CustomOption.Create(1020, Types.Modifier, cs(Color.yellow, "Tie Breaker"), rates, null, true); + + modifierBait = CustomOption.Create(1030, Types.Modifier, cs(Color.yellow, "Bait"), rates, null, true); + modifierBaitQuantity = CustomOption.Create(1031, Types.Modifier, cs(Color.yellow, "Bait Quantity"), ratesModifier, modifierBait); + modifierBaitReportDelayMin = CustomOption.Create(1032, Types.Modifier, "Bait Report Delay Min", 0f, 0f, 10f, 1f, modifierBait); + modifierBaitReportDelayMax = CustomOption.Create(1033, Types.Modifier, "Bait Report Delay Max", 0f, 0f, 10f, 1f, modifierBait); + modifierBaitShowKillFlash = CustomOption.Create(1034, Types.Modifier, "Warn The Killer With A Flash", true, modifierBait); + + modifierLover = CustomOption.Create(1040, Types.Modifier, cs(Color.yellow, "Lovers"), rates, null, true); + modifierLoverImpLoverRate = CustomOption.Create(1041, Types.Modifier, "Chance That One Lover Is Impostor", rates, modifierLover); + modifierLoverBothDie = CustomOption.Create(1042, Types.Modifier, "Both Lovers Die", true, modifierLover); + modifierLoverEnableChat = CustomOption.Create(1043, Types.Modifier, "Enable Lover Chat", true, modifierLover); + + modifierSunglasses = CustomOption.Create(1050, Types.Modifier, cs(Color.yellow, "Sunglasses"), rates, null, true); + modifierSunglassesQuantity = CustomOption.Create(1051, Types.Modifier, cs(Color.yellow, "Sunglasses Quantity"), ratesModifier, modifierSunglasses); + modifierSunglassesVision = CustomOption.Create(1052, Types.Modifier, "Vision With Sunglasses", new string[] { "-10%", "-20%", "-30%", "-40%", "-50%" }, modifierSunglasses); + + modifierMini = CustomOption.Create(1061, Types.Modifier, cs(Color.yellow, "Mini"), rates, null, true); + modifierMiniGrowingUpDuration = CustomOption.Create(1062, Types.Modifier, "Mini Growing Up Duration", 400f, 100f, 1500f, 100f, modifierMini); + + modifierVip = CustomOption.Create(1070, Types.Modifier, cs(Color.yellow, "VIP"), rates, null, true); + modifierVipQuantity = CustomOption.Create(1071, Types.Modifier, cs(Color.yellow, "VIP Quantity"), ratesModifier, modifierVip); + modifierVipShowColor = CustomOption.Create(1072, Types.Modifier, "Show Team Color", true, modifierVip); + + modifierInvert = CustomOption.Create(1080, Types.Modifier, cs(Color.yellow, "Invert"), rates, null, true); + modifierInvertQuantity = CustomOption.Create(1081, Types.Modifier, cs(Color.yellow, "Modifier Quantity"), ratesModifier, modifierInvert); + modifierInvertDuration = CustomOption.Create(1082, Types.Modifier, "Number Of Meetings Inverted", 3f, 1f, 15f, 1f, modifierInvert); // Other options - maxNumberOfMeetings = CustomOption.Create(3, "Number Of Meetings (excluding Mayor meeting)", 10, 0, 15, 1, null, true); - 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); - allowParallelMedBayScans = CustomOption.Create(7, "Allow Parallel MedBay Scans", false); - - dynamicMap = CustomOption.Create(8, "Play On A Random Map", false, null, false); - dynamicMapEnableSkeld = CustomOption.Create(501, "Enable Skeld Rotation", true, dynamicMap, false); - dynamicMapEnableMira = CustomOption.Create(502, "Enable Mira Rotation", true, dynamicMap, false); - dynamicMapEnablePolus = CustomOption.Create(503, "Enable Polus Rotation", true, dynamicMap, false); - dynamicMapEnableAirShip = CustomOption.Create(504, "Enable Airship Rotation", true, dynamicMap, false); + maxNumberOfMeetings = CustomOption.Create(3, Types.General, "Number Of Meetings (excluding Mayor meeting)", 10, 0, 15, 1, null, true); + blockSkippingInEmergencyMeetings = CustomOption.Create(4, Types.General, "Block Skipping In Emergency Meetings", false); + noVoteIsSelfVote = CustomOption.Create(5, Types.General, "No Vote Is Self Vote", false, blockSkippingInEmergencyMeetings); + hidePlayerNames = CustomOption.Create(6, Types.General, "Hide Player Names", false); + allowParallelMedBayScans = CustomOption.Create(7, Types.General, "Allow Parallel MedBay Scans", false); + shieldFirstKill = CustomOption.Create(8, Types.General, "Shield Last Game First Kill", false); + + dynamicMap = CustomOption.Create(500, Types.General, "Play On A Random Map", false, null, false); + dynamicMapEnableSkeld = CustomOption.Create(501, Types.General, "Enable Skeld Rotation", true, dynamicMap, false); + dynamicMapEnableMira = CustomOption.Create(502, Types.General, "Enable Mira Rotation", true, dynamicMap, false); + dynamicMapEnablePolus = CustomOption.Create(503, Types.General, "Enable Polus Rotation", true, dynamicMap, false); + dynamicMapEnableAirShip = CustomOption.Create(504, Types.General, "Enable Airship Rotation", true, dynamicMap, false); blockedRolePairings.Add((byte)RoleId.Vampire, new [] { (byte)RoleId.Warlock}); blockedRolePairings.Add((byte)RoleId.Warlock, new [] { (byte)RoleId.Vampire}); diff --git a/TheOtherRoles/Helpers.cs b/TheOtherRoles/Helpers.cs index 87c6f952c..f1541fff8 100644 --- a/TheOtherRoles/Helpers.cs +++ b/TheOtherRoles/Helpers.cs @@ -125,6 +125,8 @@ public static void refreshRoleDescription(PlayerControl player) { if (roleInfo.name == "Jackal") { var getSidekickText = Jackal.canCreateSidekick ? " and recruit a Sidekick" : ""; task.Text = cs(roleInfo.color, $"{roleInfo.name}: Kill everyone{getSidekickText}"); + } else if (roleInfo.name == "Invert") { + task.Text = cs(roleInfo.color, $"{roleInfo.name}: {roleInfo.shortDescription} ({Invert.meetings})"); } else { task.Text = cs(roleInfo.color, $"{roleInfo.name}: {roleInfo.shortDescription}"); } @@ -207,7 +209,7 @@ public static bool hidePlayerName(PlayerControl source, PlayerControl target) { else if (!MapOptions.hidePlayerNames) return false; // All names are visible else if (source == null || target == null) return true; else if (source == target) return false; // Player sees his own name - else if (source.Data.Role.IsImpostor && (target.Data.Role.IsImpostor || target == Spy.spy)) return false; // Members of team Impostors see the names of Impostors/Spies + else if (source.Data.Role.IsImpostor && (target.Data.Role.IsImpostor || target == Spy.spy || target == Sidekick.sidekick && Sidekick.wasTeamRed || target == Jackal.jackal && Jackal.wasTeamRed)) return false; // Members of team Impostors see the names of Impostors/Spies else if ((source == Lovers.lover1 || source == Lovers.lover2) && (target == Lovers.lover1 || target == Lovers.lover2)) return false; // Members of team Lovers see the names of each other else if ((source == Jackal.jackal || source == Sidekick.sidekick) && (target == Jackal.jackal || target == Sidekick.sidekick || target == Jackal.fakeSidekick)) return false; // Members of team Jackal see the names of each other else if (Deputy.knowsSheriff && (source == Sheriff.sheriff || source == Deputy.deputy) && (target == Sheriff.sheriff || target == Deputy.deputy)) return false; // Sheriff & Deputy see the names of each other @@ -237,6 +239,8 @@ public static void setLook(this PlayerControl target, String playerName, int col else clip = nextSkin.IdleAnim; float progress = playerPhysics.Animator.m_animator.GetCurrentAnimatorStateInfo(0).normalizedTime; playerPhysics.Skin.skin = nextSkin; + if (playerPhysics.Skin.layer.material == DestroyableSingleton.Instance.PlayerMaterial) + PlayerControl.SetPlayerMaterialColors(colorId, playerPhysics.Skin.layer); spriteAnim.Play(clip, 1f); spriteAnim.m_animator.Play("a", 0, progress % 1); spriteAnim.m_animator.Update(0f); @@ -296,6 +300,9 @@ public static MurderAttemptResult checkMuderAttempt(PlayerControl killer, Player 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 first kill attempt + if (MapOptions.shieldFirstKill && MapOptions.firstKillPlayer == target) return MurderAttemptResult.SuppressKill; + // 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); diff --git a/TheOtherRoles/Main.cs b/TheOtherRoles/Main.cs index f8b414411..2976ea871 100644 --- a/TheOtherRoles/Main.cs +++ b/TheOtherRoles/Main.cs @@ -21,7 +21,7 @@ namespace TheOtherRoles public class TheOtherRolesPlugin : BasePlugin { public const string Id = "me.eisbison.theotherroles"; - public const string VersionString = "3.4.5"; + public const string VersionString = "4.0.0"; public static System.Version Version = System.Version.Parse(VersionString); internal static BepInEx.Logging.ManualLogSource Logger; @@ -29,12 +29,13 @@ public class TheOtherRolesPlugin : BasePlugin public Harmony Harmony { get; } = new Harmony(Id); public static TheOtherRolesPlugin Instance; - public static int optionsPage = 1; + public static int optionsPage = 2; public static ConfigEntry DebugMode { get; private set; } public static ConfigEntry StreamerMode { get; set; } public static ConfigEntry GhostsSeeTasks { get; set; } public static ConfigEntry GhostsSeeRoles { get; set; } + public static ConfigEntry GhostsSeeModifier { get; set; } public static ConfigEntry GhostsSeeVotes{ get; set; } public static ConfigEntry ShowRoleSummary { get; set; } public static ConfigEntry ShowLighterDarker { get; set; } @@ -64,6 +65,7 @@ public override void Load() { StreamerMode = Config.Bind("Custom", "Enable Streamer Mode", false); GhostsSeeTasks = Config.Bind("Custom", "Ghosts See Remaining Tasks", true); GhostsSeeRoles = Config.Bind("Custom", "Ghosts See Roles", true); + GhostsSeeModifier = Config.Bind("Custom", "Ghosts See Modifier", true); GhostsSeeVotes = Config.Bind("Custom", "Ghosts See Votes", true); ShowRoleSummary = Config.Bind("Custom", "Show Role Summary", true); ShowLighterDarker = Config.Bind("Custom", "Show Lighter / Darker", true); @@ -86,7 +88,7 @@ public override void Load() { Instance = this; CustomOptionHolder.Load(); CustomColors.Load(); - + Patches.FreeNamePatch.Initialize(); Harmony.PatchAll(); } public static Sprite GetModStamp() { diff --git a/TheOtherRoles/MapOptions.cs b/TheOtherRoles/MapOptions.cs index 49b66694e..e2b165f79 100644 --- a/TheOtherRoles/MapOptions.cs +++ b/TheOtherRoles/MapOptions.cs @@ -12,18 +12,22 @@ static class MapOptions { public static bool noVoteIsSelfVote = false; public static bool hidePlayerNames = false; public static bool ghostsSeeRoles = true; + public static bool ghostsSeeModifier = true; public static bool ghostsSeeTasks = true; public static bool ghostsSeeVotes = true; public static bool showRoleSummary = true; public static bool allowParallelMedBayScans = false; public static bool showLighterDarker = true; public static bool enableHorseMode = false; + public static bool shieldFirstKill = false; // Updating values public static int meetingsCount = 0; public static List camerasToAdd = new List(); public static List ventsToSeal = new List(); public static Dictionary playerIcons = new Dictionary(); + public static string firstKillName; + public static PlayerControl firstKillPlayer; public static void clearAndReloadMapOptions() { meetingsCount = 0; @@ -36,10 +40,13 @@ public static void clearAndReloadMapOptions() { noVoteIsSelfVote = CustomOptionHolder.noVoteIsSelfVote.getBool(); hidePlayerNames = CustomOptionHolder.hidePlayerNames.getBool(); allowParallelMedBayScans = CustomOptionHolder.allowParallelMedBayScans.getBool(); + shieldFirstKill = CustomOptionHolder.shieldFirstKill.getBool(); + firstKillPlayer = null; } public static void reloadPluginOptions() { ghostsSeeRoles = TheOtherRolesPlugin.GhostsSeeRoles.Value; + ghostsSeeModifier = TheOtherRolesPlugin.GhostsSeeModifier.Value; ghostsSeeTasks = TheOtherRolesPlugin.GhostsSeeTasks.Value; ghostsSeeVotes = TheOtherRolesPlugin.GhostsSeeVotes.Value; showRoleSummary = TheOtherRolesPlugin.ShowRoleSummary.Value; @@ -48,4 +55,4 @@ public static void reloadPluginOptions() { Patches.ShouldAlwaysHorseAround.isHorseMode = TheOtherRolesPlugin.EnableHorseMode.Value; } } -} \ No newline at end of file +} diff --git a/TheOtherRoles/Modules/ChatCommands.cs b/TheOtherRoles/Modules/ChatCommands.cs index 6f34cdd9e..2631a797b 100644 --- a/TheOtherRoles/Modules/ChatCommands.cs +++ b/TheOtherRoles/Modules/ChatCommands.cs @@ -88,7 +88,7 @@ public static void Postfix(HudManager __instance) { public static class SetBubbleName { public static void Postfix(ChatBubble __instance, [HarmonyArgument(0)] string playerName) { PlayerControl sourcePlayer = PlayerControl.AllPlayerControls.ToArray().ToList().FirstOrDefault(x => x.Data.PlayerName.Equals(playerName)); - if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.Data.Role.IsImpostor && Spy.spy != null && sourcePlayer.PlayerId == Spy.spy.PlayerId && __instance != null) __instance.NameText.color = Palette.ImpostorRed; + if (PlayerControl.LocalPlayer != null && PlayerControl.LocalPlayer.Data.Role.IsImpostor && (Spy.spy != null && sourcePlayer.PlayerId == Spy.spy.PlayerId || Sidekick.sidekick != null && Sidekick.wasTeamRed && sourcePlayer.PlayerId == Sidekick.sidekick.PlayerId || Jackal.jackal != null && Jackal.wasTeamRed && sourcePlayer.PlayerId == Jackal.jackal.PlayerId) && __instance != null) __instance.NameText.color = Palette.ImpostorRed; } } diff --git a/TheOtherRoles/Modules/CustomHats.cs b/TheOtherRoles/Modules/CustomHats.cs index 0b86984e0..5cd1a0368 100644 --- a/TheOtherRoles/Modules/CustomHats.cs +++ b/TheOtherRoles/Modules/CustomHats.cs @@ -128,8 +128,7 @@ private static Sprite CreateHatSprite(string path, bool fromDisk = false) { private static HatData CreateHatBehaviour(CustomHat ch, bool fromDisk = false, bool testOnly = false) { if (hatShader == null) { - Material tmpShader = new Material("PlayerMaterial"); - tmpShader.shader = Shader.Find("Unlit/PlayerShader"); + Material tmpShader = DestroyableSingleton.Instance.PlayerMaterial; hatShader = tmpShader; } diff --git a/TheOtherRoles/Modules/CustomOptions.cs b/TheOtherRoles/Modules/CustomOptions.cs index 24589c703..4cc2e362c 100644 --- a/TheOtherRoles/Modules/CustomOptions.cs +++ b/TheOtherRoles/Modules/CustomOptions.cs @@ -11,6 +11,14 @@ namespace TheOtherRoles { public class CustomOption { + public enum CustomOptionType { + General, + Impostor, + Neutral, + Crewmate, + Modifier + } + public static List options = new List(); public static int preset = 0; @@ -24,10 +32,11 @@ public class CustomOption { public OptionBehaviour optionBehaviour; public CustomOption parent; public bool isHeader; + public CustomOptionType type; // Option creation - public CustomOption(int id, string name, System.Object[] selections, System.Object defaultValue, CustomOption parent, bool isHeader) { + public CustomOption(int id, CustomOptionType type, string name, System.Object[] selections, System.Object defaultValue, CustomOption parent, bool isHeader) { this.id = id; this.name = parent == null ? name : "- " + name; this.selections = selections; @@ -35,6 +44,7 @@ public CustomOption(int id, string name, System.Object[] selections, System.Obj this.defaultSelection = index >= 0 ? index : 0; this.parent = parent; this.isHeader = isHeader; + this.type = type; selection = 0; if (id != 0) { entry = TheOtherRolesPlugin.Instance.Config.Bind($"Preset{preset}", id.ToString(), defaultSelection); @@ -43,24 +53,24 @@ public CustomOption(int id, string name, System.Object[] selections, System.Obj options.Add(this); } - public static CustomOption Create(int id, string name, string[] selections, CustomOption parent = null, bool isHeader = false) { - return new CustomOption(id, name, selections, "", parent, isHeader); + public static CustomOption Create(int id, CustomOptionType type, string name, string[] selections, CustomOption parent = null, bool isHeader = false) { + return new CustomOption(id, type, name, selections, "", parent, isHeader); } - public static CustomOption Create(int id, string name, float defaultValue, float min, float max, float step, CustomOption parent = null, bool isHeader = false) { + public static CustomOption Create(int id, CustomOptionType type, string name, float defaultValue, float min, float max, float step, CustomOption parent = null, bool isHeader = false) { List selections = new List(); for (float s = min; s <= max; s += step) selections.Add(s); - return new CustomOption(id, name, selections.Cast().ToArray(), defaultValue, parent, isHeader); + return new CustomOption(id, type, name, selections.Cast().ToArray(), defaultValue, parent, isHeader); } - public static CustomOption Create(int id, string name, bool defaultValue, CustomOption parent = null, bool isHeader = false) { - return new CustomOption(id, name, new string[]{"Off", "On"}, defaultValue ? "On" : "Off", parent, isHeader); + public static CustomOption Create(int id, CustomOptionType type, string name, bool defaultValue, CustomOption parent = null, bool isHeader = false) { + return new CustomOption(id, type, name, new string[]{"Off", "On"}, defaultValue ? "On" : "Off", parent, isHeader); } // Static behaviour - public static void switchPreset(int newPreset) { + public static void switchPreset(int newPreset) { CustomOption.preset = newPreset; foreach (CustomOption option in CustomOption.options) { if (option.id == 0) continue; @@ -100,6 +110,10 @@ public float getFloat() { return (float)selections[selection]; } + public int getQuantity() { + return selection + 1; + } + // Option changes public void updateSelection(int newSelection) { @@ -125,28 +139,86 @@ public static void Postfix(GameOptionsMenu __instance) { GameObject.Find("TORSettings").transform.FindChild("GameGroup").FindChild("Text").GetComponent().SetText("The Other Roles Settings"); return; } + if (GameObject.Find("ImpostorSettings") != null) { + GameObject.Find("ImpostorSettings").transform.FindChild("GameGroup").FindChild("Text").GetComponent().SetText("Impostor Roles Settings"); + return; + } + if (GameObject.Find("NeutralSettings") != null) { + GameObject.Find("NeutralSettings").transform.FindChild("GameGroup").FindChild("Text").GetComponent().SetText("Neutral Roles Settings"); + return; + } + if (GameObject.Find("CrewmateSettings") != null) { + GameObject.Find("CrewmateSettings").transform.FindChild("GameGroup").FindChild("Text").GetComponent().SetText("Crewmate Roles Settings"); + return; + } + if (GameObject.Find("ModifierSettings") != null) { + GameObject.Find("ModifierSettings").transform.FindChild("GameGroup").FindChild("Text").GetComponent().SetText("Modifier Settings"); + return; + } // Setup TOR tab var template = UnityEngine.Object.FindObjectsOfType().FirstOrDefault(); if (template == null) return; var gameSettings = GameObject.Find("Game Settings"); var gameSettingMenu = UnityEngine.Object.FindObjectsOfType().FirstOrDefault(); + var torSettings = UnityEngine.Object.Instantiate(gameSettings, gameSettings.transform.parent); var torMenu = torSettings.transform.FindChild("GameGroup").FindChild("SliderInner").GetComponent(); torSettings.name = "TORSettings"; + var impostorSettings = UnityEngine.Object.Instantiate(gameSettings, gameSettings.transform.parent); + var impostorMenu = impostorSettings.transform.FindChild("GameGroup").FindChild("SliderInner").GetComponent(); + impostorSettings.name = "ImpostorSettings"; + + var neutralSettings = UnityEngine.Object.Instantiate(gameSettings, gameSettings.transform.parent); + var neutralMenu = neutralSettings.transform.FindChild("GameGroup").FindChild("SliderInner").GetComponent(); + neutralSettings.name = "NeutralSettings"; + + var crewmateSettings = UnityEngine.Object.Instantiate(gameSettings, gameSettings.transform.parent); + var crewmateMenu = crewmateSettings.transform.FindChild("GameGroup").FindChild("SliderInner").GetComponent(); + crewmateSettings.name = "CrewmateSettings"; + + var modifierSettings = UnityEngine.Object.Instantiate(gameSettings, gameSettings.transform.parent); + var modifierMenu = modifierSettings.transform.FindChild("GameGroup").FindChild("SliderInner").GetComponent(); + modifierSettings.name = "ModifierSettings"; + var roleTab = GameObject.Find("RoleTab"); var gameTab = GameObject.Find("GameTab"); var torTab = UnityEngine.Object.Instantiate(roleTab, roleTab.transform.parent); var torTabHighlight = torTab.transform.FindChild("Hat Button").FindChild("Tab Background").GetComponent(); torTab.transform.FindChild("Hat Button").FindChild("Icon").GetComponent().sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.TabIcon.png", 100f); - - gameTab.transform.position += Vector3.left * 0.5f; - torTab.transform.position += Vector3.right * 0.5f; - roleTab.transform.position += Vector3.left * 0.5f; - var tabs = new GameObject[]{gameTab, roleTab, torTab}; + var impostorTab = UnityEngine.Object.Instantiate(roleTab, torTab.transform); + var impostorTabHighlight = impostorTab.transform.FindChild("Hat Button").FindChild("Tab Background").GetComponent(); + impostorTab.transform.FindChild("Hat Button").FindChild("Icon").GetComponent().sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.TabIconImpostor.png", 100f); + impostorTab.name = "ImpostorTab"; + + var neutralTab = UnityEngine.Object.Instantiate(roleTab, impostorTab.transform); + var neutralTabHighlight = neutralTab.transform.FindChild("Hat Button").FindChild("Tab Background").GetComponent(); + neutralTab.transform.FindChild("Hat Button").FindChild("Icon").GetComponent().sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.TabIconNeutral.png", 100f); + neutralTab.name = "NeutralTab"; + + var crewmateTab = UnityEngine.Object.Instantiate(roleTab, neutralTab.transform); + var crewmateTabHighlight = crewmateTab.transform.FindChild("Hat Button").FindChild("Tab Background").GetComponent(); + crewmateTab.transform.FindChild("Hat Button").FindChild("Icon").GetComponent().sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.TabIconCrewmate.png", 100f); + crewmateTab.name = "CrewmateTab"; + + var modifierTab = UnityEngine.Object.Instantiate(roleTab, crewmateTab.transform); + var modifierTabHighlight = modifierTab.transform.FindChild("Hat Button").FindChild("Tab Background").GetComponent(); + modifierTab.transform.FindChild("Hat Button").FindChild("Icon").GetComponent().sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.TabIconModifier.png", 100f); + modifierTab.name = "ModifierTab"; + + // Position of Tab Icons + gameTab.transform.position += Vector3.left * 3f; + roleTab.transform.position += Vector3.left * 3f; + torTab.transform.position += Vector3.left * 2f; + impostorTab.transform.localPosition = Vector3.right * 1f; + neutralTab.transform.localPosition = Vector3.right * 1f; + crewmateTab.transform.localPosition = Vector3.right * 1f; + modifierTab.transform.localPosition = Vector3.right * 1f; + + var tabs = new GameObject[] { gameTab, roleTab, torTab, impostorTab, neutralTab, crewmateTab, modifierTab}; for (int i = 0; i < tabs.Length; i++) { var button = tabs[i].GetComponentInChildren(); if (button == null) continue; @@ -156,9 +228,17 @@ public static void Postfix(GameOptionsMenu __instance) { gameSettingMenu.RegularGameSettings.SetActive(false); gameSettingMenu.RolesSettings.gameObject.SetActive(false); torSettings.gameObject.SetActive(false); + impostorSettings.gameObject.SetActive(false); + neutralSettings.gameObject.SetActive(false); + crewmateSettings.gameObject.SetActive(false); + modifierSettings.gameObject.SetActive(false); gameSettingMenu.GameSettingsHightlight.enabled = false; gameSettingMenu.RolesSettingsHightlight.enabled = false; torTabHighlight.enabled = false; + impostorTabHighlight.enabled = false; + neutralTabHighlight.enabled = false; + crewmateTabHighlight.enabled = false; + modifierTabHighlight.enabled = false; if (copiedIndex == 0) { gameSettingMenu.RegularGameSettings.SetActive(true); gameSettingMenu.GameSettingsHightlight.enabled = true; @@ -168,20 +248,52 @@ public static void Postfix(GameOptionsMenu __instance) { } else if (copiedIndex == 2) { torSettings.gameObject.SetActive(true); torTabHighlight.enabled = true; + } else if (copiedIndex == 3) { + impostorSettings.gameObject.SetActive(true); + impostorTabHighlight.enabled = true; + } else if (copiedIndex == 4) { + neutralSettings.gameObject.SetActive(true); + neutralTabHighlight.enabled = true; + } else if (copiedIndex == 5) { + crewmateSettings.gameObject.SetActive(true); + crewmateTabHighlight.enabled = true; + } else if (copiedIndex == 6) { + modifierSettings.gameObject.SetActive(true); + modifierTabHighlight.enabled = true; } - })); + })); } foreach (OptionBehaviour option in torMenu.GetComponentsInChildren()) UnityEngine.Object.Destroy(option.gameObject); - List torOptions = new List(); + List torOptions = new List(); + + foreach (OptionBehaviour option in impostorMenu.GetComponentsInChildren()) + UnityEngine.Object.Destroy(option.gameObject); + List impostorOptions = new List(); + + foreach (OptionBehaviour option in neutralMenu.GetComponentsInChildren()) + UnityEngine.Object.Destroy(option.gameObject); + List neutralOptions = new List(); + + foreach (OptionBehaviour option in crewmateMenu.GetComponentsInChildren()) + UnityEngine.Object.Destroy(option.gameObject); + List crewmateOptions = new List(); + + foreach (OptionBehaviour option in modifierMenu.GetComponentsInChildren()) + UnityEngine.Object.Destroy(option.gameObject); + List modifierOptions = new List(); + + + List menus = new List() {torMenu.transform, impostorMenu.transform, neutralMenu.transform, crewmateMenu.transform, modifierMenu.transform}; + List> optionBehaviours = new List>() { torOptions, impostorOptions, neutralOptions, crewmateOptions, modifierOptions }; for (int i = 0; i < CustomOption.options.Count; i++) { CustomOption option = CustomOption.options[i]; if (option.optionBehaviour == null) { - StringOption stringOption = UnityEngine.Object.Instantiate(template, torMenu.transform); - torOptions.Add(stringOption); - stringOption.OnValueChanged = new Action((o) => {}); + StringOption stringOption = UnityEngine.Object.Instantiate(template, menus[(int)option.type]); + optionBehaviours[(int)option.type].Add(stringOption); + stringOption.OnValueChanged = new Action((o) => { }); stringOption.TitleText.text = option.name; stringOption.Value = stringOption.oldValue = option.selection; stringOption.ValueText.text = option.selections[option.selection].ToString(); @@ -194,6 +306,18 @@ public static void Postfix(GameOptionsMenu __instance) { torMenu.Children = torOptions.ToArray(); torSettings.gameObject.SetActive(false); + impostorMenu.Children = impostorOptions.ToArray(); + impostorSettings.gameObject.SetActive(false); + + neutralMenu.Children = neutralOptions.ToArray(); + neutralSettings.gameObject.SetActive(false); + + crewmateMenu.Children = crewmateOptions.ToArray(); + crewmateSettings.gameObject.SetActive(false); + + modifierMenu.Children = modifierOptions.ToArray(); + modifierSettings.gameObject.SetActive(false); + // Adapt task count for main options var commonTasksOption = __instance.Children.FirstOrDefault(x => x.name == "NumCommonTasks").TryCast(); @@ -261,15 +385,27 @@ class GameOptionsMenuUpdatePatch { private static float timer = 1f; public static void Postfix(GameOptionsMenu __instance) { - if (__instance.Children.Length < 100) return; // TODO: Introduce a cleaner way to seperate the TOR settings from the game settings + // Return Menu Update if in normal among us settings + var gameSettingMenu = UnityEngine.Object.FindObjectsOfType().FirstOrDefault(); + if (gameSettingMenu.RegularGameSettings.active || gameSettingMenu.RolesSettings.gameObject.active) return; - __instance.GetComponentInParent().ContentYBounds.max = -0.5F + __instance.Children.Length * 0.55F; + __instance.GetComponentInParent().ContentYBounds.max = -0.5F + __instance.Children.Length * 0.55F; timer += Time.deltaTime; if (timer < 0.1f) return; timer = 0f; float offset = 2.75f; foreach (CustomOption option in CustomOption.options) { + if (GameObject.Find("TORSettings") && option.type != CustomOption.CustomOptionType.General) + continue; + if (GameObject.Find("ImpostorSettings") && option.type != CustomOption.CustomOptionType.Impostor) + continue; + if (GameObject.Find("NeutralSettings") && option.type != CustomOption.CustomOptionType.Neutral) + continue; + if (GameObject.Find("CrewmateSettings") && option.type != CustomOption.CustomOptionType.Crewmate) + continue; + if (GameObject.Find("ModifierSettings") && option.type != CustomOption.CustomOptionType.Modifier) + continue; if (option?.optionBehaviour != null && option.optionBehaviour.gameObject != null) { bool enabled = true; var parent = option.parent; @@ -325,11 +461,33 @@ private static IEnumerable TargetMethods() { return typeof(GameOptionsData).GetMethods().Where(x => x.ReturnType == typeof(string) && x.GetParameters().Length == 1 && x.GetParameters()[0].ParameterType == typeof(int)); } - private static void Postfix(ref string __result) - { - StringBuilder sb = new StringBuilder(__result); - foreach (CustomOption option in CustomOption.options) { + private static string buildRoleOptions() { + var impRoles = buildOptionsOfType(CustomOption.CustomOptionType.Impostor, true) + "\n"; + var neutralRoles = buildOptionsOfType(CustomOption.CustomOptionType.Neutral, true) + "\n"; + var crewRoles = buildOptionsOfType(CustomOption.CustomOptionType.Crewmate, true) + "\n"; + var modifiers = buildOptionsOfType(CustomOption.CustomOptionType.Modifier, true); + return impRoles + neutralRoles + crewRoles + modifiers; + } + + private static string buildOptionsOfType(CustomOption.CustomOptionType type, bool headerOnly) { + StringBuilder sb = new StringBuilder("\n"); + var options = CustomOption.options.Where(o => o.type == type); + foreach (var option in options) { if (option.parent == null) { + sb.AppendLine($"{option.name}: {option.selections[option.selection].ToString()}"); + } + } + if (headerOnly) return sb.ToString(); + else sb = new StringBuilder(); + + foreach (CustomOption option in options) { + if (option.parent != null) { + bool isIrrelevant = option.parent.getSelection() == 0 || (option.parent.parent != null && option.parent.parent.getSelection() == 0); + + Color c = isIrrelevant ? Color.grey : Color.white; // No use for now + if (isIrrelevant) continue; + sb.AppendLine(Helpers.cs(c, $"{option.name}: {option.selections[option.selection].ToString()}")); + } else { if (option == CustomOptionHolder.crewmateRolesCountMin) { var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Crewmate Roles"); var min = CustomOptionHolder.crewmateRolesCountMin.getSelection(); @@ -351,65 +509,53 @@ private static void Postfix(ref string __result) if (min > max) min = max; var optionValue = (min == max) ? $"{max}" : $"{min} - {max}"; sb.AppendLine($"{optionName}: {optionValue}"); - } else if ((option == CustomOptionHolder.crewmateRolesCountMax) || (option == CustomOptionHolder.neutralRolesCountMax) || (option == CustomOptionHolder.impostorRolesCountMax)) { + } else if (option == CustomOptionHolder.modifiersCountMin) { + var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Modifiers"); + var min = CustomOptionHolder.modifiersCountMin.getSelection(); + var max = CustomOptionHolder.modifiersCountMax.getSelection(); + if (min > max) min = max; + var optionValue = (min == max) ? $"{max}" : $"{min} - {max}"; + sb.AppendLine($"{optionName}: {optionValue}"); + } else if ((option == CustomOptionHolder.crewmateRolesCountMax) || (option == CustomOptionHolder.neutralRolesCountMax) || (option == CustomOptionHolder.impostorRolesCountMax) || option == CustomOptionHolder.modifiersCountMax) { continue; } else { - sb.AppendLine($"{option.name}: {option.selections[option.selection].ToString()}"); - } - + sb.AppendLine($"\n{option.name}: {option.selections[option.selection].ToString()}"); + } } } - CustomOption parent = null; - foreach (CustomOption option in CustomOption.options) - if (option.parent != null) { - if (option.parent != parent) { - sb.AppendLine(); - parent = option.parent; - } - sb.AppendLine($"{option.name}: {option.selections[option.selection].ToString()}"); - } + return sb.ToString(); + } - var hudString = sb.ToString(); - - int defaultSettingsLines = 23; - int roleSettingsLines = defaultSettingsLines + 40; - int detailedSettingsP1 = roleSettingsLines + 40; - int detailedSettingsP2 = detailedSettingsP1 + 42; - int detailedSettingsP3 = detailedSettingsP2 + 42; - 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(); - int end4 = hudString.TakeWhile(c => (detailedSettingsP2 -= (c == '\n' ? 1 : 0)) > 0).Count(); - int end5 = hudString.TakeWhile(c => (detailedSettingsP3 -= (c == '\n' ? 1 : 0)) > 0).Count(); + private static void Postfix(ref string __result) + { int counter = TheOtherRolesPlugin.optionsPage; - if (counter == 0) { - hudString = hudString.Substring(0, end1) + "\n"; - } else if (counter == 1) { - hudString = hudString.Substring(end1 + 1, end2 - end1); - // Temporary fix, should add a new CustomOption for spaces - int gap = 2; - int index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count(); - hudString = hudString.Insert(index, "\n"); - gap = 6; - index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count(); - hudString = hudString.Insert(index, "\n"); - gap = 20; - index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count(); - hudString = hudString.Insert(index + 1, "\n"); - gap = 26; - index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count(); - hudString = hudString.Insert(index + 1, "\n"); - } else if (counter == 2) { - hudString = hudString.Substring(end2 + 1, end3 - end2); - } else if (counter == 3) { - hudString = hudString.Substring(end3 + 1, end4 - end3); - } else if (counter == 4) { - hudString = hudString.Substring(end4 + 1, end5 - end4); - } else if (counter == 5) { - hudString = hudString.Substring(end5 + 1); + string hudString = counter != 0 ? Helpers.cs(DateTime.Now.Second % 2 == 0 ? Color.white : Color.red, "(Use scroll wheel if necessary)\n\n") : ""; + + switch (counter) { + case 0: + hudString += "Page 1: Vanilla Settings \n\n" + __result; + break; + case 1: + hudString += "Page 2: The Other Roles Settings \n" + buildOptionsOfType(CustomOption.CustomOptionType.General, false); + break; + case 2: + hudString += "Page 3: Role and Modifier Rates \n" + buildRoleOptions(); + break; + case 3: + hudString += "Page 4: Impostor Role Settings \n" + buildOptionsOfType(CustomOption.CustomOptionType.Impostor, false); + break; + case 4: + hudString += "Page 5: Neutral Role Settings \n" + buildOptionsOfType(CustomOption.CustomOptionType.Neutral, false); + break; + case 5: + hudString += "Page 6: Crewmate Role Settings \n" + buildOptionsOfType(CustomOption.CustomOptionType.Crewmate, false); + break; + case 6: + hudString += "Page 7: Modifier Settings \n" + buildOptionsOfType(CustomOption.CustomOptionType.Modifier, false); + break; } - hudString += $"\n Press tab for more... ({counter+1}/6)"; + hudString += $"\n Press TAB or Page Number for more... ({counter+1}/7)"; __result = hudString; } } @@ -419,8 +565,36 @@ public static class GameOptionsNextPagePatch { public static void Postfix(KeyboardJoystick __instance) { - if(Input.GetKeyDown(KeyCode.Tab)) { - TheOtherRolesPlugin.optionsPage = (TheOtherRolesPlugin.optionsPage + 1) % 6; + int page = TheOtherRolesPlugin.optionsPage; + if (Input.GetKeyDown(KeyCode.Tab)) { + TheOtherRolesPlugin.optionsPage = (TheOtherRolesPlugin.optionsPage + 1) % 7; + } + if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1)) { + TheOtherRolesPlugin.optionsPage = 0; + } + if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2)) { + TheOtherRolesPlugin.optionsPage = 1; + } + if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3)) { + TheOtherRolesPlugin.optionsPage = 2; + } + if (Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4)) { + TheOtherRolesPlugin.optionsPage = 3; + } + if (Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5)) { + TheOtherRolesPlugin.optionsPage = 4; + } + if (Input.GetKeyDown(KeyCode.Alpha6) || Input.GetKeyDown(KeyCode.Keypad6)) { + TheOtherRolesPlugin.optionsPage = 5; + } + if (Input.GetKeyDown(KeyCode.Alpha7) || Input.GetKeyDown(KeyCode.Keypad7)) { + TheOtherRolesPlugin.optionsPage = 6; + } + if (page != TheOtherRolesPlugin.optionsPage) { + Vector3 position = (Vector3)HudManager.Instance?.GameSettings?.transform.localPosition; + if (position != null) { + HudManager.Instance.GameSettings.transform.localPosition = new Vector3(position.x, 2.9f, position.z); + } } } } @@ -432,4 +606,80 @@ public static void Prefix(HudManager __instance) { if (__instance.GameSettings != null) __instance.GameSettings.fontSize = 1.2f; } } -} \ No newline at end of file + + + // This class is taken from Town of Us Reactivated, https://github.com/eDonnes124/Town-Of-Us-R/blob/master/source/Patches/CustomOption/Patches.cs, Licensed under GPLv3 + [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] + public class HudManagerUpdate { + public static float + MinX,/*-5.3F*/ + OriginalY = 2.9F, + MinY = 2.9F; + + + public static Scroller Scroller; + private static Vector3 LastPosition; + private static float lastAspect; + private static bool setLastPosition = false; + + public static void Prefix(HudManager __instance) { + if (__instance.GameSettings?.transform == null) return; + + // Sets the MinX position to the left edge of the screen + 0.1 units + Rect safeArea = Screen.safeArea; + float aspect = Mathf.Min((Camera.main).aspect, safeArea.width / safeArea.height); + float safeOrthographicSize = CameraSafeArea.GetSafeOrthographicSize(Camera.main); + MinX = 0.1f - safeOrthographicSize * aspect; + + if (!setLastPosition || aspect != lastAspect) { + LastPosition = new Vector3(MinX, MinY); + lastAspect = aspect; + setLastPosition = true; + } + + CreateScroller(__instance); + + Scroller.gameObject.SetActive(__instance.GameSettings.gameObject.activeSelf); + + if (!Scroller.gameObject.active) return; + + var rows = __instance.GameSettings.text.Count(c => c == '\n'); + float LobbyTextRowHeight = 0.06F; + var maxY = Mathf.Max(MinY, rows * LobbyTextRowHeight + (rows - 38) * LobbyTextRowHeight); + + Scroller.ContentYBounds = new FloatRange(MinY, maxY); + + // Prevent scrolling when the player is interacting with a menu + if (PlayerControl.LocalPlayer?.CanMove != true) { + __instance.GameSettings.transform.localPosition = LastPosition; + + return; + } + + if (__instance.GameSettings.transform.localPosition.x != MinX || + __instance.GameSettings.transform.localPosition.y < MinY) return; + + LastPosition = __instance.GameSettings.transform.localPosition; + } + + private static void CreateScroller(HudManager __instance) { + if (Scroller != null) return; + + Scroller = new GameObject("SettingsScroller").AddComponent(); + Scroller.transform.SetParent(__instance.GameSettings.transform.parent); + Scroller.gameObject.layer = 5; + + Scroller.transform.localScale = Vector3.one; + Scroller.allowX = false; + Scroller.allowY = true; + Scroller.active = true; + Scroller.velocity = new Vector2(0, 0); + Scroller.ScrollbarYBounds = new FloatRange(0, 0); + Scroller.ContentXBounds = new FloatRange(MinX, MinX); + Scroller.enabled = true; + + Scroller.Inner = __instance.GameSettings.transform; + __instance.GameSettings.transform.SetParent(Scroller.transform); + } + } +} diff --git a/TheOtherRoles/Objects/Bloodytrail.cs b/TheOtherRoles/Objects/Bloodytrail.cs new file mode 100644 index 000000000..683200c98 --- /dev/null +++ b/TheOtherRoles/Objects/Bloodytrail.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Collections; +using UnityEngine; +using static TheOtherRoles.TheOtherRoles; + +namespace TheOtherRoles.Objects { + class Bloodytrail { + private static List bloodytrail = new List(); + private static List sprites = new List(); + private Color color; + private GameObject blood; + private SpriteRenderer spriteRenderer; + + public static List getBloodySprites() { + if (sprites.Count > 0) return sprites; + sprites.Add(Helpers.loadSpriteFromResources("TheOtherRoles.Resources.Blood1.png", 700)); + sprites.Add(Helpers.loadSpriteFromResources("TheOtherRoles.Resources.Blood2.png", 500)); + sprites.Add(Helpers.loadSpriteFromResources("TheOtherRoles.Resources.Blood3.png", 300)); + return sprites; + } + + public Bloodytrail(PlayerControl player, PlayerControl bloodyPlayer) { + this.color = Palette.PlayerColors[(int)bloodyPlayer.Data.DefaultOutfit.ColorId]; + var sp = getBloodySprites(); + var index = rnd.Next(0, sp.Count); + + + blood = new GameObject("Blood" + index); + Vector3 position = new Vector3(player.transform.position.x, player.transform.position.y, player.transform.position.z + 1f); + blood.transform.position = position; + blood.transform.localPosition = position; + blood.transform.SetParent(player.transform.parent); + + blood.transform.Rotate(0.0f, 0.0f, UnityEngine.Random.Range(0.0f, 360.0f)); + + spriteRenderer = blood.AddComponent(); + spriteRenderer.sprite = sp[index]; + spriteRenderer.material = DestroyableSingleton.Instance.PlayerMaterial; + PlayerControl.SetPlayerMaterialColors(color, spriteRenderer); + // spriteRenderer.color = color; + + blood.SetActive(true); + bloodytrail.Add(this); + + HudManager.Instance.StartCoroutine(Effects.Lerp(10f, new Action((p) => { + Color c = color; + if (Camouflager.camouflageTimer > 0) c = Palette.PlayerColors[6]; + if (spriteRenderer) spriteRenderer.color = new Color(c.r, c.g, c.b, Mathf.Clamp01(1 - p)); + + if (p == 1f && blood != null) { + UnityEngine.Object.Destroy(blood); + bloodytrail.Remove(this); + } + }))); + } + + public static void resetSprites() + { + sprites.Clear(); + } + } +} \ No newline at end of file diff --git a/TheOtherRoles/Objects/JackInTheBox.cs b/TheOtherRoles/Objects/JackInTheBox.cs index 13ae06cd8..8e2bd7397 100644 --- a/TheOtherRoles/Objects/JackInTheBox.cs +++ b/TheOtherRoles/Objects/JackInTheBox.cs @@ -23,18 +23,18 @@ public static Sprite getBoxAnimationSprite(int index) { public static void startAnimation(int ventId) { JackInTheBox box = AllJackInTheBoxes.FirstOrDefault((x) => x?.vent != null && x.vent.Id == ventId); if (box == null) return; - Vent vent = box.vent; HudManager.Instance.StartCoroutine(Effects.Lerp(0.6f, new Action((p) => { - if (vent != null && vent.myRend != null) { - vent.myRend.sprite = getBoxAnimationSprite((int)(p * boxAnimationSprites.Length)); - if (p == 1f) vent.myRend.sprite = getBoxAnimationSprite(0); + if (box.boxRenderer != null) { + box.boxRenderer.sprite = getBoxAnimationSprite((int)(p * boxAnimationSprites.Length)); + if (p == 1f) box.boxRenderer.sprite = getBoxAnimationSprite(0); } }))); } private GameObject gameObject; public Vent vent; + private SpriteRenderer boxRenderer; public JackInTheBox(Vector2 p) { gameObject = new GameObject("JackInTheBox"); @@ -42,7 +42,7 @@ public JackInTheBox(Vector2 p) { position += (Vector3)PlayerControl.LocalPlayer.Collider.offset; // Add collider offset that DoMove moves the player up at a valid position // Create the marker gameObject.transform.position = position; - var boxRenderer = gameObject.AddComponent(); + boxRenderer = gameObject.AddComponent(); boxRenderer.sprite = getBoxAnimationSprite(0); // Create the vent @@ -58,7 +58,7 @@ public JackInTheBox(Vector2 p) { vent.GetComponent()?.Stop(); vent.Id = ShipStatus.Instance.AllVents.Select(x => x.Id).Max() + 1; // Make sure we have a unique id var ventRenderer = vent.GetComponent(); - ventRenderer.sprite = getBoxAnimationSprite(0); + ventRenderer.sprite = null; // Use the box.boxRenderer instead vent.myRend = ventRenderer; var allVentsList = ShipStatus.Instance.AllVents.ToList(); allVentsList.Add(vent); @@ -82,7 +82,7 @@ public static void UpdateStates() { } public void convertToVent() { - gameObject.SetActive(false); + gameObject.SetActive(true); vent.gameObject.SetActive(true); return; } diff --git a/TheOtherRoles/Objects/NinjaTrace.cs b/TheOtherRoles/Objects/NinjaTrace.cs new file mode 100644 index 000000000..c9be1e2c1 --- /dev/null +++ b/TheOtherRoles/Objects/NinjaTrace.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Collections; +using UnityEngine; + +namespace TheOtherRoles.Objects { + class NinjaTrace { + public static List traces = new List(); + + private GameObject trace; + private float timeRemaining; + + private static Sprite TraceSprite; + public static Sprite getTraceSprite() { + if (TraceSprite) return TraceSprite; + TraceSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.NinjaTraceW.png", 225f); + return TraceSprite; + } + + public NinjaTrace(Vector2 p, float duration=1f) { + trace = new GameObject("NinjaTrace"); + Vector3 position = new Vector3(p.x, p.y, PlayerControl.LocalPlayer.transform.localPosition.z + 0.001f); // just behind player + trace.transform.position = position; + trace.transform.localPosition = position; + + var traceRenderer = trace.AddComponent(); + traceRenderer.sprite = getTraceSprite(); + + timeRemaining = duration; + + // display the ninjas color in the trace + float colorDuration = CustomOptionHolder.ninjaTraceColorTime.getFloat(); + HudManager.Instance.StartCoroutine(Effects.Lerp(colorDuration, new Action((p) => { + Color c = Palette.PlayerColors[(int)Ninja.ninja.Data.DefaultOutfit.ColorId]; + if (Helpers.isLighterColor(Ninja.ninja.Data.DefaultOutfit.ColorId)) c = Color.white; + else c = Palette.PlayerColors[6]; + //if (Camouflager.camouflageTimer > 0) { + // c = Palette.PlayerColors[6]; + //} + + Color g = Color.green; // Usual display color. could also be Palette.PlayerColors[6] for default grey like camo + // if this stays black (0,0,0), it can ofc be removed. + + Color combinedColor = Mathf.Clamp01(p) * g + Mathf.Clamp01(1 - p) * c; + + if (traceRenderer) traceRenderer.color = combinedColor; + }))); + + float fadeOutDuration = 1f; + if (fadeOutDuration > duration) fadeOutDuration = 0.5f * duration; + HudManager.Instance.StartCoroutine(Effects.Lerp(duration, new Action((p) => { + float interP = 0f; + if (p < (duration - fadeOutDuration) / duration) + interP = 0f; + else interP = (p * duration + fadeOutDuration - duration) / fadeOutDuration; + if (traceRenderer) traceRenderer.color = new Color(traceRenderer.color.r, traceRenderer.color.g, traceRenderer.color.b, Mathf.Clamp01(1 - interP)); + }))); + + trace.SetActive(true); + traces.Add(this); + } + + public static void clearTraces() { + traces = new List(); + } + + public static void UpdateAll() { + foreach (NinjaTrace traceCurrent in new List(traces)) + { + traceCurrent.timeRemaining -= Time.fixedDeltaTime; + if (traceCurrent.timeRemaining < 0) + { + traceCurrent.trace.SetActive(false); + UnityEngine.Object.Destroy(traceCurrent.trace); + traces.Remove(traceCurrent); + } + } + } + } +} \ No newline at end of file diff --git a/TheOtherRoles/Objects/Portal.cs b/TheOtherRoles/Objects/Portal.cs new file mode 100644 index 000000000..de6a94ea6 --- /dev/null +++ b/TheOtherRoles/Objects/Portal.cs @@ -0,0 +1,179 @@ +using System; +using UnityEngine; +using System.Collections.Generic; +using static TheOtherRoles.TheOtherRoles; + +namespace TheOtherRoles.Objects { + + public class Portal { + public static Portal firstPortal = null; + public static Portal secondPortal = null; + public static bool bothPlacedAndEnabled = false; + // public static Sprite[] portalBgAnimationSprites = new Sprite[109]; + public static Sprite[] portalFgAnimationSprites = new Sprite[205]; + public static Sprite portalSprite; + public static bool isTeleporting = false; + public static float teleportDuration = 3.4166666667f; + + public struct tpLogEntry { + public byte playerId; + public string name; + public DateTime time; + public tpLogEntry(byte playerId, string name, DateTime time) { + this.playerId = playerId; + this.time = time; + this.name = name; + } + } + + public static List teleportedPlayers; + + /*public static Sprite getBgAnimationSprite(int index) { + if (portalBgAnimationSprites == null || portalBgAnimationSprites.Length == 0) return null; + index = Mathf.Clamp(index, 0, portalBgAnimationSprites.Length - 1); + if (portalBgAnimationSprites[index] == null) + portalBgAnimationSprites[index] = (Helpers.loadSpriteFromResources($"TheOtherRoles.Resources.PortalAnimation.plattform.png", 115f)); + return portalBgAnimationSprites[index]; + }*/ + + public static Sprite getFgAnimationSprite(int index) { + if (portalFgAnimationSprites == null || portalFgAnimationSprites.Length == 0) return null; + index = Mathf.Clamp(index, 0, portalFgAnimationSprites.Length - 1); + if (portalFgAnimationSprites[index] == null) + portalFgAnimationSprites[index] = (Helpers.loadSpriteFromResources($"TheOtherRoles.Resources.PortalAnimation.portal_{(index):000}.png", 115f)); + return portalFgAnimationSprites[index]; + } + + public static void startTeleport(byte playerId) { + if (firstPortal == null || secondPortal == null) return; + isTeleporting = true; + + // Generate log info + PlayerControl playerControl = Helpers.playerById(playerId); + bool flip = playerControl.MyRend.flipX; // use the original player control here, not the morhpTarget. + firstPortal.animationFgRenderer.flipX = flip; + secondPortal.animationFgRenderer.flipX = flip; + if (Morphling.morphling != null && Morphling.morphTimer > 0) playerControl = Morphling.morphTarget; // Will output info of morph-target instead + string playerNameDisplay = Portalmaker.logOnlyHasColors ? "A player (" + (Helpers.isLighterColor(playerControl.Data.DefaultOutfit.ColorId) ? "L" : "D") + ")" : playerControl.Data.PlayerName; + + int colorId = playerControl.Data.DefaultOutfit.ColorId; + + if (Camouflager.camouflageTimer > 0) { + playerNameDisplay = "A camouflaged player"; + colorId = 6; + } + teleportedPlayers.Add(new tpLogEntry(playerId, playerNameDisplay, DateTime.UtcNow)); + + HudManager.Instance.StartCoroutine(Effects.Lerp(teleportDuration, new Action((p) => { + if (firstPortal != null && firstPortal.animationFgRenderer != null && secondPortal != null && secondPortal.animationFgRenderer != null) { + firstPortal.animationFgRenderer.sprite = getFgAnimationSprite((int)(p * portalFgAnimationSprites.Length)); + secondPortal.animationFgRenderer.sprite = getFgAnimationSprite((int)(p * portalFgAnimationSprites.Length)); + PlayerControl.SetPlayerMaterialColors(colorId, firstPortal.animationFgRenderer); + PlayerControl.SetPlayerMaterialColors(colorId, secondPortal.animationFgRenderer); + /*firstPortal.animationBgRenderer.sprite = getBgAnimationSprite((int)(p * portalFgAnimationSprites.Length)); + secondPortal.animationBgRenderer.sprite = getBgAnimationSprite((int)(p * portalFgAnimationSprites.Length));*/ + if (p == 1f) { + firstPortal.animationFgRenderer.sprite = null; + secondPortal.animationFgRenderer.sprite = null; + firstPortal.animationBgRenderer.sprite = null; + secondPortal.animationBgRenderer.sprite = null; + isTeleporting = false; + } + } + }))); + } + + public GameObject portalFgAnimationGameObject; + public GameObject portalBgAnimationGameObject; + public GameObject portalGameObject; + private SpriteRenderer animationFgRenderer; + private SpriteRenderer animationBgRenderer; + private SpriteRenderer portalRenderer; + + public Portal(Vector2 p) { + portalGameObject = new GameObject("Portal"); + Vector3 position = new Vector3(p.x, p.y, PlayerControl.LocalPlayer.transform.position.z + 1f); + + + // Create the portal + portalGameObject.transform.position = position; + portalRenderer = portalGameObject.AddComponent(); + animationBgRenderer = portalGameObject.AddComponent(); + portalRenderer.sprite = portalSprite; + + portalBgAnimationGameObject = new GameObject("PortalAnimationBG"); + portalBgAnimationGameObject.transform.position = position; + animationBgRenderer = portalBgAnimationGameObject.AddComponent(); + + Vector3 fgPosition = new Vector3(p.x, p.y, PlayerControl.LocalPlayer.transform.position.z - 0.1f); + portalFgAnimationGameObject = new GameObject("PortalAnimationFG"); + portalFgAnimationGameObject.transform.position = fgPosition; + animationFgRenderer = portalFgAnimationGameObject.AddComponent(); + animationFgRenderer.material = DestroyableSingleton.Instance.PlayerMaterial; + + // Only render the inactive portals for the Portalmaker + bool playerIsPortalmaker = PlayerControl.LocalPlayer == TheOtherRoles.Portalmaker.portalmaker; + portalGameObject.SetActive(playerIsPortalmaker); + portalFgAnimationGameObject.SetActive(true); + portalBgAnimationGameObject.SetActive(true); + + if (firstPortal == null) firstPortal = this; + else if (secondPortal == null) { + secondPortal = this; + } + } + + public static bool locationNearEntry(Vector2 p) { + if (!bothPlacedAndEnabled) return false; + float maxDist = 0.25f; + + var dist1 = Vector2.Distance(p, firstPortal.portalGameObject.transform.position); + var dist2 = Vector2.Distance(p, secondPortal.portalGameObject.transform.position); + if (dist1 > maxDist && dist2 > maxDist) return false; + return true; + } + + public static Vector2 findExit(Vector2 p) { + var dist1 = Vector2.Distance(p, firstPortal.portalGameObject.transform.position); + var dist2 = Vector2.Distance(p, secondPortal.portalGameObject.transform.position); + return dist1 < dist2 ? secondPortal.portalGameObject.transform.position : firstPortal.portalGameObject.transform.position; + } + + public static Vector2 findEntry(Vector2 p) { + var dist1 = Vector2.Distance(p, firstPortal.portalGameObject.transform.position); + var dist2 = Vector2.Distance(p, secondPortal.portalGameObject.transform.position); + return dist1 > dist2 ? secondPortal.portalGameObject.transform.position : firstPortal.portalGameObject.transform.position; + } + + public static void meetingEndsUpdate() { + // checkAndEnable + if (secondPortal != null) { + firstPortal.portalGameObject.SetActive(true); + secondPortal.portalGameObject.SetActive(true); + bothPlacedAndEnabled = true; + } + + // reset teleported players + teleportedPlayers = new List(); + } + + private static void preloadSprites() { + for (int i = 0; i < portalFgAnimationSprites.Length; i++) { + /*getBgAnimationSprite(i);*/ + getFgAnimationSprite(i); + } + portalSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.PortalAnimation.plattform.png", 115f); + } + + public static void clearPortals() { + preloadSprites(); // Force preload of sprites to avoid lag + bothPlacedAndEnabled = false; + firstPortal = null; + secondPortal = null; + isTeleporting = false; + teleportedPlayers = new List(); + } + + } + +} \ No newline at end of file diff --git a/TheOtherRoles/Patches/ClientOptionsPatch.cs b/TheOtherRoles/Patches/ClientOptionsPatch.cs index 69837284c..5de2618f3 100644 --- a/TheOtherRoles/Patches/ClientOptionsPatch.cs +++ b/TheOtherRoles/Patches/ClientOptionsPatch.cs @@ -18,6 +18,7 @@ public static class ClientOptionsPatch new SelectionBehaviour("Ghosts See Remaining Tasks", () => MapOptions.ghostsSeeTasks = TheOtherRolesPlugin.GhostsSeeTasks.Value = !TheOtherRolesPlugin.GhostsSeeTasks.Value, TheOtherRolesPlugin.GhostsSeeTasks.Value), new SelectionBehaviour("Ghosts Can See Votes", () => MapOptions.ghostsSeeVotes = TheOtherRolesPlugin.GhostsSeeVotes.Value = !TheOtherRolesPlugin.GhostsSeeVotes.Value, TheOtherRolesPlugin.GhostsSeeVotes.Value), new SelectionBehaviour("Ghosts Can See Roles", () => MapOptions.ghostsSeeRoles = TheOtherRolesPlugin.GhostsSeeRoles.Value = !TheOtherRolesPlugin.GhostsSeeRoles.Value, TheOtherRolesPlugin.GhostsSeeRoles.Value), + new SelectionBehaviour("Ghosts Can Additionally See Modifier", () => MapOptions.ghostsSeeModifier = TheOtherRolesPlugin.GhostsSeeModifier.Value = !TheOtherRolesPlugin.GhostsSeeModifier.Value, TheOtherRolesPlugin.GhostsSeeModifier.Value), new SelectionBehaviour("Show Role Summary", () => MapOptions.showRoleSummary = TheOtherRolesPlugin.ShowRoleSummary.Value = !TheOtherRolesPlugin.ShowRoleSummary.Value, TheOtherRolesPlugin.ShowRoleSummary.Value), new SelectionBehaviour("Show Lighter / Darker", () => MapOptions.showLighterDarker = TheOtherRolesPlugin.ShowLighterDarker.Value = !TheOtherRolesPlugin.ShowLighterDarker.Value, TheOtherRolesPlugin.ShowLighterDarker.Value), }; diff --git a/TheOtherRoles/Patches/CredentialsPatch.cs b/TheOtherRoles/Patches/CredentialsPatch.cs index 0ee14ca07..e3dec1cce 100644 --- a/TheOtherRoles/Patches/CredentialsPatch.cs +++ b/TheOtherRoles/Patches/CredentialsPatch.cs @@ -13,13 +13,15 @@ public static class CredentialsPatch { $@"TheOtherRoles v{TheOtherRolesPlugin.Version.ToString()} Modded by Eisbison, EndOfFile Thunderstorm584 & Mallöris -Button design by Bavari"; +Design by Bavari"; public static string mainMenuCredentials = $@"Modded by Eisbison, Thunderstorm584, EndOfFile & Mallöris Design by Bavari"; - public static string contributorsCredentials = "GitHub Contributors: Alex2911, amsyarasyiq, gendelo3, MaximeGillot"; + public static string contributorsCredentials = +$@" Special thanks to K3ndo & Smeggy +GitHub Contributors: Gendelo3, Alex2911, amsyarasyiq, MaximeGillot, Psynomit"; [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] private static class VersionShowerPatch diff --git a/TheOtherRoles/Patches/EndGamePatch.cs b/TheOtherRoles/Patches/EndGamePatch.cs index 28a2966a7..4080a8f92 100644 --- a/TheOtherRoles/Patches/EndGamePatch.cs +++ b/TheOtherRoles/Patches/EndGamePatch.cs @@ -33,7 +33,6 @@ enum WinCondition { VultureWin, LawyerSoloWin, AdditionalLawyerBonusWin, - AdditionalLawyerStolenWin, AdditionalAlivePursuerWin } @@ -101,6 +100,8 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)]ref End bool vultureWin = Vulture.vulture != null && gameOverReason == (GameOverReason)CustomGameOverReason.VultureWin; bool lawyerSoloWin = Lawyer.lawyer != null && gameOverReason == (GameOverReason)CustomGameOverReason.LawyerSoloWin; + bool isPursurerLose = jesterWin || arsonistWin || miniLose || vultureWin || teamJackalWin; + // Mini lose if (miniLose) { TempData.winners = new Il2CppSystem.Collections.Generic.List(); @@ -181,7 +182,7 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)]ref End } // Lawyer solo win - else if (lawyerSoloWin) { + else if (lawyerSoloWin && !Pursuer.notAckedExiled) { TempData.winners = new Il2CppSystem.Collections.Generic.List(); WinningPlayerData wpd = new WinningPlayerData(Lawyer.lawyer.Data); TempData.winners.Add(wpd); @@ -189,7 +190,7 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)]ref End } // Possible Additional winner: Lawyer - if (!lawyerSoloWin && Lawyer.lawyer != null && Lawyer.target != null && !Lawyer.target.Data.IsDead) { + if (!lawyerSoloWin && Lawyer.lawyer != null && Lawyer.target != null && (!Lawyer.target.Data.IsDead || Lawyer.target == Jester.jester) && !Pursuer.notAckedExiled) { WinningPlayerData winningClient = null; foreach (WinningPlayerData winner in TempData.winners) { if (winner.PlayerName == Lawyer.target.Data.PlayerName) @@ -198,17 +199,12 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)]ref End if (winningClient != null) { // The Lawyer wins if the client is winning (and alive, but if he wasn't the Lawyer shouldn't exist anymore) if (!TempData.winners.ToArray().Any(x => x.PlayerName == Lawyer.lawyer.Data.PlayerName)) TempData.winners.Add(new WinningPlayerData(Lawyer.lawyer.Data)); - if (!Lawyer.lawyer.Data.IsDead) { // The Lawyer steals the clients win - TempData.winners.Remove(winningClient); - AdditionalTempData.additionalWinConditions.Add(WinCondition.AdditionalLawyerStolenWin); - } else { // The Lawyer wins together with the client - AdditionalTempData.additionalWinConditions.Add(WinCondition.AdditionalLawyerBonusWin); - } + AdditionalTempData.additionalWinConditions.Add(WinCondition.AdditionalLawyerBonusWin); // The Lawyer wins together with the client } } // Possible Additional winner: Pursuer - if (Pursuer.pursuer != null && !Pursuer.pursuer.Data.IsDead && !Pursuer.notAckedExiled) { + if (Pursuer.pursuer != null && !Pursuer.pursuer.Data.IsDead && !Pursuer.notAckedExiled && !isPursurerLose && !TempData.winners.ToArray().Any(x => x.IsImpostor)) { 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); @@ -310,9 +306,7 @@ public static void Postfix(EndGameManager __instance) { } foreach (WinCondition cond in AdditionalTempData.additionalWinConditions) { - if (cond == WinCondition.AdditionalLawyerStolenWin) { - textRenderer.text += $"\n{Helpers.cs(Lawyer.color, "The Lawyer stole the win from the client")}"; - } else if (cond == WinCondition.AdditionalLawyerBonusWin) { + if (cond == WinCondition.AdditionalLawyerBonusWin) { textRenderer.text += $"\n{Helpers.cs(Lawyer.color, "The Lawyer wins with the client")}"; } else if (cond == WinCondition.AdditionalAlivePursuerWin) { textRenderer.text += $"\n{Helpers.cs(Pursuer.color, "The Pursuer survived")}"; diff --git a/TheOtherRoles/Patches/ExileControllerPatch.cs b/TheOtherRoles/Patches/ExileControllerPatch.cs index 37f4358ed..3672e9c7c 100644 --- a/TheOtherRoles/Patches/ExileControllerPatch.cs +++ b/TheOtherRoles/Patches/ExileControllerPatch.cs @@ -23,6 +23,7 @@ public static void Prefix(ExileController __instance, [HarmonyArgument(0)]ref Ga AmongUsClient.Instance.FinishRpcImmediately(writer); RPCProcedure.medicSetShielded(Medic.futureShielded.PlayerId); } + if (Medic.usedShield) Medic.meetingAfterShielding = true; // Has to be after the setting of the shield // Shifter shift if (Shifter.shifter != null && AmongUsClient.Instance.AmHost && Shifter.futureShift != null) { // We need to send the RPC from the host here, to make sure that the order of shifting and erasing is correct (for that reason the futureShifted and futureErased are being synced) @@ -51,6 +52,9 @@ public static void Prefix(ExileController __instance, [HarmonyArgument(0)]ref Ga JackInTheBox.convertToVents(); } + // Activate portals. + Portal.meetingEndsUpdate(); + // Witch execute casted spells if (Witch.witch != null && Witch.futureSpelled != null && AmongUsClient.Instance.AmHost) { bool exiledIsWitch = exiled != null && exiled.PlayerId == Witch.witch.PlayerId; @@ -110,7 +114,7 @@ public static void Postfix(AirshipExileController __instance) { static void WrapUpPostfix(GameData.PlayerInfo exiled) { // Mini exile lose condition - if (exiled != null && Mini.mini != null && Mini.mini.PlayerId == exiled.PlayerId && !Mini.isGrownUp() && !Mini.mini.Data.Role.IsImpostor) { + if (exiled != null && Mini.mini != null && Mini.mini.PlayerId == exiled.PlayerId && !Mini.isGrownUp() && !Mini.mini.Data.Role.IsImpostor && !RoleInfo.getRoleInfoForPlayer(Mini.mini).Any(x => x.isNeutral)) { Mini.triggerMiniLose = true; } // Jester win condition @@ -199,9 +203,17 @@ static void WrapUpPostfix(GameData.PlayerInfo exiled) { Medium.featureDeadBodies = new List>(); } } - + // Lawyer add meeting if (Lawyer.lawyer != null && PlayerControl.LocalPlayer == Lawyer.lawyer && !Lawyer.lawyer.Data.IsDead) Lawyer.meetings++; + + // AntiTeleport set position + if (AntiTeleport.antiTeleport.FindAll(x => x.PlayerId == PlayerControl.LocalPlayer.PlayerId).Count > 0) { + PlayerControl.LocalPlayer.transform.position = AntiTeleport.position; + } + + // Invert add meeting + if (Invert.meetings > 0) Invert.meetings--; } } @@ -214,12 +226,14 @@ static void Postfix(ref string __result, [HarmonyArgument(0)]StringNames id) { if (player == null) return; // Exile role text if (id == StringNames.ExileTextPN || id == StringNames.ExileTextSN || id == StringNames.ExileTextPP || id == StringNames.ExileTextSP) { - __result = player.Data.PlayerName + " was The " + String.Join(" ", RoleInfo.getRoleInfoForPlayer(player).Select(x => x.name).ToArray()); + __result = player.Data.PlayerName + " was The " + String.Join(" ", RoleInfo.getRoleInfoForPlayer(player, false).Select(x => x.name).ToArray()); } // Hide number of remaining impostors on Jester win if (id == StringNames.ImpostorsRemainP || id == StringNames.ImpostorsRemainS) { if (Jester.jester != null && player.PlayerId == Jester.jester.PlayerId) __result = ""; } + if (Tiebreaker.isTiebreak) __result += " (Tiebreaker)"; + Tiebreaker.isTiebreak = false; } } catch { // pass - Hopefully prevent leaving while exiling to softlock game diff --git a/TheOtherRoles/Patches/FreeNamePatch.cs b/TheOtherRoles/Patches/FreeNamePatch.cs new file mode 100644 index 000000000..3af6fe88e --- /dev/null +++ b/TheOtherRoles/Patches/FreeNamePatch.cs @@ -0,0 +1,60 @@ +// Taken from https://github.com/NuclearPowered/Reactor/ , licensed under the LGPLv3 + +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using TMPro; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object = UnityEngine.Object; + +namespace TheOtherRoles.Patches { + internal class FreeNamePatch { + + public static void Initialize() { + SceneManager.add_sceneLoaded((Action)((scene, _) => { + if (!scene.name.Equals("MMOnline")) return; + if (!TryMoveObjects()) return; + + var editName = DestroyableSingleton.Instance.accountTab.editNameScreen; + var nameText = Object.Instantiate(editName.nameText.gameObject); + + nameText.transform.localPosition += Vector3.up * 2.2f; + + var textBox = nameText.GetComponent(); + textBox.outputText.alignment = TextAlignmentOptions.CenterGeoAligned; + textBox.outputText.transform.position = nameText.transform.position; + textBox.outputText.fontSize = 4f; + + textBox.OnChange.AddListener((Action)(() => { + SaveManager.PlayerName = textBox.text; + })); + textBox.OnEnter = textBox.OnFocusLost = textBox.OnChange; + + textBox.Pipe.GetComponent().fontSize = 4f; + })); + } + + private static bool TryMoveObjects() { + var toMove = new List + { + "HostGameButton", + "FindGameButton", + "JoinGameButton" + }; + + var yStart = Vector3.up; + var yOffset = Vector3.down * 1.5f; + + var gameObjects = toMove.Select(x => GameObject.Find("NormalMenu/" + x)).ToList(); + if (gameObjects.Any(x => x == null)) return false; + + for (var i = 0; i < gameObjects.Count; i++) { + gameObjects[i].transform.position = yStart + (yOffset * i); + } + + return true; + } + } +} diff --git a/TheOtherRoles/Patches/IntroPatch.cs b/TheOtherRoles/Patches/IntroPatch.cs index 5dcfae570..8689dcd4f 100644 --- a/TheOtherRoles/Patches/IntroPatch.cs +++ b/TheOtherRoles/Patches/IntroPatch.cs @@ -4,6 +4,7 @@ using UnityEngine; using System.Collections.Generic; using System.Linq; +using Hazel; namespace TheOtherRoles.Patches { [HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.OnDestroy))] @@ -52,7 +53,18 @@ public static void Prefix(IntroCutscene __instance) { } } + // First kill + if (AmongUsClient.Instance.AmHost && MapOptions.shieldFirstKill && MapOptions.firstKillName != "") { + PlayerControl target = PlayerControl.AllPlayerControls.ToArray().ToList().FirstOrDefault(x => x.Data.PlayerName.Equals(MapOptions.firstKillName)); + if (target != null) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetFirstKill, Hazel.SendOption.Reliable, -1); + writer.Write(target.PlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.setFirstKill(target.PlayerId); + } } + MapOptions.firstKillName = ""; + } } [HarmonyPatch] @@ -80,7 +92,7 @@ public static void setupIntroTeamIcons(IntroCutscene __instance, ref Il2CppSyst public static void setupIntroTeam(IntroCutscene __instance, ref Il2CppSystem.Collections.Generic.List yourTeam) { List infos = RoleInfo.getRoleInfoForPlayer(PlayerControl.LocalPlayer); - RoleInfo roleInfo = infos.Where(info => info.roleId != RoleId.Lover).FirstOrDefault(); + RoleInfo roleInfo = infos.Where(info => !info.isModifier).FirstOrDefault(); if (roleInfo == null) return; if (roleInfo.isNeutral) { var neutralColor = new Color32(76, 84, 78, 255); @@ -92,7 +104,6 @@ public static void setupIntroTeam(IntroCutscene __instance, ref Il2CppSystem.Co public static IEnumerator EndShowRole(IntroCutscene __instance) { yield return new WaitForSeconds(5f); - __instance.YouAreText.gameObject.SetActive(false); __instance.RoleText.gameObject.SetActive(false); __instance.RoleBlurbText.gameObject.SetActive(false); @@ -105,16 +116,22 @@ class SetUpRoleTextPatch { static public void SetRoleTexts(IntroCutscene __instance) { // Don't override the intro of the vanilla roles List infos = RoleInfo.getRoleInfoForPlayer(PlayerControl.LocalPlayer); - RoleInfo roleInfo = infos.Where(info => info.roleId != RoleId.Lover).FirstOrDefault(); + RoleInfo roleInfo = infos.Where(info => !info.isModifier).FirstOrDefault(); + RoleInfo modifierInfo = infos.Where(info => info.isModifier).FirstOrDefault(); + __instance.RoleBlurbText.text = ""; if (roleInfo != null) { __instance.RoleText.text = roleInfo.name; __instance.RoleText.color = roleInfo.color; __instance.RoleBlurbText.text = roleInfo.introDescription; __instance.RoleBlurbText.color = roleInfo.color; } - if (infos.Any(info => info.roleId == RoleId.Lover)) { - PlayerControl otherLover = PlayerControl.LocalPlayer == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1; - __instance.RoleBlurbText.text += Helpers.cs(Lovers.color, $"\n♥ You are in love with {otherLover?.Data?.PlayerName ?? ""} ♥"); + if (modifierInfo != null) { + if (modifierInfo.roleId != RoleId.Lover) + __instance.RoleBlurbText.text += Helpers.cs(modifierInfo.color, $"\n{modifierInfo.introDescription}"); + else { + PlayerControl otherLover = PlayerControl.LocalPlayer == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1; + __instance.RoleBlurbText.text += Helpers.cs(Lovers.color, $"\n♥ You are in love with {otherLover?.Data?.PlayerName ?? ""} ♥"); + } } if (Deputy.knowsSheriff && Deputy.deputy != null && Sheriff.sheriff != null) { if (infos.Any(info => info.roleId == RoleId.Sheriff)) diff --git a/TheOtherRoles/Patches/MapBehaviourPatch.cs b/TheOtherRoles/Patches/MapBehaviourPatch.cs new file mode 100644 index 000000000..cc7958081 --- /dev/null +++ b/TheOtherRoles/Patches/MapBehaviourPatch.cs @@ -0,0 +1,38 @@ +using HarmonyLib; +using UnityEngine; + + +namespace TheOtherRoles.Patches { + + [HarmonyPatch(typeof(MapBehaviour))] + class MapBehaviourPatch { + + [HarmonyPatch(typeof(MapBehaviour), nameof(MapBehaviour.FixedUpdate))] + static bool Prefix(MapBehaviour __instance) { + if (!MeetingHud.Instance) return true; // Only run in meetings, and then set the Position of the HerePoint to the Position before the Meeting! + if (!ShipStatus.Instance) { + return false; + } + Vector3 vector = AntiTeleport.position != null? AntiTeleport.position : PlayerControl.LocalPlayer.transform.position; + vector /= ShipStatus.Instance.MapScale; + vector.x *= Mathf.Sign(ShipStatus.Instance.transform.localScale.x); + vector.z = -1f; + __instance.HerePoint.transform.localPosition = vector; + PlayerControl.LocalPlayer.SetPlayerMaterialColors(__instance.HerePoint); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(MapBehaviour), nameof(MapBehaviour.ShowNormalMap))] + static bool Prefix3(MapBehaviour __instance) { + if (!MeetingHud.Instance || __instance.IsOpen) return true; // Only run in meetings and when the map is closed + + PlayerControl.LocalPlayer.SetPlayerMaterialColors(__instance.HerePoint); + __instance.GenericShow(); + __instance.taskOverlay.Show(); + __instance.ColorControl.SetColor(new Color(0.05f, 0.2f, 1f, 1f)); + DestroyableSingleton.Instance.SetHudActive(false); + return false; + } + } +} \ No newline at end of file diff --git a/TheOtherRoles/Patches/MeetingPatch.cs b/TheOtherRoles/Patches/MeetingPatch.cs index 82ad8006e..0687b0b8a 100644 --- a/TheOtherRoles/Patches/MeetingPatch.cs +++ b/TheOtherRoles/Patches/MeetingPatch.cs @@ -5,6 +5,7 @@ using UnhollowerBaseLib; using static TheOtherRoles.TheOtherRoles; using static TheOtherRoles.MapOptions; +using TheOtherRoles.Objects; using System.Collections; using System; using System.Text; @@ -18,6 +19,9 @@ class MeetingHudPatch { static SpriteRenderer[] renderers; private static GameData.PlayerInfo target = null; private const float scale = 0.65f; + private static TMPro.TextMeshPro swapperChargesText; + private static PassiveButton[] swapperButtonList; + private static TMPro.TextMeshPro swapperConfirmButtonLabel; [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.CheckForEndVoting))] class MeetingCalculateVotesPatch { @@ -55,6 +59,8 @@ private static Dictionary CalculateVotes(MeetingHud __instance) { } } + + return dictionary; } @@ -64,7 +70,7 @@ static bool Prefix(MeetingHud __instance) { // If skipping is disabled, replace skipps/no-votes with self vote if (target == null && blockSkippingInEmergencyMeetings && noVoteIsSelfVote) { foreach (PlayerVoteArea playerVoteArea in __instance.playerStates) { - if (playerVoteArea.VotedFor < 0) playerVoteArea.VotedFor = playerVoteArea.TargetPlayerId; // TargetPlayerId + if (playerVoteArea.VotedFor == byte.MaxValue - 1) playerVoteArea.VotedFor = playerVoteArea.TargetPlayerId; // TargetPlayerId } } @@ -73,6 +79,22 @@ static bool Prefix(MeetingHud __instance) { KeyValuePair max = self.MaxPair(out tie); GameData.PlayerInfo exiled = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(v => !tie && v.PlayerId == max.Key && !v.IsDead); + // TieBreaker + Tiebreaker.isTiebreak = false; + int maxVoteValue = self.Values.Max(); + List potentialExiled = new List(); + + + PlayerVoteArea tb = null; + if (Tiebreaker.tiebreaker != null) + tb = __instance.playerStates.ToArray().FirstOrDefault(x => x.TargetPlayerId == Tiebreaker.tiebreaker.PlayerId); + bool isTiebreakerSkip = tb == null || tb.VotedFor == 253; + if (tb != null && tb.AmDead) isTiebreakerSkip = true; + + foreach (KeyValuePair pair in self) + if (pair.Value == maxVoteValue && !isTiebreakerSkip && pair.Key != 253) + potentialExiled.Add(GameData.Instance.AllPlayers.ToArray().FirstOrDefault(x => x.PlayerId == pair.Key)); + MeetingHud.VoterState[] array = new MeetingHud.VoterState[__instance.playerStates.Length]; for (int i = 0; i < __instance.playerStates.Length; i++) { @@ -81,6 +103,15 @@ static bool Prefix(MeetingHud __instance) { VoterId = playerVoteArea.TargetPlayerId, VotedForId = playerVoteArea.VotedFor }; + + if (Tiebreaker.tiebreaker != null && tie && playerVoteArea.TargetPlayerId == Tiebreaker.tiebreaker.PlayerId && potentialExiled.FindAll(x => x != null && x.PlayerId == playerVoteArea.VotedFor).Count > 0) { + exiled = potentialExiled.ToArray().FirstOrDefault(v => v.PlayerId == playerVoteArea.VotedFor); + tie = false; + + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetTiebreak, Hazel.SendOption.Reliable, -1); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.setTiebreak(); + } } // RPCVotingComplete @@ -94,7 +125,7 @@ static bool Prefix(MeetingHud __instance) { class MeetingHudBloopAVoteIconPatch { public static bool Prefix(MeetingHud __instance, [HarmonyArgument(0)]GameData.PlayerInfo voterPlayer, [HarmonyArgument(1)]int index, [HarmonyArgument(2)]Transform parent) { SpriteRenderer spriteRenderer = UnityEngine.Object.Instantiate(__instance.PlayerVotePrefab); - if (!PlayerControl.GameOptions.AnonymousVotes || (PlayerControl.LocalPlayer.Data.IsDead && MapOptions.ghostsSeeVotes)) + if (!PlayerControl.GameOptions.AnonymousVotes || (PlayerControl.LocalPlayer.Data.IsDead && MapOptions.ghostsSeeVotes) || Mayor.mayor != null && PlayerControl.LocalPlayer == Mayor.mayor && Mayor.canSeeVoteColors && TasksHandler.taskInfo(PlayerControl.LocalPlayer.Data).Item1 >= Mayor.tasksNeededToSeeVoteColors) PlayerControl.SetPlayerMaterialColors(voterPlayer.DefaultOutfit.ColorId, spriteRenderer); else PlayerControl.SetPlayerMaterialColors(Palette.DisabledGrey, spriteRenderer); @@ -170,28 +201,27 @@ static void Postfix(MeetingHud __instance, [HarmonyArgument(0)]byte[] states, [H Swapper.playerId1 = Byte.MaxValue; Swapper.playerId2 = Byte.MaxValue; - // Lovers & Pursuer save next to be exiled, because RPC of ending game comes before RPC of exiled + // Lovers, Lawyer & Pursuer save next to be exiled, because RPC of ending game comes before RPC of exiled Lovers.notAckedExiledIsLover = false; Pursuer.notAckedExiled = false; if (exiled != null) { Lovers.notAckedExiledIsLover = ((Lovers.lover1 != null && Lovers.lover1.PlayerId == exiled.PlayerId) || (Lovers.lover2 != null && Lovers.lover2.PlayerId == exiled.PlayerId)); - Pursuer.notAckedExiled = (Pursuer.pursuer != null && Pursuer.pursuer.PlayerId == exiled.PlayerId); + Pursuer.notAckedExiled = (Pursuer.pursuer != null && Pursuer.pursuer.PlayerId == exiled.PlayerId) || (Lawyer.lawyer != null && Lawyer.target != null && Lawyer.target.PlayerId == exiled.PlayerId && Lawyer.target != Jester.jester); } - } } static void swapperOnClick(int i, MeetingHud __instance) { - if (__instance.state == MeetingHud.VoteStates.Results) return; + if (__instance.state == MeetingHud.VoteStates.Results || Swapper.charges <= 0) return; if (__instance.playerStates[i].AmDead) return; int selectedCount = selections.Where(b => b).Count(); SpriteRenderer renderer = renderers[i]; if (selectedCount == 0) { - renderer.color = Color.green; + renderer.color = Color.yellow; selections[i] = true; } else if (selectedCount == 1) { if (selections[i]) { @@ -199,30 +229,49 @@ static void swapperOnClick(int i, MeetingHud __instance) { selections[i] = false; } else { selections[i] = true; - renderer.color = Color.green; - - PlayerVoteArea firstPlayer = null; - PlayerVoteArea secondPlayer = null; - for (int A = 0; A < selections.Length; A++) { - if (selections[A]) { - if (firstPlayer != null) { - secondPlayer = __instance.playerStates[A]; - break; - } else { - firstPlayer = __instance.playerStates[A]; - } - } - } - - if (firstPlayer != null && secondPlayer != null) { - MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SwapperSwap, Hazel.SendOption.Reliable, -1); - writer.Write((byte)firstPlayer.TargetPlayerId); - writer.Write((byte)secondPlayer.TargetPlayerId); - AmongUsClient.Instance.FinishRpcImmediately(writer); + renderer.color = Color.yellow; + swapperConfirmButtonLabel.text = Helpers.cs(Color.yellow, "Confirm Swap"); + } + } else if (selectedCount == 2) { + if (selections[i]) { + renderer.color = Color.red; + selections[i] = false; + swapperConfirmButtonLabel.text = Helpers.cs(Color.red, "Confirm Swap"); + } + } + } - RPCProcedure.swapperSwap((byte)firstPlayer.TargetPlayerId, (byte)secondPlayer.TargetPlayerId); + static void swapperConfirm(MeetingHud __instance) { + __instance.playerStates[0].Cancel(); // This will stop the underlying buttons of the template from showing up + if (__instance.state == MeetingHud.VoteStates.Results) return; + if (selections.Where(b => b).Count() != 2) return; + if (Swapper.charges <= 0 || Swapper.playerId1 != Byte.MaxValue) return; + + PlayerVoteArea firstPlayer = null; + PlayerVoteArea secondPlayer = null; + for (int A = 0; A < selections.Length; A++) { + if (selections[A]) { + if (firstPlayer == null) { + firstPlayer = __instance.playerStates[A]; + } else { + secondPlayer = __instance.playerStates[A]; } - } + renderers[A].color = Color.green; + } else if (renderers[A] != null) { + renderers[A].color = Color.gray; + } + if (swapperButtonList[A] != null) swapperButtonList[A].OnClick.RemoveAllListeners(); // Swap buttons can't be clicked / changed anymore + } + if (firstPlayer != null && secondPlayer != null) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SwapperSwap, Hazel.SendOption.Reliable, -1); + writer.Write((byte)firstPlayer.TargetPlayerId); + writer.Write((byte)secondPlayer.TargetPlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + + RPCProcedure.swapperSwap((byte)firstPlayer.TargetPlayerId, (byte)secondPlayer.TargetPlayerId); + swapperConfirmButtonLabel.text = Helpers.cs(Color.green, "Swapping!"); + Swapper.charges--; + swapperChargesText.text = $"Swaps: {Swapper.charges}"; } } @@ -259,7 +308,7 @@ static void guesserOnClick(int buttonTarget, MeetingHud __instance) { foreach (RoleInfo roleInfo in RoleInfo.allRoleInfos) { RoleId guesserRole = (Guesser.niceGuesser != null && PlayerControl.LocalPlayer.PlayerId == Guesser.niceGuesser.PlayerId) ? RoleId.NiceGuesser : RoleId.EvilGuesser; - if (roleInfo.roleId == RoleId.Lover || roleInfo.roleId == guesserRole || roleInfo == RoleInfo.niceMini || (!Guesser.evilGuesserCanGuessSpy && guesserRole == RoleId.EvilGuesser && roleInfo.roleId == RoleId.Spy)) continue; // Not guessable roles + if (roleInfo.isModifier || roleInfo.roleId == guesserRole || (!Guesser.evilGuesserCanGuessSpy && guesserRole == RoleId.EvilGuesser && roleInfo.roleId == RoleId.Spy)) continue; // Not guessable roles & modifier if (Guesser.guesserCantGuessSnitch && Snitch.snitch != null) { var (playerCompleted, playerTotal) = TasksHandler.taskInfo(Snitch.snitch.Data); @@ -302,7 +351,7 @@ static void guesserOnClick(int buttonTarget, MeetingHud __instance) { return; } - var mainRoleInfo = RoleInfo.getRoleInfoForPlayer(focusedTarget).FirstOrDefault(); + var mainRoleInfo = RoleInfo.getRoleInfoForPlayer(focusedTarget, false).FirstOrDefault(); if (mainRoleInfo == null) return; PlayerControl dyingTarget = (mainRoleInfo == roleInfo) ? focusedTarget : PlayerControl.LocalPlayer; @@ -338,12 +387,12 @@ static bool Prefix(MeetingHud __instance) { } } - static void populateButtonsPostfix(MeetingHud __instance) { // Add Swapper Buttons if (Swapper.swapper != null && PlayerControl.LocalPlayer == Swapper.swapper && !Swapper.swapper.Data.IsDead) { selections = new bool[__instance.playerStates.Length]; renderers = new SpriteRenderer[__instance.playerStates.Length]; + swapperButtonList = new PassiveButton[__instance.playerStates.Length]; for (int i = 0; i < __instance.playerStates.Length; i++) { PlayerVoteArea playerVoteArea = __instance.playerStates[i]; @@ -358,7 +407,10 @@ static void populateButtonsPostfix(MeetingHud __instance) { renderer.sprite = Swapper.getCheckSprite(); renderer.color = Color.red; + if (Swapper.charges <= 0) renderer.color = Color.gray; + PassiveButton button = checkbox.GetComponent(); + swapperButtonList[i] = button; button.OnClick.RemoveAllListeners(); int copiedIndex = i; button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => swapperOnClick(copiedIndex, __instance))); @@ -366,6 +418,43 @@ static void populateButtonsPostfix(MeetingHud __instance) { selections[i] = false; renderers[i] = renderer; } + + // Add the "Confirm Swap" button and "Swaps: X" text next to it + Transform meetingUI = __instance.transform.FindChild("PhoneUI"); + var buttonTemplate = __instance.playerStates[0].transform.FindChild("votePlayerBase"); + var maskTemplate = __instance.playerStates[0].transform.FindChild("MaskArea"); + var textTemplate = __instance.playerStates[0].NameText; + Transform confirmSwapButtonParent = (new GameObject()).transform; + confirmSwapButtonParent.SetParent(meetingUI); + Transform confirmSwapButton = UnityEngine.Object.Instantiate(buttonTemplate, confirmSwapButtonParent); + + Transform infoTransform = __instance.playerStates[0].NameText.transform.parent.FindChild("Info"); + TMPro.TextMeshPro meetingInfo = infoTransform != null ? infoTransform.GetComponent() : null; + swapperChargesText = UnityEngine.Object.Instantiate(__instance.playerStates[0].NameText, confirmSwapButtonParent); + swapperChargesText.text = $"Swaps: {Swapper.charges}"; + swapperChargesText.enableWordWrapping = false; + swapperChargesText.transform.localScale = Vector3.one * 1.7f; + swapperChargesText.transform.localPosition = new Vector3(-2.5f, 0f, 0f); + + Transform confirmSwapButtonMask = UnityEngine.Object.Instantiate(maskTemplate, confirmSwapButtonParent); + swapperConfirmButtonLabel = UnityEngine.Object.Instantiate(textTemplate, confirmSwapButton); + confirmSwapButton.GetComponent().sprite = DestroyableSingleton.Instance.GetNamePlateById("nameplate_NoPlate")?.viewData?.viewData?.Image; + confirmSwapButtonParent.localPosition = new Vector3(0, -2.225f, -5); + confirmSwapButtonParent.localScale = new Vector3(0.55f, 0.55f, 1f); + swapperConfirmButtonLabel.text = Helpers.cs(Color.red, "Confirm Swap"); + swapperConfirmButtonLabel.alignment = TMPro.TextAlignmentOptions.Center; + swapperConfirmButtonLabel.transform.localPosition = new Vector3(0, 0, swapperConfirmButtonLabel.transform.localPosition.z); + swapperConfirmButtonLabel.transform.localScale *= 1.7f; + + PassiveButton passiveButton = confirmSwapButton.GetComponent(); + passiveButton.OnClick.RemoveAllListeners(); + if (!PlayerControl.LocalPlayer.Data.IsDead) passiveButton.OnClick.AddListener((UnityEngine.Events.UnityAction)(() => swapperConfirm(__instance))); + confirmSwapButton.parent.gameObject.SetActive(false); + __instance.StartCoroutine(Effects.Lerp(7.27f, new Action((p) => { // Button appears delayed, so that its visible in the voting screen only! + if (p == 1f) { + confirmSwapButton.parent.gameObject.SetActive(true); + } + }))); } //Fix visor in Meetings @@ -430,6 +519,10 @@ static void Postfix(MeetingHud __instance, [HarmonyArgument(0)]MessageReader rea [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CoStartMeeting))] class StartMeetingPatch { public static void Prefix(PlayerControl __instance, [HarmonyArgument(0)]GameData.PlayerInfo meetingTarget) { + // Resett Bait list + Bait.active = new Dictionary(); + // Safe AntiTeleport positions + AntiTeleport.position = PlayerControl.LocalPlayer.transform.position; // Medium meeting start time Medium.meetingStartTime = DateTime.UtcNow; // Reset vampire bitten @@ -438,6 +531,20 @@ public static void Prefix(PlayerControl __instance, [HarmonyArgument(0)]GameData if (meetingTarget == null) meetingsCount++; // Save the meeting target target = meetingTarget; + + + // Add Portal info into Portalmaker Chat: + if (Portalmaker.portalmaker != null && PlayerControl.LocalPlayer == Portalmaker.portalmaker && !PlayerControl.LocalPlayer.Data.IsDead) { + foreach (var entry in Portal.teleportedPlayers) { + float timeBeforeMeeting = ((float)(DateTime.UtcNow - entry.time).TotalMilliseconds) / 1000; + string msg = Portalmaker.logShowsTime ? $"{(int)timeBeforeMeeting}s ago: " : ""; + msg = msg + $"{entry.name} used the teleporter"; + DestroyableSingleton.Instance.Chat.AddChat(PlayerControl.LocalPlayer, $"{msg}"); + } + } + + // Remove first kill shield + MapOptions.firstKillPlayer = null; } } diff --git a/TheOtherRoles/Patches/PlayerControlPatch.cs b/TheOtherRoles/Patches/PlayerControlPatch.cs index 69ef856a9..6b66b506e 100644 --- a/TheOtherRoles/Patches/PlayerControlPatch.cs +++ b/TheOtherRoles/Patches/PlayerControlPatch.cs @@ -61,15 +61,23 @@ static void setBasePlayerOutlines() { bool isMorphedMorphling = target == Morphling.morphling && Morphling.morphTarget != null && Morphling.morphTimer > 0f; bool hasVisibleShield = false; + Color color = Medic.shieldedColor; 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 + // Make shield invisible till after the next meeting if the option is set (the medic can already see the shield) + hasVisibleShield = hasVisibleShield && (Medic.meetingAfterShielding || !Medic.showShieldAfterMeeting || PlayerControl.LocalPlayer == Medic.medic); + } + + if (Camouflager.camouflageTimer <= 0f && MapOptions.firstKillPlayer != null && MapOptions.shieldFirstKill && ((target == MapOptions.firstKillPlayer && !isMorphedMorphling) || (isMorphedMorphling && Morphling.morphTarget == MapOptions.firstKillPlayer))) { + hasVisibleShield = true; + color = Color.blue; } if (hasVisibleShield) { - target.MyRend.material.SetFloat("_Outline", 1f); - target.MyRend.material.SetColor("_OutlineColor", Medic.shieldedColor); + target.MyRend.material.SetFloat("_Outline", 1f); + target.MyRend.material.SetColor("_OutlineColor", color); } else { target.MyRend.material.SetFloat("_Outline", 0f); @@ -186,16 +194,16 @@ static void vampireSetTarget() { if (Vampire.vampire == null || Vampire.vampire != PlayerControl.LocalPlayer) return; PlayerControl target = null; - if (Spy.spy != null) { + if (Spy.spy != null || Sidekick.wasSpy || Jackal.wasSpy) { if (Spy.impostorsCanKillAnyone) { target = setTarget(false, true); } else { - target = setTarget(true, true, new List() { Spy.spy }); + target = setTarget(true, true, new List() { Spy.spy, Sidekick.wasTeamRed ? Sidekick.sidekick : null, Jackal.wasTeamRed ? Jackal.jackal : null }); } } else { - target = setTarget(true, true); + target = setTarget(true, true, new List() { Sidekick.wasImpostor ? Sidekick.sidekick : null, Jackal.wasImpostor ? Jackal.jackal : null }); } bool targetNearGarlic = false; @@ -248,6 +256,8 @@ static void eraserSetTarget() { List untargetables = new List(); if (Spy.spy != null) untargetables.Add(Spy.spy); + if (Sidekick.wasTeamRed) untargetables.Add(Sidekick.sidekick); + if (Jackal.wasTeamRed) untargetables.Add(Jackal.jackal); Eraser.currentTarget = setTarget(onlyCrewmates: !Eraser.canEraseAnyone, untargetablePlayers: Eraser.canEraseAnyone ? new List() : untargetables); setPlayerOutline(Eraser.currentTarget, Eraser.color); } @@ -293,16 +303,16 @@ static void impostorSetTarget() { } PlayerControl target = null; - if (Spy.spy != null) { + if (Spy.spy != null || Sidekick.wasSpy || Jackal.wasSpy) { if (Spy.impostorsCanKillAnyone) { target = setTarget(false, true); } else { - target = setTarget(true, true, new List() { Spy.spy }); + target = setTarget(true, true, new List() { Spy.spy, Sidekick.wasTeamRed ? Sidekick.sidekick : null, Jackal.wasTeamRed ? Jackal.jackal : null}); } } else { - target = setTarget(true, true); + target = setTarget(true, true, new List() { Sidekick.wasImpostor ? Sidekick.sidekick : null, Jackal.wasImpostor ? Jackal.jackal : null}); } HudManager.Instance.KillButton.SetTarget(target); // Includes setPlayerOutline(target, Palette.ImpstorRed); @@ -324,6 +334,36 @@ static void warlockSetTarget() { } } + static void ninjaUpdate() + { + if (Ninja.arrow?.arrow != null) + { + if (Ninja.ninja == null || Ninja.ninja != PlayerControl.LocalPlayer || !Ninja.knowsTargetLocation) { + Ninja.arrow.arrow.SetActive(false); + return; + } + if (Ninja.ninjaMarked != null && !PlayerControl.LocalPlayer.Data.IsDead) + { + bool trackedOnMap = !Ninja.ninjaMarked.Data.IsDead; + Vector3 position = Ninja.ninjaMarked.transform.position; + if (!trackedOnMap) + { // Check for dead body + DeadBody body = UnityEngine.Object.FindObjectsOfType().FirstOrDefault(b => b.ParentId == Ninja.ninjaMarked.PlayerId); + if (body != null) + { + trackedOnMap = true; + position = body.transform.position; + } + } + Ninja.arrow.Update(position); + Ninja.arrow.arrow.SetActive(trackedOnMap); + } else + { + Ninja.arrow.arrow.SetActive(false); + } + } + } + static void trackerUpdate() { // Handle player tracking if (Tracker.arrow?.arrow != null) { @@ -387,7 +427,7 @@ public static void playerSizeUpdate(PlayerControl p) { 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 || Mini.mini == Morphling.morphling && Morphling.morphTimer > 0) return; float growingProgress = Mini.growingProgress(); float scale = growingProgress * 0.35f + 0.35f; @@ -432,13 +472,16 @@ public static void updatePlayerInfo() { } var (tasksCompleted, tasksTotal) = TasksHandler.taskInfo(p.Data); - string roleNames = RoleInfo.GetRolesString(p, true); + string roleNames = RoleInfo.GetRolesString(p, true, false); + string roleText = RoleInfo.GetRolesString(p, true, MapOptions.ghostsSeeModifier); string taskInfo = tasksTotal > 0 ? $"({tasksCompleted}/{tasksTotal})" : ""; string playerInfoText = ""; - string meetingInfoText = ""; + string meetingInfoText = ""; if (p == PlayerControl.LocalPlayer) { + if (p.Data.IsDead) roleNames = roleText; playerInfoText = $"{roleNames}"; + if (p == Swapper.swapper) playerInfoText = $"{roleNames}" + Helpers.cs(Swapper.color, $" ({Swapper.charges})"); if (DestroyableSingleton.InstanceExists) { TMPro.TextMeshPro tabText = DestroyableSingleton.Instance.tab.transform.FindChild("TabText_TMP").GetComponent(); tabText.SetText($"Tasks {taskInfo}"); @@ -446,7 +489,7 @@ public static void updatePlayerInfo() { meetingInfoText = $"{roleNames} {taskInfo}".Trim(); } else if (MapOptions.ghostsSeeRoles && MapOptions.ghostsSeeTasks) { - playerInfoText = $"{roleNames} {taskInfo}".Trim(); + playerInfoText = $"{roleText} {taskInfo}".Trim(); meetingInfoText = playerInfoText; } else if (MapOptions.ghostsSeeTasks) { @@ -454,7 +497,7 @@ public static void updatePlayerInfo() { meetingInfoText = playerInfoText; } else if (MapOptions.ghostsSeeRoles || (Lawyer.lawyerKnowsRole && PlayerControl.LocalPlayer == Lawyer.lawyer && p == Lawyer.target)) { - playerInfoText = $"{roleNames}"; + playerInfoText = $"{roleText}"; meetingInfoText = playerInfoText; } @@ -566,7 +609,7 @@ static void bountyHunterUpdate() { BountyHunter.bountyUpdateTimer = BountyHunter.bountyDuration; var possibleTargets = new List(); foreach (PlayerControl p in PlayerControl.AllPlayerControls) { - if (!p.Data.IsDead && !p.Data.Disconnected && p != p.Data.Role.IsImpostor && p != Spy.spy && (p != Mini.mini || Mini.isGrownUp()) && (Lovers.getPartner(BountyHunter.bountyHunter) == null || p != Lovers.getPartner(BountyHunter.bountyHunter))) possibleTargets.Add(p); + if (!p.Data.IsDead && !p.Data.Disconnected && p != p.Data.Role.IsImpostor && p != Spy.spy && (p != Sidekick.sidekick || !Sidekick.wasTeamRed) && (p != Jackal.jackal || !Jackal.wasTeamRed) && (p != Mini.mini || Mini.isGrownUp()) && (Lovers.getPartner(BountyHunter.bountyHunter) == null || p != Lovers.getPartner(BountyHunter.bountyHunter))) possibleTargets.Add(p); } BountyHunter.bounty = possibleTargets[TheOtherRoles.rnd.Next(0, possibleTargets.Count)]; if (BountyHunter.bounty == null) return; @@ -595,49 +638,6 @@ static void bountyHunterUpdate() { } } - static void baitUpdate() { - if (Bait.bait == null || Bait.bait != PlayerControl.LocalPlayer) return; - - // Bait report - if (Bait.bait.Data.IsDead && !Bait.reported) { - 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); - - MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedCmdReportDeadBody, Hazel.SendOption.Reliable, -1); - writer.Write(deadPlayer.killerIfExisting.PlayerId); - writer.Write(Bait.bait.PlayerId); - AmongUsClient.Instance.FinishRpcImmediately(writer); - Bait.reported = true; - } - } - - // Bait Vents - if (ShipStatus.Instance?.AllVents != null) { - var ventsWithPlayers = new List(); - foreach (PlayerControl player in PlayerControl.AllPlayerControls) { - if (player.inVent) { - Vent target = ShipStatus.Instance.AllVents.OrderBy(x => Vector2.Distance(x.transform.position, player.GetTruePosition())).FirstOrDefault(); - if (target != null) ventsWithPlayers.Add(target.Id); - } - } - - foreach (Vent vent in ShipStatus.Instance.AllVents) { - if (vent.myRend == null || vent.myRend.material == null) continue; - if (ventsWithPlayers.Contains(vent.Id) || (ventsWithPlayers.Count > 0 && Bait.highlightAllVents)) { - vent.myRend.material.SetFloat("_Outline", 1f); - vent.myRend.material.SetColor("_OutlineColor", Color.yellow); - } - else { - vent.myRend.material.SetFloat("_Outline", 0); - } - } - } - } - static void vultureUpdate() { if (Vulture.vulture == null || PlayerControl.LocalPlayer != Vulture.vulture || Vulture.localArrows == null || !Vulture.showArrows) return; if (Vulture.vulture.Data.IsDead) { @@ -734,6 +734,16 @@ public static void hackerUpdate() { } } + // For swapper swap charges + public static void swapperUpdate() { + if (Swapper.swapper == null || PlayerControl.LocalPlayer != Swapper.swapper || PlayerControl.LocalPlayer.Data.IsDead) return; + var (playerCompleted, _) = TasksHandler.taskInfo(PlayerControl.LocalPlayer.Data); + if (playerCompleted == Swapper.rechargedTasks) { + Swapper.rechargedTasks += Swapper.rechargeTasksNumber; + Swapper.charges++; + } + } + static void pursuerSetTarget() { if (Pursuer.pursuer == null || Pursuer.pursuer != PlayerControl.LocalPlayer) return; Pursuer.target = setTarget(); @@ -748,13 +758,72 @@ static void witchSetTarget() { else { untargetables = new List(); // Also target players that have already been spelled, to hide spells that were blanks/blocked by shields if (Spy.spy != null && !Witch.canSpellAnyone) untargetables.Add(Spy.spy); + if (Sidekick.wasTeamRed && !Witch.canSpellAnyone) untargetables.Add(Sidekick.sidekick); + if (Jackal.wasTeamRed && !Witch.canSpellAnyone) untargetables.Add(Jackal.jackal); } Witch.currentTarget = setTarget(onlyCrewmates: !Witch.canSpellAnyone, untargetablePlayers: untargetables); setPlayerOutline(Witch.currentTarget, Witch.color); } + static void ninjaSetTarget() + { + if (Ninja.ninja == null || Ninja.ninja != PlayerControl.LocalPlayer) return; + List untargetables = new List(); + if (Spy.spy != null && !Spy.impostorsCanKillAnyone) untargetables.Add(Spy.spy); + if (Mini.mini != null) untargetables.Add(Mini.mini); + if (Sidekick.wasTeamRed && !Spy.impostorsCanKillAnyone) untargetables.Add(Sidekick.sidekick); + if (Jackal.wasTeamRed && !Spy.impostorsCanKillAnyone) untargetables.Add(Jackal.jackal); + Ninja.currentTarget = setTarget(onlyCrewmates: true, untargetablePlayers: untargetables); + setPlayerOutline(Ninja.currentTarget, Ninja.color); + } + + static void baitUpdate() { + if (!Bait.active.Any()) return; + + // Bait report + foreach (KeyValuePair entry in new Dictionary(Bait.active)) { + Bait.active[entry.Key] = entry.Value - Time.fixedDeltaTime; + if (entry.Value <= 0) { + Bait.active.Remove(entry.Key); + if (entry.Key.killerIfExisting != null) { + Helpers.handleVampireBiteOnBodyReport(); // Manually call Vampire handling, since the CmdReportDeadBody Prefix won't be called + RPCProcedure.uncheckedCmdReportDeadBody(entry.Key.killerIfExisting.PlayerId, entry.Key.player.PlayerId); + + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.UncheckedCmdReportDeadBody, Hazel.SendOption.Reliable, -1); + writer.Write(entry.Key.killerIfExisting.PlayerId); + writer.Write(entry.Key.player.PlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + } + } + } + + static void bloodyUpdate() { + if (!Bloody.active.Any()) return; + foreach (KeyValuePair entry in new Dictionary(Bloody.active)) { + PlayerControl player = Helpers.playerById(entry.Key); + PlayerControl bloodyPlayer = Helpers.playerById(Bloody.bloodyKillerMap[player.PlayerId]); + new Bloodytrail(player, bloodyPlayer); + Bloody.active[entry.Key] = entry.Value - Time.fixedDeltaTime; + if (entry.Value <= 0) Bloody.active.Remove(entry.Key); + } + } + + // Mini set adapted button cooldown for Vampire, Sheriff, Jackal, Sidekick, Warlock, Cleaner + public static void miniCooldownUpdate() { + if (Mini.mini != null && PlayerControl.LocalPlayer == Mini.mini) { + var multiplier = Mini.isGrownUp() ? 0.66f : 2f; + HudManagerStartPatch.sheriffKillButton.MaxTimer = Sheriff.cooldown * multiplier; + HudManagerStartPatch.vampireKillButton.MaxTimer = Vampire.cooldown * multiplier; + HudManagerStartPatch.jackalKillButton.MaxTimer = Jackal.cooldown * multiplier; + HudManagerStartPatch.sidekickKillButton.MaxTimer = Sidekick.cooldown * multiplier; + HudManagerStartPatch.warlockCurseButton.MaxTimer = Warlock.cooldown * multiplier; + HudManagerStartPatch.cleanerCleanButton.MaxTimer = Cleaner.cooldown * multiplier; + HudManagerStartPatch.witchSpellButton.MaxTimer = (Witch.cooldown + Witch.currentCooldownAddition) * multiplier; + } + } - public static void Postfix(PlayerControl __instance) { + public static void Postfix(PlayerControl __instance) { if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return; // Mini and Morphling shrink @@ -817,8 +886,6 @@ public static void Postfix(PlayerControl __instance) { snitchUpdate(); // BountyHunter bountyHunterUpdate(); - // Bait - baitUpdate(); // Vulture vultureUpdate(); // Medium @@ -831,7 +898,24 @@ public static void Postfix(PlayerControl __instance) { pursuerSetTarget(); // Witch witchSetTarget(); + // Ninja + ninjaSetTarget(); + NinjaTrace.UpdateAll(); + ninjaUpdate(); + hackerUpdate(); + swapperUpdate(); + + // Hacker + hackerUpdate(); + + // --MODIFIER-- + // Bait + baitUpdate(); + // Bloody + bloodyUpdate(); + // mini (for the cooldowns) + miniCooldownUpdate(); } } } @@ -841,6 +925,7 @@ class PlayerPhysicsWalkPlayerToPatch { private static Vector2 offset = Vector2.zero; public static void Prefix(PlayerPhysics __instance) { bool correctOffset = Camouflager.camouflageTimer <= 0f && (__instance.myPlayer == Mini.mini || (Morphling.morphling != null && __instance.myPlayer == Morphling.morphling && Morphling.morphTarget == Mini.mini && Morphling.morphTimer > 0f)); + correctOffset = correctOffset && !(Mini.mini == Morphling.morphling && Morphling.morphTimer > 0f); if (correctOffset) { float currentScaling = (Mini.growingProgress() + 1) * 0.5f; __instance.myPlayer.Collider.offset = currentScaling * Mini.defaultColliderOffset * Vector2.down; @@ -951,22 +1036,6 @@ public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)]PlayerC 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) { - 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) { Helpers.showFlash(new Color(42f / 255f, 187f / 255f, 245f / 255f)); @@ -981,12 +1050,6 @@ public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)]PlayerC Medium.featureDeadBodies.Add(new Tuple(deadPlayer, target.transform.position)); } - // Mini set adapted kill cooldown - if (Mini.mini != null && PlayerControl.LocalPlayer == Mini.mini && Mini.mini.Data.Role.IsImpostor && Mini.mini == __instance) { - var multiplier = Mini.isGrownUp() ? 0.66f : 2f; - Mini.mini.SetKillTimer(PlayerControl.GameOptions.KillCooldown * multiplier); - } - // Set bountyHunter cooldown if (BountyHunter.bountyHunter != null && PlayerControl.LocalPlayer == BountyHunter.bountyHunter && __instance == BountyHunter.bountyHunter) { if (target == BountyHunter.bounty) { @@ -997,10 +1060,62 @@ public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)]PlayerC BountyHunter.bountyHunter.SetKillTimer(PlayerControl.GameOptions.KillCooldown + BountyHunter.punishmentTime); } - // Show flash on bait kill to the killer if enabled - if (Bait.bait != null && target == Bait.bait && Bait.showKillFlash && __instance == PlayerControl.LocalPlayer) { - Helpers.showFlash(new Color(204f / 255f, 102f / 255f, 0f / 255f)); + // Mini Set Impostor Mini kill timer (Due to mini being a modifier, all "SetKillTimers" must have happened before this!) + if (Mini.mini != null && __instance == Mini.mini && __instance == PlayerControl.LocalPlayer) { + float multiplier = 1f; + if (Mini.mini != null && PlayerControl.LocalPlayer == Mini.mini) multiplier = Mini.isGrownUp() ? 0.66f : 2f; + Mini.mini.SetKillTimer(__instance.killTimer * multiplier); } + + // 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) { + HudManagerStartPatch.warlockCurseButton.Timer = Warlock.warlock.killTimer; + } + } + // Ninja Button Sync + if (Ninja.ninja != null && PlayerControl.LocalPlayer == Ninja.ninja && __instance == Ninja.ninja && HudManagerStartPatch.ninjaButton != null) + HudManagerStartPatch.ninjaButton.Timer = HudManagerStartPatch.ninjaButton.MaxTimer; + + + // Bait + if (Bait.bait.FindAll(x => x.PlayerId == target.PlayerId).Count > 0) { + float reportDelay = (float) rnd.Next((int)Bait.reportDelayMin, (int)Bait.reportDelayMax + 1); + Bait.active.Add(deadPlayer, reportDelay); + + if (Bait.showKillFlash && __instance == PlayerControl.LocalPlayer) Helpers.showFlash(new Color(204f / 255f, 102f / 255f, 0f / 255f)); + } + + // Add Bloody Modifier + if (Bloody.bloody.FindAll(x => x.PlayerId == target.PlayerId).Count > 0) { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.Bloody, Hazel.SendOption.Reliable, -1); + writer.Write(__instance.PlayerId); + writer.Write(target.PlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.bloody(__instance.PlayerId, target.PlayerId); + } + + // VIP Modifier + if (Vip.vip.FindAll(x => x.PlayerId == target.PlayerId).Count > 0) { + Color color = Color.yellow; + if (Vip.showColor) { + color = Color.white; + if (target.Data.Role.IsImpostor) color = Color.red; + else if (RoleInfo.getRoleInfoForPlayer(target, false).FirstOrDefault().isNeutral) color = Color.blue; + } + Helpers.showFlash(color, 1.5f); + } + + // First kill + if (MapOptions.firstKillName == "") MapOptions.firstKillName = target.Data.PlayerName; } } @@ -1010,7 +1125,7 @@ public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)]float ti if (PlayerControl.GameOptions.KillCooldown <= 0f) return false; float multiplier = 1f; float addition = 0f; - if (Mini.mini != null && PlayerControl.LocalPlayer == Mini.mini && Mini.mini.Data.Role.IsImpostor) multiplier = Mini.isGrownUp() ? 0.66f : 2f; + if (Mini.mini != null && PlayerControl.LocalPlayer == Mini.mini) multiplier = Mini.isGrownUp() ? 0.66f : 2f; if (BountyHunter.bountyHunter != null && PlayerControl.LocalPlayer == BountyHunter.bountyHunter) addition = BountyHunter.punishmentTime; __instance.killTimer = Mathf.Clamp(time, 0f, PlayerControl.GameOptions.KillCooldown * multiplier + addition); @@ -1074,11 +1189,18 @@ public static void Postfix(PlayerControl __instance) } // 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) { + if (__instance == Lawyer.target && Lawyer.target != Jester.jester && AmongUsClient.Instance.AmHost) { MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.LawyerPromotesToPursuer, Hazel.SendOption.Reliable, -1); AmongUsClient.Instance.FinishRpcImmediately(writer); RPCProcedure.lawyerPromotesToPursuer(); } } } + + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.FixedUpdate))] + public static class PlayerPhysicsFixedUpdate { + public static void Postfix(PlayerPhysics __instance) { + if (__instance.AmOwner && !PlayerControl.LocalPlayer.Data.IsDead && Invert.invert.FindAll(x => x.PlayerId == PlayerControl.LocalPlayer.PlayerId).Count > 0 && Invert.meetings > 0 && GameData.Instance && __instance.myPlayer.CanMove) __instance.body.velocity *= -1; + } + } } diff --git a/TheOtherRoles/Patches/RoleAssignmentPatch.cs b/TheOtherRoles/Patches/RoleAssignmentPatch.cs index ac7aa85a3..bdce8c382 100644 --- a/TheOtherRoles/Patches/RoleAssignmentPatch.cs +++ b/TheOtherRoles/Patches/RoleAssignmentPatch.cs @@ -37,6 +37,7 @@ private static void assignRoles() { assignDependentRoles(data); // Assign roles that may have a dependent role assignChanceRoles(data); // Assign roles that may or may not be in the game last assignRoleTargets(data); + assignModifiers(); } private static RoleAssignmentData getRoleAssignmentData() { @@ -82,6 +83,7 @@ private static RoleAssignmentData getRoleAssignmentData() { impSettings.Add((byte)RoleId.Warlock, CustomOptionHolder.warlockSpawnRate.getSelection()); impSettings.Add((byte)RoleId.BountyHunter, CustomOptionHolder.bountyHunterSpawnRate.getSelection()); impSettings.Add((byte)RoleId.Witch, CustomOptionHolder.witchSpawnRate.getSelection()); + impSettings.Add((byte)RoleId.Ninja, CustomOptionHolder.ninjaSpawnRate.getSelection()); neutralSettings.Add((byte)RoleId.Jester, CustomOptionHolder.jesterSpawnRate.getSelection()); neutralSettings.Add((byte)RoleId.Arsonist, CustomOptionHolder.arsonistSpawnRate.getSelection()); @@ -90,6 +92,7 @@ private static RoleAssignmentData getRoleAssignmentData() { neutralSettings.Add((byte)RoleId.Lawyer, CustomOptionHolder.lawyerSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Mayor, CustomOptionHolder.mayorSpawnRate.getSelection()); + crewSettings.Add((byte)RoleId.Portalmaker, CustomOptionHolder.portalmakerSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Engineer, CustomOptionHolder.engineerSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Lighter, CustomOptionHolder.lighterSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Detective, CustomOptionHolder.detectiveSpawnRate.getSelection()); @@ -101,7 +104,6 @@ private static RoleAssignmentData getRoleAssignmentData() { crewSettings.Add((byte)RoleId.Hacker, CustomOptionHolder.hackerSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Tracker, CustomOptionHolder.trackerSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Snitch, CustomOptionHolder.snitchSpawnRate.getSelection()); - crewSettings.Add((byte)RoleId.Bait, CustomOptionHolder.baitSpawnRate.getSelection()); crewSettings.Add((byte)RoleId.Medium, CustomOptionHolder.mediumSpawnRate.getSelection()); if (impostors.Count > 1) { // Only add Spy if more than 1 impostor as the spy role is otherwise useless @@ -122,29 +124,6 @@ private static RoleAssignmentData getRoleAssignmentData() { } private static void assignSpecialRoles(RoleAssignmentData data) { - // Assign Lovers - if (rnd.Next(1, 101) <= CustomOptionHolder.loversSpawnRate.getSelection() * 10) { - bool isOnlyRole = !CustomOptionHolder.loversCanHaveAnotherRole.getBool(); - if (data.impostors.Count > 0 && data.crewmates.Count > 0 && (!isOnlyRole || (data.maxCrewmateRoles > 0 && data.maxImpostorRoles > 0)) && rnd.Next(1, 101) <= CustomOptionHolder.loversImpLoverRate.getSelection() * 10) { - setRoleToRandomPlayer((byte)RoleId.Lover, data.impostors, 0, isOnlyRole); - setRoleToRandomPlayer((byte)RoleId.Lover, data.crewmates, 1, isOnlyRole); - if (isOnlyRole) { - data.maxCrewmateRoles--; - data.maxImpostorRoles--; - } - } else if (data.crewmates.Count >= 2 && (isOnlyRole || data.maxCrewmateRoles >= 2)) { - byte firstLoverId = setRoleToRandomPlayer((byte)RoleId.Lover, data.crewmates, 0, isOnlyRole); - if (isOnlyRole) { - setRoleToRandomPlayer((byte)RoleId.Lover, data.crewmates, 1); - data.maxCrewmateRoles -= 2; - } else { - var crewmatesWithoutFirstLover = data.crewmates.ToList(); - crewmatesWithoutFirstLover.RemoveAll(p => p.PlayerId == firstLoverId); - setRoleToRandomPlayer((byte)RoleId.Lover, crewmatesWithoutFirstLover, 1, false); - } - } - } - // Assign Mafia if (data.impostors.Count >= 3 && data.maxImpostorRoles >= 3 && (rnd.Next(1, 101) <= CustomOptionHolder.mafiaSpawnRate.getSelection() * 10)) { setRoleToRandomPlayer((byte)RoleId.Godfather, data.impostors); @@ -155,13 +134,6 @@ private static void assignSpecialRoles(RoleAssignmentData data) { } private static void selectFactionForFactionIndependentRoles(RoleAssignmentData data) { - // Assign Mini (33% chance impostor / 67% chance crewmate) - if (data.impostors.Count > 0 && data.maxImpostorRoles > 0 && rnd.Next(1, 101) <= 33) { - data.impSettings.Add((byte)RoleId.Mini, CustomOptionHolder.miniSpawnRate.getSelection()); - } else if (data.crewmates.Count > 0 && data.maxCrewmateRoles > 0) { - data.crewSettings.Add((byte)RoleId.Mini, CustomOptionHolder.miniSpawnRate.getSelection()); - } - // Assign Guesser (chance to be impostor based on setting) isEvilGuesser = rnd.Next(1, 101) <= CustomOptionHolder.guesserIsImpGuesserRate.getSelection() * 10; if ((CustomOptionHolder.guesserSpawnBothRate.getSelection() > 0 && @@ -359,7 +331,7 @@ private static void assignRoleTargets(RoleAssignmentData data) { if (Lawyer.lawyer != null) { var possibleTargets = new List(); foreach (PlayerControl p in PlayerControl.AllPlayerControls) { - if (!p.Data.IsDead && !p.Data.Disconnected && p != Lovers.lover1 && p != Lovers.lover2 && (p.Data.Role.IsImpostor || p == Jackal.jackal)) + if (!p.Data.IsDead && !p.Data.Disconnected && p != Lovers.lover1 && p != Lovers.lover2 && (p.Data.Role.IsImpostor || p == Jackal.jackal || (Lawyer.targetCanBeJester && p == Jester.jester))) possibleTargets.Add(p); } if (possibleTargets.Count == 0) { @@ -376,7 +348,74 @@ private static void assignRoleTargets(RoleAssignmentData data) { } } - private static byte setRoleToRandomPlayer(byte roleId, List playerList, byte flag = 0, bool removePlayer = true) { + private static void assignModifiers() { + var modifierMin = CustomOptionHolder.modifiersCountMin.getSelection(); + var modifierMax = CustomOptionHolder.modifiersCountMax.getSelection(); + if (modifierMin > modifierMax) modifierMin = modifierMax; + int modifierCountSettings = rnd.Next(modifierMin, modifierMax + 1); + List players = PlayerControl.AllPlayerControls.ToArray().ToList(); + int modifierCount = Mathf.Min(players.Count, modifierCountSettings); + + if (modifierCount == 0) return; + + List allModifiers = new List(); + List ensuredModifiers = new List(); + List chanceModifiers = new List(); + allModifiers.AddRange(new List { + RoleId.Tiebreaker, + RoleId.Mini, + RoleId.Bait, + RoleId.Bloody, + RoleId.AntiTeleport, + RoleId.Sunglasses, + RoleId.Vip, + RoleId.Invert + }); + + if (rnd.Next(1, 101) <= CustomOptionHolder.modifierLover.getSelection() * 10) { // Assign lover + bool isEvilLover = rnd.Next(1, 101) <= CustomOptionHolder.modifierLoverImpLoverRate.getSelection() * 10; + byte firstLoverId; + List impPlayer = new List(players); + List crewPlayer = new List(players); + impPlayer.RemoveAll(x => !x.Data.Role.IsImpostor); + crewPlayer.RemoveAll(x => x.Data.Role.IsImpostor); + + if (isEvilLover) firstLoverId = setModifierToRandomPlayer((byte)RoleId.Lover, impPlayer); + else firstLoverId = setModifierToRandomPlayer((byte)RoleId.Lover, crewPlayer); + byte secondLoverId = setModifierToRandomPlayer((byte)RoleId.Lover, crewPlayer, 1); + + players.RemoveAll(x => x.PlayerId == firstLoverId || x.PlayerId == secondLoverId); + modifierCount--; + } + + foreach (RoleId m in allModifiers) { + if (getSelectionForRoleId(m) == 10) ensuredModifiers.AddRange(Enumerable.Repeat(m, getSelectionForRoleId(m, true) / 10)); + else chanceModifiers.AddRange(Enumerable.Repeat(m, getSelectionForRoleId(m, true))); + } + + assignModifiersToPlayers(ensuredModifiers, players, modifierCount); // Assign ensured modifier + + modifierCount -= ensuredModifiers.Count; + if (modifierCount <= 0) return; + int chanceModifierCount = Mathf.Min(modifierCount, chanceModifiers.Count); + List chanceModifierToAssign = new List(); + while (chanceModifierCount > 0 && chanceModifiers.Count > 0) { + var index = rnd.Next(0, chanceModifiers.Count); + RoleId modifierId = chanceModifiers[index]; + chanceModifierToAssign.Add(modifierId); + + int modifierSelection = getSelectionForRoleId(modifierId); + while (modifierSelection > 0) { + chanceModifiers.Remove(modifierId); + modifierSelection--; + } + chanceModifierCount--; + } + + assignModifiersToPlayers(chanceModifierToAssign, players, modifierCount); // Assign chance modifier + } + + private static byte setRoleToRandomPlayer(byte roleId, List playerList, bool removePlayer = true) { var index = rnd.Next(0, playerList.Count); byte playerId = playerList[index].PlayerId; if (removePlayer) playerList.RemoveAt(index); @@ -384,12 +423,91 @@ private static byte setRoleToRandomPlayer(byte roleId, List playe MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetRole, Hazel.SendOption.Reliable, -1); writer.Write(roleId); writer.Write(playerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + RPCProcedure.setRole(roleId, playerId); + return playerId; + } + + private static byte setModifierToRandomPlayer(byte modifierId, List playerList, byte flag = 0) { + var index = rnd.Next(0, playerList.Count); + byte playerId = playerList[index].PlayerId; + playerList.RemoveAt(index); + + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetModifier, Hazel.SendOption.Reliable, -1); + writer.Write(modifierId); + writer.Write(playerId); writer.Write(flag); AmongUsClient.Instance.FinishRpcImmediately(writer); - RPCProcedure.setRole(roleId, playerId, flag); + RPCProcedure.setModifier(modifierId, playerId, flag); return playerId; } + private static void assignModifiersToPlayers(List modifiers, List playerList, int modifierCount) { + modifiers = modifiers.OrderBy(x => rnd.Next()).ToList(); // randomize list + + while (modifierCount < modifiers.Count) { + var index = rnd.Next(0, modifiers.Count); + modifiers.RemoveAt(index); + } + + byte playerId; + if (modifiers.Contains(RoleId.Sunglasses)) { + List crewPlayer = new List(playerList); + crewPlayer.RemoveAll(x => x.Data.Role.IsImpostor || RoleInfo.getRoleInfoForPlayer(x).Any(r => r.isNeutral)); + int sunglassesCount = 0; + while (sunglassesCount < modifiers.FindAll(x => x == RoleId.Sunglasses).Count) { + playerId = setModifierToRandomPlayer((byte)RoleId.Sunglasses, crewPlayer); + crewPlayer.RemoveAll(x => x.PlayerId == playerId); + playerList.RemoveAll(x => x.PlayerId == playerId); + sunglassesCount++; + } + modifiers.RemoveAll(x => x == RoleId.Sunglasses); + } + + foreach (RoleId modifier in modifiers) { + if (playerList.Count == 0) break; + playerId = setModifierToRandomPlayer((byte)modifier, playerList); + playerList.RemoveAll(x => x.PlayerId == playerId); + } + } + + private static int getSelectionForRoleId(RoleId roleId, bool multiplyQuantity = false) { + int selection = 0; + switch (roleId) { + case RoleId.Lover: + selection = CustomOptionHolder.modifierLover.getSelection(); break; + case RoleId.Tiebreaker: + selection = CustomOptionHolder.modifierTieBreaker.getSelection(); break; + case RoleId.Mini: + selection = CustomOptionHolder.modifierMini.getSelection(); break; + case RoleId.Bait: + selection = CustomOptionHolder.modifierBait.getSelection(); + if (multiplyQuantity) selection *= CustomOptionHolder.modifierBaitQuantity.getQuantity(); + break; + case RoleId.Bloody: + selection = CustomOptionHolder.modifierBloody.getSelection(); + if (multiplyQuantity) selection *= CustomOptionHolder.modifierBloodyQuantity.getQuantity(); + break; + case RoleId.AntiTeleport: + selection = CustomOptionHolder.modifierAntiTeleport.getSelection(); + if (multiplyQuantity) selection *= CustomOptionHolder.modifierAntiTeleportQuantity.getQuantity(); + break; + case RoleId.Sunglasses: + selection = CustomOptionHolder.modifierSunglasses.getSelection(); + if (multiplyQuantity) selection *= CustomOptionHolder.modifierSunglassesQuantity.getQuantity(); + break; + case RoleId.Vip: + selection = CustomOptionHolder.modifierVip.getSelection(); + if (multiplyQuantity) selection *= CustomOptionHolder.modifierVipQuantity.getQuantity(); + break; + case RoleId.Invert: + selection = CustomOptionHolder.modifierInvert.getSelection(); + if (multiplyQuantity) selection *= CustomOptionHolder.modifierInvertQuantity.getQuantity(); + break; + } + + return selection; + } private class RoleAssignmentData { diff --git a/TheOtherRoles/Patches/ShipStatusPatch.cs b/TheOtherRoles/Patches/ShipStatusPatch.cs index 896239a45..8c3cae441 100644 --- a/TheOtherRoles/Patches/ShipStatusPatch.cs +++ b/TheOtherRoles/Patches/ShipStatusPatch.cs @@ -17,8 +17,10 @@ public static bool Prefix(ref float __result, ShipStatus __instance, [HarmonyArg float num = (float)switchSystem.Value / 255f; - if (player == null || player.IsDead) // IsDead + if (player == null || player.IsDead) {// IsDead __result = __instance.MaxLightRadius; + return false; + } else if (player.Role.IsImpostor || (Jackal.jackal != null && Jackal.jackal.PlayerId == player.PlayerId && Jackal.hasImpostorVision) || (Sidekick.sidekick != null && Sidekick.sidekick.PlayerId == player.PlayerId && Sidekick.hasImpostorVision) @@ -33,10 +35,13 @@ public static bool Prefix(ref float __result, ShipStatus __instance, [HarmonyArg 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 + else if (Lawyer.lawyer != null && Lawyer.lawyer.PlayerId == player.PlayerId) // Lawyer __result = Mathf.Lerp(__instance.MinLightRadius, __instance.MaxLightRadius * Lawyer.vision, num); else __result = Mathf.Lerp(__instance.MinLightRadius, __instance.MaxLightRadius, num) * PlayerControl.GameOptions.CrewLightMod; + if (Sunglasses.sunglasses.FindAll(x => x.PlayerId == player.PlayerId).Count > 0) // Sunglasses + __result *= 1f - Sunglasses.vision * 0.1f; + return false; } diff --git a/TheOtherRoles/Patches/UpdatePatch.cs b/TheOtherRoles/Patches/UpdatePatch.cs index 211073758..7cf680117 100644 --- a/TheOtherRoles/Patches/UpdatePatch.cs +++ b/TheOtherRoles/Patches/UpdatePatch.cs @@ -74,13 +74,13 @@ static void setNameColors() { if (Deputy.deputy != null && Deputy.knowsSheriff) { setPlayerNameColor(Deputy.deputy, Deputy.color); } - } - else if (Deputy.deputy != null && Deputy.deputy == PlayerControl.LocalPlayer) { + } else if (Deputy.deputy != null && Deputy.deputy == PlayerControl.LocalPlayer) { setPlayerNameColor(Deputy.deputy, Deputy.color); if (Sheriff.sheriff != null && Deputy.knowsSheriff) { setPlayerNameColor(Sheriff.sheriff, Sheriff.color); } - } + } else if (Portalmaker.portalmaker != null && Portalmaker.portalmaker == PlayerControl.LocalPlayer) + setPlayerNameColor(Portalmaker.portalmaker, Portalmaker.color); else if (Lighter.lighter != null && Lighter.lighter == PlayerControl.LocalPlayer) setPlayerNameColor(Lighter.lighter, Lighter.color); else if (Detective.detective != null && Detective.detective == PlayerControl.LocalPlayer) @@ -121,8 +121,6 @@ static void setNameColors() { setPlayerNameColor(Guesser.niceGuesser, Guesser.color); } else if (Guesser.evilGuesser != null && Guesser.evilGuesser == PlayerControl.LocalPlayer) { setPlayerNameColor(Guesser.evilGuesser, Palette.ImpostorRed); - } else if (Bait.bait != null && Bait.bait == PlayerControl.LocalPlayer) { - setPlayerNameColor(Bait.bait, Bait.color); } else if (Vulture.vulture != null && Vulture.vulture == PlayerControl.LocalPlayer) { setPlayerNameColor(Vulture.vulture, Vulture.color); } else if (Medium.medium != null && Medium.medium == PlayerControl.LocalPlayer) { @@ -146,6 +144,12 @@ static void setNameColors() { if (Spy.spy != null && PlayerControl.LocalPlayer.Data.Role.IsImpostor) { setPlayerNameColor(Spy.spy, Spy.color); } + if (Sidekick.sidekick != null && Sidekick.wasTeamRed && PlayerControl.LocalPlayer.Data.Role.IsImpostor) { + setPlayerNameColor(Sidekick.sidekick, Spy.color); + } + if (Jackal.jackal != null && Jackal.wasTeamRed && PlayerControl.LocalPlayer.Data.Role.IsImpostor) { + setPlayerNameColor(Jackal.jackal, Spy.color); + } // Crewmate roles with no changes: Mini // Impostor roles with no changes: Morphling, Camouflager, Vampire, Godfather, Eraser, Janitor, Cleaner, Warlock, BountyHunter, Witch and Mafioso @@ -184,9 +188,7 @@ static void setNameTags() { } // 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 && !Lawyer.lawyer.Data.IsDead)) { + if (Lawyer.lawyer != null && Lawyer.target != null && Lawyer.lawyer == PlayerControl.LocalPlayer) { string suffix = Helpers.cs(Lawyer.color, " §"); Lawyer.target.nameText.text += suffix; @@ -223,7 +225,7 @@ static void timerUpdate() { } public static void miniUpdate() { - if (Mini.mini == null || Camouflager.camouflageTimer > 0f) return; + if (Mini.mini == null || Camouflager.camouflageTimer > 0f || Mini.mini == Morphling.morphling && Morphling.morphTimer > 0f) return; float growingProgress = Mini.growingProgress(); float scale = growingProgress * 0.35f + 0.35f; diff --git a/TheOtherRoles/Patches/UsablesPatch.cs b/TheOtherRoles/Patches/UsablesPatch.cs index 2447b460f..db1e725da 100644 --- a/TheOtherRoles/Patches/UsablesPatch.cs +++ b/TheOtherRoles/Patches/UsablesPatch.cs @@ -147,6 +147,8 @@ public static bool Prefix(KillButton __instance) { 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; + else if (PlayerControl.LocalPlayer == Ninja.ninja) + Ninja.ninja.killTimer = HudManagerStartPatch.ninjaButton.Timer = HudManagerStartPatch.ninjaButton.MaxTimer; } __instance.SetTarget(null); } diff --git a/TheOtherRoles/RPC.cs b/TheOtherRoles/RPC.cs index 71e8143d6..938c7ca7f 100644 --- a/TheOtherRoles/RPC.cs +++ b/TheOtherRoles/RPC.cs @@ -16,6 +16,7 @@ namespace TheOtherRoles enum RoleId { Jester, Mayor, + Portalmaker, Engineer, Sheriff, Deputy, @@ -28,12 +29,10 @@ enum RoleId { Medic, Shifter, Swapper, - Lover, Seer, Morphling, Camouflager, Hacker, - Mini, Tracker, Vampire, Snitch, @@ -49,14 +48,24 @@ enum RoleId { EvilGuesser, NiceGuesser, BountyHunter, - Bait, Vulture, Medium, Lawyer, Pursuer, Witch, + Ninja, Crewmate, - Impostor + Impostor, + // Modifier --- + Lover, + Bait, + Bloody, + AntiTeleport, + Tiebreaker, + Sunglasses, + Mini, + Vip, + Invert } enum CustomRPC @@ -67,6 +76,7 @@ enum CustomRPC ShareOptions, ForceEnd, SetRole, + SetModifier, VersionHandshake, UseUncheckedVent, UncheckedMurderPlayer, @@ -99,6 +109,9 @@ enum CustomRPC SetFutureShifted, SetFutureShielded, SetFutureSpelled, + PlaceNinjaTrace, + PlacePortal, + UsePortal, PlaceJackInTheBox, LightsOut, PlaceCamera, @@ -110,6 +123,10 @@ enum CustomRPC LawyerSetTarget, LawyerPromotesToPursuer, SetBlanked, + Bloody, + SetFirstKill, + Invert, + SetTiebreak } public static class RPCProcedure { @@ -119,6 +136,9 @@ public static class RPCProcedure { public static void resetVariables() { Garlic.clearGarlics(); JackInTheBox.clearJackInTheBoxes(); + NinjaTrace.clearTraces(); + Portal.clearPortals(); + Bloodytrail.resetSprites(); clearAndReloadMapOptions(); clearAndReloadRoles(); clearGameHistory(); @@ -150,7 +170,7 @@ public static void forceEnd() { } } - public static void setRole(byte roleId, byte playerId, byte flag) { + public static void setRole(byte roleId, byte playerId) { foreach (PlayerControl player in PlayerControl.AllPlayerControls) if (player.PlayerId == playerId) { switch((RoleId)roleId) { @@ -160,7 +180,10 @@ public static void setRole(byte roleId, byte playerId, byte flag) { case RoleId.Mayor: Mayor.mayor = player; break; - case RoleId.Engineer: + case RoleId.Portalmaker: + Portalmaker.portalmaker = player; + break; + case RoleId.Engineer: Engineer.engineer = player; break; case RoleId.Sheriff: @@ -196,10 +219,6 @@ public static void setRole(byte roleId, byte playerId, byte flag) { case RoleId.Swapper: Swapper.swapper = player; break; - case RoleId.Lover: - if (flag == 0) Lovers.lover1 = player; - else Lovers.lover2 = player; - break; case RoleId.Seer: Seer.seer = player; break; @@ -212,9 +231,6 @@ public static void setRole(byte roleId, byte playerId, byte flag) { case RoleId.Hacker: Hacker.hacker = player; break; - case RoleId.Mini: - Mini.mini = player; - break; case RoleId.Tracker: Tracker.tracker = player; break; @@ -260,9 +276,6 @@ public static void setRole(byte roleId, byte playerId, byte flag) { case RoleId.BountyHunter: BountyHunter.bountyHunter = player; break; - case RoleId.Bait: - Bait.bait = player; - break; case RoleId.Vulture: Vulture.vulture = player; break; @@ -278,10 +291,47 @@ public static void setRole(byte roleId, byte playerId, byte flag) { case RoleId.Witch: Witch.witch = player; break; + case RoleId.Ninja: + Ninja.ninja = player; + break; } } } + public static void setModifier(byte modifierId, byte playerId, byte flag) { + PlayerControl player = Helpers.playerById(playerId); + switch ((RoleId)modifierId) { + case RoleId.Bait: + Bait.bait.Add(player); + break; + case RoleId.Lover: + if (flag == 0) Lovers.lover1 = player; + else Lovers.lover2 = player; + break; + case RoleId.Bloody: + Bloody.bloody.Add(player); + break; + case RoleId.AntiTeleport: + AntiTeleport.antiTeleport.Add(player); + break; + case RoleId.Tiebreaker: + Tiebreaker.tiebreaker = player; + break; + case RoleId.Sunglasses: + Sunglasses.sunglasses.Add(player); + break; + case RoleId.Mini: + Mini.mini = player; + break; + case RoleId.Vip: + Vip.vip.Add(player); + break; + case RoleId.Invert: + Invert.invert.Add(player); + break; + } + } + public static void versionHandshake(int major, int minor, int build, int revision, Guid guid, int clientId) { System.Version ver; if (revision < 0) @@ -390,6 +440,7 @@ public static void shieldedMurderAttempt() { if (Medic.shielded == null || Medic.medic == null) return; bool isShieldedAndShow = Medic.shielded == PlayerControl.LocalPlayer && Medic.showAttemptToShielded; + isShieldedAndShow = isShieldedAndShow && (Medic.meetingAfterShielding || !Medic.showShieldAfterMeeting); // Dont show attempt, if shield is not shown yet bool isMedicAndShow = Medic.medic == PlayerControl.LocalPlayer && Medic.showAttemptToMedic; if (isShieldedAndShow || isMedicAndShow) Helpers.showFlash(Palette.ImpostorRed, duration: 0.5f); @@ -422,11 +473,15 @@ public static void shifterShift(byte targetId) { if (Lovers.lover2 != null && oldShifter == Lovers.lover2) Lovers.lover2 = player; else if (Lovers.lover2 != null && player == Lovers.lover2) Lovers.lover2 = oldShifter; + + // TODO other Modifiers? } // Shift role if (Mayor.mayor != null && Mayor.mayor == player) Mayor.mayor = oldShifter; + if (Portalmaker.portalmaker != null && Portalmaker.portalmaker == player) + Portalmaker.portalmaker = oldShifter; if (Engineer.engineer != null && Engineer.engineer == player) Engineer.engineer = oldShifter; if (Sheriff.sheriff != null && Sheriff.sheriff == player) { @@ -449,8 +504,6 @@ public static void shifterShift(byte targetId) { Seer.seer = oldShifter; if (Hacker.hacker != null && Hacker.hacker == player) Hacker.hacker = oldShifter; - if (Mini.mini != null && Mini.mini == player) - Mini.mini = oldShifter; if (Tracker.tracker != null && Tracker.tracker == player) Tracker.tracker = oldShifter; if (Snitch.snitch != null && Snitch.snitch == player) @@ -461,10 +514,6 @@ public static void shifterShift(byte targetId) { SecurityGuard.securityGuard = oldShifter; if (Guesser.niceGuesser != null && Guesser.niceGuesser == player) Guesser.niceGuesser = oldShifter; - if (Bait.bait != null && Bait.bait == player) { - Bait.bait = oldShifter; - if (Bait.bait.Data.IsDead) Bait.reported = true; - } if (Medium.medium != null && Medium.medium == player) Medium.medium = oldShifter; @@ -550,10 +599,15 @@ public static void jackalCreatesSidekick(byte targetId) { if (!Jackal.canCreateSidekickFromImpostor && player.Data.Role.IsImpostor) { Jackal.fakeSidekick = player; } else { + bool wasSpy = Spy.spy != null && player == Spy.spy; + bool wasImpostor = player.Data.Role.IsImpostor; // This can only be reached if impostors can be sidekicked. DestroyableSingleton.Instance.SetRole(player, RoleTypes.Crewmate); erasePlayerRoles(player.PlayerId, true); Sidekick.sidekick = player; - if (player.PlayerId == PlayerControl.LocalPlayer.PlayerId) PlayerControl.LocalPlayer.moveable = true; + if (player.PlayerId == PlayerControl.LocalPlayer.PlayerId) PlayerControl.LocalPlayer.moveable = true; + if (wasSpy || wasImpostor) Sidekick.wasTeamRed = true; + Sidekick.wasSpy = wasSpy; + Sidekick.wasImpostor = wasImpostor; } Jackal.canCreateSidekick = false; } @@ -562,16 +616,20 @@ public static void sidekickPromotes() { Jackal.removeCurrentJackal(); Jackal.jackal = Sidekick.sidekick; Jackal.canCreateSidekick = Jackal.jackalPromotedFromSidekickCanCreateSidekick; + Jackal.wasTeamRed = Sidekick.wasTeamRed; + Jackal.wasSpy = Sidekick.wasSpy; + Jackal.wasImpostor = Sidekick.wasImpostor; Sidekick.clearAndReload(); return; } - public static void erasePlayerRoles(byte playerId, bool ignoreLovers = false) { + public static void erasePlayerRoles(byte playerId, bool ignoreModifier = false) { PlayerControl player = Helpers.playerById(playerId); if (player == null) return; // Crewmate roles if (player == Mayor.mayor) Mayor.clearAndReload(); + if (player == Portalmaker.portalmaker) Portalmaker.clearAndReload(); if (player == Engineer.engineer) Engineer.clearAndReload(); if (player == Sheriff.sheriff) Sheriff.clearAndReload(); if (player == Deputy.deputy) Deputy.clearAndReload(); @@ -582,13 +640,11 @@ public static void erasePlayerRoles(byte playerId, bool ignoreLovers = false) { if (player == Shifter.shifter) Shifter.clearAndReload(); if (player == Seer.seer) Seer.clearAndReload(); if (player == Hacker.hacker) Hacker.clearAndReload(); - if (player == Mini.mini) Mini.clearAndReload(); if (player == Tracker.tracker) Tracker.clearAndReload(); if (player == Snitch.snitch) Snitch.clearAndReload(); if (player == Swapper.swapper) Swapper.clearAndReload(); if (player == Spy.spy) Spy.clearAndReload(); if (player == SecurityGuard.securityGuard) SecurityGuard.clearAndReload(); - if (player == Bait.bait) Bait.clearAndReload(); if (player == Medium.medium) Medium.clearAndReload(); // Impostor roles @@ -603,14 +659,12 @@ public static void erasePlayerRoles(byte playerId, bool ignoreLovers = false) { if (player == Cleaner.cleaner) Cleaner.clearAndReload(); if (player == Warlock.warlock) Warlock.clearAndReload(); if (player == Witch.witch) Witch.clearAndReload(); + if (player == Ninja.ninja) Ninja.clearAndReload(); // Other roles if (player == Jester.jester) Jester.clearAndReload(); if (player == Arsonist.arsonist) Arsonist.clearAndReload(); if (Guesser.isGuesser(player.PlayerId)) Guesser.clear(player.PlayerId); - if (!ignoreLovers && (player == Lovers.lover1 || player == Lovers.lover2)) { // The whole Lover couple is being erased - Lovers.clearAndReload(); - } if (player == Jackal.jackal) { // Promote Sidekick and hence override the the Jackal or erase Jackal if (Sidekick.promotesToJackal && Sidekick.sidekick != null && !Sidekick.sidekick.Data.IsDead) { RPCProcedure.sidekickPromotes(); @@ -623,6 +677,20 @@ public static void erasePlayerRoles(byte playerId, bool ignoreLovers = false) { if (player == Vulture.vulture) Vulture.clearAndReload(); if (player == Lawyer.lawyer) Lawyer.clearAndReload(); if (player == Pursuer.pursuer) Pursuer.clearAndReload(); + + // Modifier + if (!ignoreModifier) + { + if (player == Lovers.lover1 || player == Lovers.lover2) Lovers.clearAndReload(); // The whole Lover couple is being erased + if (Bait.bait.Any(x => x.PlayerId == player.PlayerId)) Bait.bait.RemoveAll(x => x.PlayerId == player.PlayerId); + if (Bloody.bloody.Any(x => x.PlayerId == player.PlayerId)) Bloody.bloody.RemoveAll(x => x.PlayerId == player.PlayerId); + if (AntiTeleport.antiTeleport.Any(x => x.PlayerId == player.PlayerId)) AntiTeleport.antiTeleport.RemoveAll(x => x.PlayerId == player.PlayerId); + if (Sunglasses.sunglasses.Any(x => x.PlayerId == player.PlayerId)) Sunglasses.sunglasses.RemoveAll(x => x.PlayerId == player.PlayerId); + if (player == Tiebreaker.tiebreaker) Tiebreaker.clearAndReload(); + if (player == Mini.mini) Mini.clearAndReload(); + if (Vip.vip.Any(x => x.PlayerId == player.PlayerId)) Vip.vip.RemoveAll(x => x.PlayerId == player.PlayerId); + if (Invert.invert.Any(x => x.PlayerId == player.PlayerId)) Invert.invert.RemoveAll(x => x.PlayerId == player.PlayerId); + } } public static void setFutureErased(byte playerId) { @@ -652,6 +720,23 @@ public static void setFutureSpelled(byte playerId) { } } + public static void placeNinjaTrace(byte[] buff) { + Vector3 position = Vector3.zero; + position.x = BitConverter.ToSingle(buff, 0 * sizeof(float)); + position.y = BitConverter.ToSingle(buff, 1 * sizeof(float)); + new NinjaTrace(position, Ninja.traceTime); + } + + public static void placePortal(byte[] buff) { + Vector3 position = Vector2.zero; + position.x = BitConverter.ToSingle(buff, 0 * sizeof(float)); + position.y = BitConverter.ToSingle(buff, 1 * sizeof(float)); + new Portal(position); + } + + public static void usePortal(byte playerId) { + Portal.startTeleport(playerId); + } public static void placeJackInTheBox(byte[] buff) { Vector3 position = Vector3.zero; @@ -794,6 +879,23 @@ public static void setBlanked(byte playerId, byte value) { Pursuer.blankedList.RemoveAll(x => x.PlayerId == playerId); if (value > 0) Pursuer.blankedList.Add(target); } + + public static void bloody(byte killerPlayerId, byte bloodyPlayerId) { + if (Bloody.active.ContainsKey(killerPlayerId)) return; + Bloody.active.Add(killerPlayerId, Bloody.duration); + Bloody.bloodyKillerMap.Add(killerPlayerId, bloodyPlayerId); + } + + public static void setFirstKill(byte playerId) { + PlayerControl target = Helpers.playerById(playerId); + if (target == null) return; + MapOptions.firstKillPlayer = target; + } + + public static void setTiebreak() + { + Tiebreaker.isTiebreak = true; + } } [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] @@ -814,12 +916,17 @@ static void Postfix([HarmonyArgument(0)]byte callId, [HarmonyArgument(1)]Message break; case (byte)CustomRPC.ForceEnd: RPCProcedure.forceEnd(); - break; + break; case (byte)CustomRPC.SetRole: byte roleId = reader.ReadByte(); byte playerId = reader.ReadByte(); + RPCProcedure.setRole(roleId, playerId); + break; + case (byte)CustomRPC.SetModifier: + byte modifierId = reader.ReadByte(); + byte pId = reader.ReadByte(); byte flag = reader.ReadByte(); - RPCProcedure.setRole(roleId, playerId, flag); + RPCProcedure.setModifier(modifierId, pId, flag); break; case (byte)CustomRPC.VersionHandshake: byte major = reader.ReadByte(); @@ -936,6 +1043,15 @@ static void Postfix([HarmonyArgument(0)]byte callId, [HarmonyArgument(1)]Message case (byte)CustomRPC.SetFutureShielded: RPCProcedure.setFutureShielded(reader.ReadByte()); break; + case (byte)CustomRPC.PlaceNinjaTrace: + RPCProcedure.placeNinjaTrace(reader.ReadBytesAndSize()); + break; + case (byte)CustomRPC.PlacePortal: + RPCProcedure.placePortal(reader.ReadBytesAndSize()); + break; + case (byte)CustomRPC.UsePortal: + RPCProcedure.usePortal(reader.ReadByte()); + break; case (byte)CustomRPC.PlaceJackInTheBox: RPCProcedure.placeJackInTheBox(reader.ReadBytesAndSize()); break; @@ -978,6 +1094,18 @@ static void Postfix([HarmonyArgument(0)]byte callId, [HarmonyArgument(1)]Message case (byte)CustomRPC.SetFutureSpelled: RPCProcedure.setFutureSpelled(reader.ReadByte()); break; + case (byte)CustomRPC.Bloody: + byte bloodyKiller = reader.ReadByte(); + byte bloodyDead = reader.ReadByte(); + RPCProcedure.bloody(bloodyKiller, bloodyDead); + break; + case (byte)CustomRPC.SetFirstKill: + byte firstKill = reader.ReadByte(); + RPCProcedure.setFirstKill(firstKill); + break; + case (byte)CustomRPC.SetTiebreak: + RPCProcedure.setTiebreak(); + break; } } } diff --git a/TheOtherRoles/Resources/Blood1.png b/TheOtherRoles/Resources/Blood1.png new file mode 100644 index 000000000..ba81a86b6 Binary files /dev/null and b/TheOtherRoles/Resources/Blood1.png differ diff --git a/TheOtherRoles/Resources/Blood2.png b/TheOtherRoles/Resources/Blood2.png new file mode 100644 index 000000000..5f32a41df Binary files /dev/null and b/TheOtherRoles/Resources/Blood2.png differ diff --git a/TheOtherRoles/Resources/Blood3.png b/TheOtherRoles/Resources/Blood3.png new file mode 100644 index 000000000..5b0013b99 Binary files /dev/null and b/TheOtherRoles/Resources/Blood3.png differ diff --git a/TheOtherRoles/Resources/NinjaAssassinateButton.png b/TheOtherRoles/Resources/NinjaAssassinateButton.png new file mode 100644 index 000000000..674ad477f Binary files /dev/null and b/TheOtherRoles/Resources/NinjaAssassinateButton.png differ diff --git a/TheOtherRoles/Resources/NinjaMarkButton.png b/TheOtherRoles/Resources/NinjaMarkButton.png new file mode 100644 index 000000000..7e7baa65c Binary files /dev/null and b/TheOtherRoles/Resources/NinjaMarkButton.png differ diff --git a/TheOtherRoles/Resources/NinjaTraceW.png b/TheOtherRoles/Resources/NinjaTraceW.png new file mode 100644 index 000000000..cdb293765 Binary files /dev/null and b/TheOtherRoles/Resources/NinjaTraceW.png differ diff --git a/TheOtherRoles/Resources/PlacePortalButton.png b/TheOtherRoles/Resources/PlacePortalButton.png new file mode 100644 index 000000000..97e717d06 Binary files /dev/null and b/TheOtherRoles/Resources/PlacePortalButton.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/plattform.png b/TheOtherRoles/Resources/PortalAnimation/plattform.png new file mode 100644 index 000000000..78e9462b1 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/plattform.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_000.png b/TheOtherRoles/Resources/PortalAnimation/portal_000.png new file mode 100644 index 000000000..8cf6656fb Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_000.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_001.png b/TheOtherRoles/Resources/PortalAnimation/portal_001.png new file mode 100644 index 000000000..0efb94db3 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_001.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_002.png b/TheOtherRoles/Resources/PortalAnimation/portal_002.png new file mode 100644 index 000000000..37463fa42 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_002.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_003.png b/TheOtherRoles/Resources/PortalAnimation/portal_003.png new file mode 100644 index 000000000..c3b3de563 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_003.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_004.png b/TheOtherRoles/Resources/PortalAnimation/portal_004.png new file mode 100644 index 000000000..8fb9f0b2a Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_004.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_005.png b/TheOtherRoles/Resources/PortalAnimation/portal_005.png new file mode 100644 index 000000000..986aa934e Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_005.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_006.png b/TheOtherRoles/Resources/PortalAnimation/portal_006.png new file mode 100644 index 000000000..d24a511cf Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_006.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_007.png b/TheOtherRoles/Resources/PortalAnimation/portal_007.png new file mode 100644 index 000000000..d7cd939b7 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_007.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_008.png b/TheOtherRoles/Resources/PortalAnimation/portal_008.png new file mode 100644 index 000000000..74d9a0e5e Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_008.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_009.png b/TheOtherRoles/Resources/PortalAnimation/portal_009.png new file mode 100644 index 000000000..ee8003ba3 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_009.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_010.png b/TheOtherRoles/Resources/PortalAnimation/portal_010.png new file mode 100644 index 000000000..899e2af34 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_010.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_011.png b/TheOtherRoles/Resources/PortalAnimation/portal_011.png new file mode 100644 index 000000000..197675056 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_011.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_012.png b/TheOtherRoles/Resources/PortalAnimation/portal_012.png new file mode 100644 index 000000000..945ab0cb2 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_012.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_013.png b/TheOtherRoles/Resources/PortalAnimation/portal_013.png new file mode 100644 index 000000000..fc730cce7 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_013.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_014.png b/TheOtherRoles/Resources/PortalAnimation/portal_014.png new file mode 100644 index 000000000..54579b01a Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_014.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_015.png b/TheOtherRoles/Resources/PortalAnimation/portal_015.png new file mode 100644 index 000000000..6b5e48221 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_015.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_016.png b/TheOtherRoles/Resources/PortalAnimation/portal_016.png new file mode 100644 index 000000000..8f8f1343d Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_016.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_017.png b/TheOtherRoles/Resources/PortalAnimation/portal_017.png new file mode 100644 index 000000000..4fdd037dc Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_017.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_018.png b/TheOtherRoles/Resources/PortalAnimation/portal_018.png new file mode 100644 index 000000000..20463fd8f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_018.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_019.png b/TheOtherRoles/Resources/PortalAnimation/portal_019.png new file mode 100644 index 000000000..fa7030aa7 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_019.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_020.png b/TheOtherRoles/Resources/PortalAnimation/portal_020.png new file mode 100644 index 000000000..63a4935ec Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_020.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_021.png b/TheOtherRoles/Resources/PortalAnimation/portal_021.png new file mode 100644 index 000000000..11cc00447 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_021.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_022.png b/TheOtherRoles/Resources/PortalAnimation/portal_022.png new file mode 100644 index 000000000..635e2816c Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_022.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_023.png b/TheOtherRoles/Resources/PortalAnimation/portal_023.png new file mode 100644 index 000000000..a27c3197f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_023.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_024.png b/TheOtherRoles/Resources/PortalAnimation/portal_024.png new file mode 100644 index 000000000..39898140f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_024.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_025.png b/TheOtherRoles/Resources/PortalAnimation/portal_025.png new file mode 100644 index 000000000..b31078183 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_025.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_026.png b/TheOtherRoles/Resources/PortalAnimation/portal_026.png new file mode 100644 index 000000000..345b56e6a Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_026.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_027.png b/TheOtherRoles/Resources/PortalAnimation/portal_027.png new file mode 100644 index 000000000..4addf77a0 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_027.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_028.png b/TheOtherRoles/Resources/PortalAnimation/portal_028.png new file mode 100644 index 000000000..58861cd7e Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_028.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_029.png b/TheOtherRoles/Resources/PortalAnimation/portal_029.png new file mode 100644 index 000000000..ef64dbdb4 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_029.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_030.png b/TheOtherRoles/Resources/PortalAnimation/portal_030.png new file mode 100644 index 000000000..e8dc199ea Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_030.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_031.png b/TheOtherRoles/Resources/PortalAnimation/portal_031.png new file mode 100644 index 000000000..416f15d1e Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_031.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_032.png b/TheOtherRoles/Resources/PortalAnimation/portal_032.png new file mode 100644 index 000000000..dcc1ef766 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_032.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_033.png b/TheOtherRoles/Resources/PortalAnimation/portal_033.png new file mode 100644 index 000000000..8e52ddf27 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_033.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_034.png b/TheOtherRoles/Resources/PortalAnimation/portal_034.png new file mode 100644 index 000000000..9bfaecde7 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_034.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_035.png b/TheOtherRoles/Resources/PortalAnimation/portal_035.png new file mode 100644 index 000000000..67c984565 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_035.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_036.png b/TheOtherRoles/Resources/PortalAnimation/portal_036.png new file mode 100644 index 000000000..f016c7de0 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_036.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_037.png b/TheOtherRoles/Resources/PortalAnimation/portal_037.png new file mode 100644 index 000000000..1ff246785 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_037.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_038.png b/TheOtherRoles/Resources/PortalAnimation/portal_038.png new file mode 100644 index 000000000..778b93a27 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_038.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_039.png b/TheOtherRoles/Resources/PortalAnimation/portal_039.png new file mode 100644 index 000000000..647291f46 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_039.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_040.png b/TheOtherRoles/Resources/PortalAnimation/portal_040.png new file mode 100644 index 000000000..840ac15c1 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_040.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_041.png b/TheOtherRoles/Resources/PortalAnimation/portal_041.png new file mode 100644 index 000000000..9d12df10d Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_041.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_042.png b/TheOtherRoles/Resources/PortalAnimation/portal_042.png new file mode 100644 index 000000000..b8d5d0d72 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_042.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_043.png b/TheOtherRoles/Resources/PortalAnimation/portal_043.png new file mode 100644 index 000000000..3854d84cf Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_043.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_044.png b/TheOtherRoles/Resources/PortalAnimation/portal_044.png new file mode 100644 index 000000000..caeaf95f3 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_044.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_045.png b/TheOtherRoles/Resources/PortalAnimation/portal_045.png new file mode 100644 index 000000000..25d5ed802 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_045.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_046.png b/TheOtherRoles/Resources/PortalAnimation/portal_046.png new file mode 100644 index 000000000..fea38741b Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_046.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_047.png b/TheOtherRoles/Resources/PortalAnimation/portal_047.png new file mode 100644 index 000000000..8a6ad8e42 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_047.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_048.png b/TheOtherRoles/Resources/PortalAnimation/portal_048.png new file mode 100644 index 000000000..9e47b8bf5 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_048.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_049.png b/TheOtherRoles/Resources/PortalAnimation/portal_049.png new file mode 100644 index 000000000..6f84846ce Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_049.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_050.png b/TheOtherRoles/Resources/PortalAnimation/portal_050.png new file mode 100644 index 000000000..c8fd79392 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_050.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_051.png b/TheOtherRoles/Resources/PortalAnimation/portal_051.png new file mode 100644 index 000000000..d250d2360 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_051.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_052.png b/TheOtherRoles/Resources/PortalAnimation/portal_052.png new file mode 100644 index 000000000..e8554a5e2 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_052.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_053.png b/TheOtherRoles/Resources/PortalAnimation/portal_053.png new file mode 100644 index 000000000..0082d9bc2 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_053.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_054.png b/TheOtherRoles/Resources/PortalAnimation/portal_054.png new file mode 100644 index 000000000..e013a2f90 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_054.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_055.png b/TheOtherRoles/Resources/PortalAnimation/portal_055.png new file mode 100644 index 000000000..585f333c9 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_055.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_056.png b/TheOtherRoles/Resources/PortalAnimation/portal_056.png new file mode 100644 index 000000000..f83fdb3db Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_056.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_057.png b/TheOtherRoles/Resources/PortalAnimation/portal_057.png new file mode 100644 index 000000000..ac4de1b3d Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_057.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_058.png b/TheOtherRoles/Resources/PortalAnimation/portal_058.png new file mode 100644 index 000000000..2cb913df6 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_058.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_059.png b/TheOtherRoles/Resources/PortalAnimation/portal_059.png new file mode 100644 index 000000000..527c212f5 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_059.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_060.png b/TheOtherRoles/Resources/PortalAnimation/portal_060.png new file mode 100644 index 000000000..00ceba28c Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_060.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_061.png b/TheOtherRoles/Resources/PortalAnimation/portal_061.png new file mode 100644 index 000000000..52ee9e898 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_061.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_062.png b/TheOtherRoles/Resources/PortalAnimation/portal_062.png new file mode 100644 index 000000000..6e0f50cdc Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_062.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_063.png b/TheOtherRoles/Resources/PortalAnimation/portal_063.png new file mode 100644 index 000000000..24a022fd3 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_063.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_064.png b/TheOtherRoles/Resources/PortalAnimation/portal_064.png new file mode 100644 index 000000000..cd60605c3 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_064.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_065.png b/TheOtherRoles/Resources/PortalAnimation/portal_065.png new file mode 100644 index 000000000..e4e43acae Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_065.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_066.png b/TheOtherRoles/Resources/PortalAnimation/portal_066.png new file mode 100644 index 000000000..396fe4b1a Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_066.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_067.png b/TheOtherRoles/Resources/PortalAnimation/portal_067.png new file mode 100644 index 000000000..80a1b0526 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_067.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_068.png b/TheOtherRoles/Resources/PortalAnimation/portal_068.png new file mode 100644 index 000000000..57f61eb84 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_068.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_069.png b/TheOtherRoles/Resources/PortalAnimation/portal_069.png new file mode 100644 index 000000000..d9bc04972 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_069.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_070.png b/TheOtherRoles/Resources/PortalAnimation/portal_070.png new file mode 100644 index 000000000..ae0db0314 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_070.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_071.png b/TheOtherRoles/Resources/PortalAnimation/portal_071.png new file mode 100644 index 000000000..7fbff2f73 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_071.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_072.png b/TheOtherRoles/Resources/PortalAnimation/portal_072.png new file mode 100644 index 000000000..e9ae84c63 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_072.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_073.png b/TheOtherRoles/Resources/PortalAnimation/portal_073.png new file mode 100644 index 000000000..f51846001 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_073.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_074.png b/TheOtherRoles/Resources/PortalAnimation/portal_074.png new file mode 100644 index 000000000..216c5d5df Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_074.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_075.png b/TheOtherRoles/Resources/PortalAnimation/portal_075.png new file mode 100644 index 000000000..9bd7188e0 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_075.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_076.png b/TheOtherRoles/Resources/PortalAnimation/portal_076.png new file mode 100644 index 000000000..6fb9074d5 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_076.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_077.png b/TheOtherRoles/Resources/PortalAnimation/portal_077.png new file mode 100644 index 000000000..5f47e9ba3 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_077.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_078.png b/TheOtherRoles/Resources/PortalAnimation/portal_078.png new file mode 100644 index 000000000..4a05e35f7 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_078.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_079.png b/TheOtherRoles/Resources/PortalAnimation/portal_079.png new file mode 100644 index 000000000..9641cbb2f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_079.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_080.png b/TheOtherRoles/Resources/PortalAnimation/portal_080.png new file mode 100644 index 000000000..1ddd0c096 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_080.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_081.png b/TheOtherRoles/Resources/PortalAnimation/portal_081.png new file mode 100644 index 000000000..3d438743f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_081.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_082.png b/TheOtherRoles/Resources/PortalAnimation/portal_082.png new file mode 100644 index 000000000..d9235a71d Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_082.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_083.png b/TheOtherRoles/Resources/PortalAnimation/portal_083.png new file mode 100644 index 000000000..252096eee Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_083.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_084.png b/TheOtherRoles/Resources/PortalAnimation/portal_084.png new file mode 100644 index 000000000..90709efaa Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_084.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_085.png b/TheOtherRoles/Resources/PortalAnimation/portal_085.png new file mode 100644 index 000000000..0b6772511 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_085.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_086.png b/TheOtherRoles/Resources/PortalAnimation/portal_086.png new file mode 100644 index 000000000..aa1079187 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_086.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_087.png b/TheOtherRoles/Resources/PortalAnimation/portal_087.png new file mode 100644 index 000000000..16d749abf Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_087.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_088.png b/TheOtherRoles/Resources/PortalAnimation/portal_088.png new file mode 100644 index 000000000..a6cf27e2e Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_088.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_089.png b/TheOtherRoles/Resources/PortalAnimation/portal_089.png new file mode 100644 index 000000000..8c62efb32 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_089.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_090.png b/TheOtherRoles/Resources/PortalAnimation/portal_090.png new file mode 100644 index 000000000..6a43d2b20 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_090.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_091.png b/TheOtherRoles/Resources/PortalAnimation/portal_091.png new file mode 100644 index 000000000..b66670e33 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_091.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_092.png b/TheOtherRoles/Resources/PortalAnimation/portal_092.png new file mode 100644 index 000000000..b66670e33 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_092.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_093.png b/TheOtherRoles/Resources/PortalAnimation/portal_093.png new file mode 100644 index 000000000..56b05b5da Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_093.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_094.png b/TheOtherRoles/Resources/PortalAnimation/portal_094.png new file mode 100644 index 000000000..56b05b5da Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_094.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_095.png b/TheOtherRoles/Resources/PortalAnimation/portal_095.png new file mode 100644 index 000000000..56b05b5da Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_095.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_096.png b/TheOtherRoles/Resources/PortalAnimation/portal_096.png new file mode 100644 index 000000000..56b05b5da Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_096.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_097.png b/TheOtherRoles/Resources/PortalAnimation/portal_097.png new file mode 100644 index 000000000..1cdfcf741 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_097.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_098.png b/TheOtherRoles/Resources/PortalAnimation/portal_098.png new file mode 100644 index 000000000..e6259a927 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_098.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_099.png b/TheOtherRoles/Resources/PortalAnimation/portal_099.png new file mode 100644 index 000000000..ad9fc0f49 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_099.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_100.png b/TheOtherRoles/Resources/PortalAnimation/portal_100.png new file mode 100644 index 000000000..28b905f4f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_100.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_101.png b/TheOtherRoles/Resources/PortalAnimation/portal_101.png new file mode 100644 index 000000000..a187c22c7 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_101.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_102.png b/TheOtherRoles/Resources/PortalAnimation/portal_102.png new file mode 100644 index 000000000..746661323 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_102.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_103.png b/TheOtherRoles/Resources/PortalAnimation/portal_103.png new file mode 100644 index 000000000..d4f147ab0 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_103.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_104.png b/TheOtherRoles/Resources/PortalAnimation/portal_104.png new file mode 100644 index 000000000..791e61ded Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_104.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_105.png b/TheOtherRoles/Resources/PortalAnimation/portal_105.png new file mode 100644 index 000000000..ffa4a4cce Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_105.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_106.png b/TheOtherRoles/Resources/PortalAnimation/portal_106.png new file mode 100644 index 000000000..42a02c9df Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_106.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_107.png b/TheOtherRoles/Resources/PortalAnimation/portal_107.png new file mode 100644 index 000000000..88a145c8d Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_107.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_108.png b/TheOtherRoles/Resources/PortalAnimation/portal_108.png new file mode 100644 index 000000000..c55a87efb Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_108.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_109.png b/TheOtherRoles/Resources/PortalAnimation/portal_109.png new file mode 100644 index 000000000..06e31ae8b Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_109.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_110.png b/TheOtherRoles/Resources/PortalAnimation/portal_110.png new file mode 100644 index 000000000..f69432c78 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_110.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_111.png b/TheOtherRoles/Resources/PortalAnimation/portal_111.png new file mode 100644 index 000000000..65a952d47 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_111.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_112.png b/TheOtherRoles/Resources/PortalAnimation/portal_112.png new file mode 100644 index 000000000..ffee63e16 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_112.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_113.png b/TheOtherRoles/Resources/PortalAnimation/portal_113.png new file mode 100644 index 000000000..4ac67334b Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_113.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_114.png b/TheOtherRoles/Resources/PortalAnimation/portal_114.png new file mode 100644 index 000000000..c7d33e715 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_114.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_115.png b/TheOtherRoles/Resources/PortalAnimation/portal_115.png new file mode 100644 index 000000000..4fcdb2796 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_115.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_116.png b/TheOtherRoles/Resources/PortalAnimation/portal_116.png new file mode 100644 index 000000000..c63e80429 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_116.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_117.png b/TheOtherRoles/Resources/PortalAnimation/portal_117.png new file mode 100644 index 000000000..dad5251db Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_117.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_118.png b/TheOtherRoles/Resources/PortalAnimation/portal_118.png new file mode 100644 index 000000000..aa529ebe8 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_118.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_119.png b/TheOtherRoles/Resources/PortalAnimation/portal_119.png new file mode 100644 index 000000000..01910a027 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_119.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_120.png b/TheOtherRoles/Resources/PortalAnimation/portal_120.png new file mode 100644 index 000000000..5b29a6354 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_120.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_121.png b/TheOtherRoles/Resources/PortalAnimation/portal_121.png new file mode 100644 index 000000000..4fd3a6f6c Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_121.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_122.png b/TheOtherRoles/Resources/PortalAnimation/portal_122.png new file mode 100644 index 000000000..348acdb6f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_122.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_123.png b/TheOtherRoles/Resources/PortalAnimation/portal_123.png new file mode 100644 index 000000000..e4552e64b Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_123.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_124.png b/TheOtherRoles/Resources/PortalAnimation/portal_124.png new file mode 100644 index 000000000..9fbc68b04 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_124.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_125.png b/TheOtherRoles/Resources/PortalAnimation/portal_125.png new file mode 100644 index 000000000..e62047996 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_125.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_126.png b/TheOtherRoles/Resources/PortalAnimation/portal_126.png new file mode 100644 index 000000000..f12f69165 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_126.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_127.png b/TheOtherRoles/Resources/PortalAnimation/portal_127.png new file mode 100644 index 000000000..0a18f09c0 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_127.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_128.png b/TheOtherRoles/Resources/PortalAnimation/portal_128.png new file mode 100644 index 000000000..9911486d6 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_128.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_129.png b/TheOtherRoles/Resources/PortalAnimation/portal_129.png new file mode 100644 index 000000000..96186743d Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_129.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_130.png b/TheOtherRoles/Resources/PortalAnimation/portal_130.png new file mode 100644 index 000000000..dc3affaac Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_130.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_131.png b/TheOtherRoles/Resources/PortalAnimation/portal_131.png new file mode 100644 index 000000000..88949c5fa Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_131.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_132.png b/TheOtherRoles/Resources/PortalAnimation/portal_132.png new file mode 100644 index 000000000..d903e7188 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_132.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_133.png b/TheOtherRoles/Resources/PortalAnimation/portal_133.png new file mode 100644 index 000000000..7f6742486 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_133.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_134.png b/TheOtherRoles/Resources/PortalAnimation/portal_134.png new file mode 100644 index 000000000..f7dc5919b Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_134.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_135.png b/TheOtherRoles/Resources/PortalAnimation/portal_135.png new file mode 100644 index 000000000..bcde204f8 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_135.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_136.png b/TheOtherRoles/Resources/PortalAnimation/portal_136.png new file mode 100644 index 000000000..df667b453 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_136.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_137.png b/TheOtherRoles/Resources/PortalAnimation/portal_137.png new file mode 100644 index 000000000..bd2ebd0cf Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_137.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_138.png b/TheOtherRoles/Resources/PortalAnimation/portal_138.png new file mode 100644 index 000000000..e78d3b82c Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_138.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_139.png b/TheOtherRoles/Resources/PortalAnimation/portal_139.png new file mode 100644 index 000000000..3262a7ba8 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_139.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_140.png b/TheOtherRoles/Resources/PortalAnimation/portal_140.png new file mode 100644 index 000000000..675b4062a Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_140.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_141.png b/TheOtherRoles/Resources/PortalAnimation/portal_141.png new file mode 100644 index 000000000..9ed1c6787 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_141.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_142.png b/TheOtherRoles/Resources/PortalAnimation/portal_142.png new file mode 100644 index 000000000..671435b79 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_142.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_143.png b/TheOtherRoles/Resources/PortalAnimation/portal_143.png new file mode 100644 index 000000000..1757fe9b4 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_143.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_144.png b/TheOtherRoles/Resources/PortalAnimation/portal_144.png new file mode 100644 index 000000000..e23470c33 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_144.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_145.png b/TheOtherRoles/Resources/PortalAnimation/portal_145.png new file mode 100644 index 000000000..85bc5657f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_145.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_146.png b/TheOtherRoles/Resources/PortalAnimation/portal_146.png new file mode 100644 index 000000000..7cc94608f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_146.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_147.png b/TheOtherRoles/Resources/PortalAnimation/portal_147.png new file mode 100644 index 000000000..09fce0fed Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_147.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_148.png b/TheOtherRoles/Resources/PortalAnimation/portal_148.png new file mode 100644 index 000000000..095983d86 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_148.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_149.png b/TheOtherRoles/Resources/PortalAnimation/portal_149.png new file mode 100644 index 000000000..33d225561 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_149.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_150.png b/TheOtherRoles/Resources/PortalAnimation/portal_150.png new file mode 100644 index 000000000..65cee6399 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_150.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_151.png b/TheOtherRoles/Resources/PortalAnimation/portal_151.png new file mode 100644 index 000000000..17c915738 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_151.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_152.png b/TheOtherRoles/Resources/PortalAnimation/portal_152.png new file mode 100644 index 000000000..07194aa3d Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_152.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_153.png b/TheOtherRoles/Resources/PortalAnimation/portal_153.png new file mode 100644 index 000000000..a7bfdcc60 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_153.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_154.png b/TheOtherRoles/Resources/PortalAnimation/portal_154.png new file mode 100644 index 000000000..7eff3177c Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_154.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_155.png b/TheOtherRoles/Resources/PortalAnimation/portal_155.png new file mode 100644 index 000000000..8358cd7c4 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_155.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_156.png b/TheOtherRoles/Resources/PortalAnimation/portal_156.png new file mode 100644 index 000000000..891602319 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_156.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_157.png b/TheOtherRoles/Resources/PortalAnimation/portal_157.png new file mode 100644 index 000000000..c78738c3c Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_157.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_158.png b/TheOtherRoles/Resources/PortalAnimation/portal_158.png new file mode 100644 index 000000000..6cbbe6b7a Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_158.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_159.png b/TheOtherRoles/Resources/PortalAnimation/portal_159.png new file mode 100644 index 000000000..775570f40 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_159.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_160.png b/TheOtherRoles/Resources/PortalAnimation/portal_160.png new file mode 100644 index 000000000..6159def29 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_160.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_161.png b/TheOtherRoles/Resources/PortalAnimation/portal_161.png new file mode 100644 index 000000000..dba0597fc Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_161.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_162.png b/TheOtherRoles/Resources/PortalAnimation/portal_162.png new file mode 100644 index 000000000..66bdf19d7 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_162.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_163.png b/TheOtherRoles/Resources/PortalAnimation/portal_163.png new file mode 100644 index 000000000..661b62be9 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_163.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_164.png b/TheOtherRoles/Resources/PortalAnimation/portal_164.png new file mode 100644 index 000000000..20008192e Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_164.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_165.png b/TheOtherRoles/Resources/PortalAnimation/portal_165.png new file mode 100644 index 000000000..26627b89f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_165.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_166.png b/TheOtherRoles/Resources/PortalAnimation/portal_166.png new file mode 100644 index 000000000..918f118d9 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_166.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_167.png b/TheOtherRoles/Resources/PortalAnimation/portal_167.png new file mode 100644 index 000000000..d99d8ccea Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_167.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_168.png b/TheOtherRoles/Resources/PortalAnimation/portal_168.png new file mode 100644 index 000000000..3e45b0175 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_168.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_169.png b/TheOtherRoles/Resources/PortalAnimation/portal_169.png new file mode 100644 index 000000000..2051a0376 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_169.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_170.png b/TheOtherRoles/Resources/PortalAnimation/portal_170.png new file mode 100644 index 000000000..28ac52b6b Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_170.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_171.png b/TheOtherRoles/Resources/PortalAnimation/portal_171.png new file mode 100644 index 000000000..4172d26c7 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_171.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_172.png b/TheOtherRoles/Resources/PortalAnimation/portal_172.png new file mode 100644 index 000000000..8ce88d46a Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_172.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_173.png b/TheOtherRoles/Resources/PortalAnimation/portal_173.png new file mode 100644 index 000000000..1804cde9f Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_173.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_174.png b/TheOtherRoles/Resources/PortalAnimation/portal_174.png new file mode 100644 index 000000000..979fc2c03 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_174.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_175.png b/TheOtherRoles/Resources/PortalAnimation/portal_175.png new file mode 100644 index 000000000..cc8f13b27 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_175.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_176.png b/TheOtherRoles/Resources/PortalAnimation/portal_176.png new file mode 100644 index 000000000..09fdb0c46 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_176.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_177.png b/TheOtherRoles/Resources/PortalAnimation/portal_177.png new file mode 100644 index 000000000..97bc08419 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_177.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_178.png b/TheOtherRoles/Resources/PortalAnimation/portal_178.png new file mode 100644 index 000000000..2c5325bf5 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_178.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_179.png b/TheOtherRoles/Resources/PortalAnimation/portal_179.png new file mode 100644 index 000000000..c69066e5d Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_179.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_180.png b/TheOtherRoles/Resources/PortalAnimation/portal_180.png new file mode 100644 index 000000000..02d44d9e4 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_180.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_181.png b/TheOtherRoles/Resources/PortalAnimation/portal_181.png new file mode 100644 index 000000000..c56ce86f9 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_181.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_182.png b/TheOtherRoles/Resources/PortalAnimation/portal_182.png new file mode 100644 index 000000000..88bf869a6 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_182.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_183.png b/TheOtherRoles/Resources/PortalAnimation/portal_183.png new file mode 100644 index 000000000..539ac9c78 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_183.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_184.png b/TheOtherRoles/Resources/PortalAnimation/portal_184.png new file mode 100644 index 000000000..46f34fe33 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_184.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_185.png b/TheOtherRoles/Resources/PortalAnimation/portal_185.png new file mode 100644 index 000000000..45586afcc Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_185.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_186.png b/TheOtherRoles/Resources/PortalAnimation/portal_186.png new file mode 100644 index 000000000..456062255 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_186.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_187.png b/TheOtherRoles/Resources/PortalAnimation/portal_187.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_187.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_188.png b/TheOtherRoles/Resources/PortalAnimation/portal_188.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_188.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_189.png b/TheOtherRoles/Resources/PortalAnimation/portal_189.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_189.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_190.png b/TheOtherRoles/Resources/PortalAnimation/portal_190.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_190.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_191.png b/TheOtherRoles/Resources/PortalAnimation/portal_191.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_191.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_192.png b/TheOtherRoles/Resources/PortalAnimation/portal_192.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_192.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_193.png b/TheOtherRoles/Resources/PortalAnimation/portal_193.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_193.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_194.png b/TheOtherRoles/Resources/PortalAnimation/portal_194.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_194.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_195.png b/TheOtherRoles/Resources/PortalAnimation/portal_195.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_195.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_196.png b/TheOtherRoles/Resources/PortalAnimation/portal_196.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_196.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_197.png b/TheOtherRoles/Resources/PortalAnimation/portal_197.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_197.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_198.png b/TheOtherRoles/Resources/PortalAnimation/portal_198.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_198.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_199.png b/TheOtherRoles/Resources/PortalAnimation/portal_199.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_199.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_200.png b/TheOtherRoles/Resources/PortalAnimation/portal_200.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_200.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_201.png b/TheOtherRoles/Resources/PortalAnimation/portal_201.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_201.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_202.png b/TheOtherRoles/Resources/PortalAnimation/portal_202.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_202.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_203.png b/TheOtherRoles/Resources/PortalAnimation/portal_203.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_203.png differ diff --git a/TheOtherRoles/Resources/PortalAnimation/portal_204.png b/TheOtherRoles/Resources/PortalAnimation/portal_204.png new file mode 100644 index 000000000..3a65da134 Binary files /dev/null and b/TheOtherRoles/Resources/PortalAnimation/portal_204.png differ diff --git a/TheOtherRoles/Resources/TabIcon.png b/TheOtherRoles/Resources/TabIcon.png index 1ac410bef..db404fa53 100644 Binary files a/TheOtherRoles/Resources/TabIcon.png and b/TheOtherRoles/Resources/TabIcon.png differ diff --git a/TheOtherRoles/Resources/TabIconCrewmate.png b/TheOtherRoles/Resources/TabIconCrewmate.png new file mode 100644 index 000000000..a459ba7a4 Binary files /dev/null and b/TheOtherRoles/Resources/TabIconCrewmate.png differ diff --git a/TheOtherRoles/Resources/TabIconImpostor.png b/TheOtherRoles/Resources/TabIconImpostor.png new file mode 100644 index 000000000..073c1966e Binary files /dev/null and b/TheOtherRoles/Resources/TabIconImpostor.png differ diff --git a/TheOtherRoles/Resources/TabIconModifier.png b/TheOtherRoles/Resources/TabIconModifier.png new file mode 100644 index 000000000..a564d301b Binary files /dev/null and b/TheOtherRoles/Resources/TabIconModifier.png differ diff --git a/TheOtherRoles/Resources/TabIconNeutral.png b/TheOtherRoles/Resources/TabIconNeutral.png new file mode 100644 index 000000000..f747816c5 Binary files /dev/null and b/TheOtherRoles/Resources/TabIconNeutral.png differ diff --git a/TheOtherRoles/Resources/UsePortalButton.png b/TheOtherRoles/Resources/UsePortalButton.png new file mode 100644 index 000000000..8ec3316b8 Binary files /dev/null and b/TheOtherRoles/Resources/UsePortalButton.png differ diff --git a/TheOtherRoles/RoleInfo.cs b/TheOtherRoles/RoleInfo.cs index b787ccbbf..384f30088 100644 --- a/TheOtherRoles/RoleInfo.cs +++ b/TheOtherRoles/RoleInfo.cs @@ -14,18 +14,21 @@ class RoleInfo { public string shortDescription; public RoleId roleId; public bool isNeutral; + public bool isModifier; - RoleInfo(string name, Color color, string introDescription, string shortDescription, RoleId roleId, bool isNeutral = false) { + RoleInfo(string name, Color color, string introDescription, string shortDescription, RoleId roleId, bool isNeutral = false, bool isModifier = false) { this.color = color; this.name = name; this.introDescription = introDescription; this.shortDescription = shortDescription; this.roleId = roleId; this.isNeutral = isNeutral; + this.isModifier = isModifier; } public static RoleInfo jester = new RoleInfo("Jester", Jester.color, "Get voted out", "Get voted out", RoleId.Jester, true); public static RoleInfo mayor = new RoleInfo("Mayor", Mayor.color, "Your vote counts twice", "Your vote counts twice", RoleId.Mayor); + public static RoleInfo portalmaker = new RoleInfo("Portalmaker", Portalmaker.color, "You can create portals", "You can create portals", RoleId.Portalmaker); public static RoleInfo engineer = new RoleInfo("Engineer", Engineer.color, "Maintain important systems on the ship", "Repair the ship", RoleId.Engineer); public static RoleInfo sheriff = new RoleInfo("Sheriff", Sheriff.color, "Shoot the Impostors", "Shoot the Impostors", RoleId.Sheriff); public static RoleInfo deputy = new RoleInfo("Deputy", Sheriff.color, "Handcuff the Impostors", "Handcuff the Impostors", RoleId.Deputy); @@ -40,7 +43,7 @@ class RoleInfo { public static RoleInfo trickster = new RoleInfo("Trickster", Trickster.color, "Use your jack-in-the-boxes to surprise others", "Surprise your enemies", RoleId.Trickster); public static RoleInfo cleaner = new RoleInfo("Cleaner", Cleaner.color, "Kill everyone and leave no traces", "Clean up dead bodies", RoleId.Cleaner); public static RoleInfo warlock = new RoleInfo("Warlock", Warlock.color, "Curse other players and kill everyone", "Curse and kill everyone", RoleId.Warlock); - public static RoleInfo bountyHunter = new RoleInfo("Bounty Hunter", BountyHunter.color, "Hunt your Bounty down", "Hunt your Bounty down", RoleId.BountyHunter); + public static RoleInfo bountyHunter = new RoleInfo("Bounty Hunter", BountyHunter.color, "Hunt your bounty down", "Hunt your bounty down", RoleId.BountyHunter); public static RoleInfo detective = new RoleInfo("Detective", Detective.color, "Find the Impostors by examining footprints", "Examine footprints", RoleId.Detective); public static RoleInfo timeMaster = new RoleInfo("Time Master", TimeMaster.color, "Save yourself with your time shield", "Use your time shield", RoleId.TimeMaster); public static RoleInfo medic = new RoleInfo("Medic", Medic.color, "Protect someone with your shield", "Protect other players", RoleId.Medic); @@ -48,8 +51,6 @@ class RoleInfo { public static RoleInfo swapper = new RoleInfo("Swapper", Swapper.color, "Swap votes to exile the Impostors", "Swap votes", RoleId.Swapper); public static RoleInfo seer = new RoleInfo("Seer", Seer.color, "You will see players die", "You will see players die", RoleId.Seer); public static RoleInfo hacker = new RoleInfo("Hacker", Hacker.color, "Hack systems to find the Impostors", "Hack to find the Impostors", RoleId.Hacker); - public static RoleInfo niceMini = new RoleInfo("Nice Mini", Mini.color, "No one will harm you until you grow up", "No one will harm you", RoleId.Mini); - public static RoleInfo evilMini = new RoleInfo("Evil Mini", Palette.ImpostorRed, "No one will harm you until you grow up", "No one will harm you", RoleId.Mini); public static RoleInfo tracker = new RoleInfo("Tracker", Tracker.color, "Track the Impostors down", "Track the Impostors down", RoleId.Tracker); public static RoleInfo snitch = new RoleInfo("Snitch", Snitch.color, "Finish your tasks to find the Impostors", "Finish your tasks", RoleId.Snitch); public static RoleInfo jackal = new RoleInfo("Jackal", Jackal.color, "Kill all Crewmates and Impostors to win", "Kill everyone", RoleId.Jackal, true); @@ -59,15 +60,28 @@ class RoleInfo { public static RoleInfo arsonist = new RoleInfo("Arsonist", Arsonist.color, "Let them burn", "Let them burn", RoleId.Arsonist, true); public static RoleInfo goodGuesser = new RoleInfo("Nice Guesser", Guesser.color, "Guess and shoot", "Guess and shoot", RoleId.NiceGuesser); public static RoleInfo badGuesser = new RoleInfo("Evil Guesser", Palette.ImpostorRed, "Guess and shoot", "Guess and shoot", RoleId.EvilGuesser); - 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 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 RoleInfo ninja = new RoleInfo("Ninja", Ninja.color, "Surprise and assassinate your foes", "Surprise and assassinate your foes", RoleId.Ninja); + + + + // Modifier + public static RoleInfo bloody = new RoleInfo("Bloody", Color.yellow, "Your killer leaves a bloody trail", "Your killer leaves a bloody trail", RoleId.Bloody, false, true); + public static RoleInfo antiTeleport = new RoleInfo("Anti tp", Color.yellow, "You will not get teleported", "You will not get teleported", RoleId.AntiTeleport, false, true); + public static RoleInfo tiebreaker = new RoleInfo("Tiebreaker", Color.yellow, "Your vote break the tie", "Break the tie", RoleId.Tiebreaker, false, true); + public static RoleInfo bait = new RoleInfo("Bait", Color.yellow, "Bait your enemies", "Bait your enemies", RoleId.Bait, false, true); + public static RoleInfo sunglasses = new RoleInfo("Sunglasses", Color.yellow, "You got the sunglasses", "Your vision is reduced", RoleId.Sunglasses, false, true); + public static RoleInfo lover = new RoleInfo("Lover", Lovers.color, $"You are in love", $"You are in love", RoleId.Lover, false, true); + public static RoleInfo mini = new RoleInfo("Mini", Color.yellow, "No one will harm you until you grow up", "No one will harm you", RoleId.Mini, false, true); + public static RoleInfo vip = new RoleInfo("VIP", Color.yellow, "You are the VIP", "Everyone is notified when you die", RoleId.Vip, false, true); + public static RoleInfo invert = new RoleInfo("Invert", Color.yellow, "Your movement is inverted", "Your movement is inverted", RoleId.Invert, false, true); + public static List allRoleInfos = new List() { impostor, @@ -83,8 +97,7 @@ class RoleInfo { warlock, bountyHunter, witch, - niceMini, - evilMini, + ninja, goodGuesser, badGuesser, lover, @@ -98,6 +111,7 @@ class RoleInfo { crewmate, shifter, mayor, + portalmaker, engineer, sheriff, deputy, @@ -113,16 +127,37 @@ class RoleInfo { spy, securityGuard, bait, - medium + medium, + bloody, + antiTeleport, + tiebreaker, + sunglasses, + mini, + vip, + invert }; - public static List getRoleInfoForPlayer(PlayerControl p) { + public static List getRoleInfoForPlayer(PlayerControl p, bool showModifier = true) { List infos = new List(); if (p == null) return infos; + // Modifier + if (showModifier) { + if (p == Lovers.lover1 || p == Lovers.lover2) infos.Add(lover); + if (p == Tiebreaker.tiebreaker) infos.Add(tiebreaker); + if (Bait.bait.Any(x => x.PlayerId == p.PlayerId)) infos.Add(bait); + if (Bloody.bloody.Any(x => x.PlayerId == p.PlayerId)) infos.Add(bloody); + if (AntiTeleport.antiTeleport.Any(x => x.PlayerId == p.PlayerId)) infos.Add(antiTeleport); + if (Sunglasses.sunglasses.Any(x => x.PlayerId == p.PlayerId)) infos.Add(sunglasses); + if (p == Mini.mini) infos.Add(mini); + if (Vip.vip.Any(x => x.PlayerId == p.PlayerId)) infos.Add(vip); + if (Invert.invert.Any(x => x.PlayerId == p.PlayerId)) infos.Add(invert); + } + // Special roles if (p == Jester.jester) infos.Add(jester); if (p == Mayor.mayor) infos.Add(mayor); + if (p == Portalmaker.portalmaker) infos.Add(portalmaker); if (p == Engineer.engineer) infos.Add(engineer); if (p == Sheriff.sheriff || p == Sheriff.formerSheriff) infos.Add(sheriff); if (p == Deputy.deputy) infos.Add(deputy); @@ -138,6 +173,7 @@ public static List getRoleInfoForPlayer(PlayerControl p) { if (p == Cleaner.cleaner) infos.Add(cleaner); if (p == Warlock.warlock) infos.Add(warlock); if (p == Witch.witch) infos.Add(witch); + if (p == Ninja.ninja) infos.Add(ninja); if (p == Detective.detective) infos.Add(detective); if (p == TimeMaster.timeMaster) infos.Add(timeMaster); if (p == Medic.medic) infos.Add(medic); @@ -145,7 +181,6 @@ public static List getRoleInfoForPlayer(PlayerControl p) { if (p == Swapper.swapper) infos.Add(swapper); if (p == Seer.seer) infos.Add(seer); if (p == Hacker.hacker) infos.Add(hacker); - if (p == Mini.mini) infos.Add(p.Data.Role.IsImpostor ? evilMini : niceMini); if (p == Tracker.tracker) infos.Add(tracker); if (p == Snitch.snitch) infos.Add(snitch); if (p == Jackal.jackal || (Jackal.formerJackals != null && Jackal.formerJackals.Any(x => x.PlayerId == p.PlayerId))) infos.Add(jackal); @@ -156,7 +191,6 @@ public static List getRoleInfoForPlayer(PlayerControl p) { if (p == Guesser.niceGuesser) infos.Add(goodGuesser); if (p == Guesser.evilGuesser) infos.Add(badGuesser); if (p == BountyHunter.bountyHunter) infos.Add(bountyHunter); - 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); @@ -166,15 +200,12 @@ public static List getRoleInfoForPlayer(PlayerControl p) { if (infos.Count == 0 && p.Data.Role.IsImpostor) infos.Add(impostor); // Just Impostor if (infos.Count == 0 && !p.Data.Role.IsImpostor) infos.Add(crewmate); // Just Crewmate - // Modifier - if (p == Lovers.lover1|| p == Lovers.lover2) infos.Add(lover); - return infos; } - public static String GetRolesString(PlayerControl p, bool useColors) { + public static String GetRolesString(PlayerControl p, bool useColors, bool showModifier = true) { string roleName; - roleName = String.Join(" ", getRoleInfoForPlayer(p).Select(x => useColors ? Helpers.cs(x.color, x.name) : x.name).ToArray()); + roleName = String.Join(" ", getRoleInfoForPlayer(p, showModifier).Select(x => useColors ? Helpers.cs(x.color, x.name) : x.name).ToArray()); if (Lawyer.target != null && p.PlayerId == Lawyer.target.PlayerId && PlayerControl.LocalPlayer != Lawyer.target) roleName += (useColors ? Helpers.cs(Pursuer.color, " §") : " §"); return roleName; } diff --git a/TheOtherRoles/TheOtherRoles.cs b/TheOtherRoles/TheOtherRoles.cs index 83f4d73cc..179c162c3 100644 --- a/TheOtherRoles/TheOtherRoles.cs +++ b/TheOtherRoles/TheOtherRoles.cs @@ -22,6 +22,7 @@ public static class TheOtherRoles public static void clearAndReloadRoles() { Jester.clearAndReload(); Mayor.clearAndReload(); + Portalmaker.clearAndReload(); Engineer.clearAndReload(); Sheriff.clearAndReload(); Deputy.clearAndReload(); @@ -39,7 +40,6 @@ public static void clearAndReloadRoles() { Morphling.clearAndReload(); Camouflager.clearAndReload(); Hacker.clearAndReload(); - Mini.clearAndReload(); Tracker.clearAndReload(); Vampire.clearAndReload(); Snitch.clearAndReload(); @@ -54,12 +54,22 @@ public static void clearAndReloadRoles() { Arsonist.clearAndReload(); Guesser.clearAndReload(); BountyHunter.clearAndReload(); - Bait.clearAndReload(); Vulture.clearAndReload(); Medium.clearAndReload(); Lawyer.clearAndReload(); Pursuer.clearAndReload(); Witch.clearAndReload(); + Ninja.clearAndReload(); + + // Modifier + Bait.clearAndReload(); + Bloody.clearAndReload(); + AntiTeleport.clearAndReload(); + Tiebreaker.clearAndReload(); + Sunglasses.clearAndReload(); + Mini.clearAndReload(); + Vip.clearAndReload(); + Invert.clearAndReload(); } public static class Jester { @@ -77,13 +87,59 @@ public static void clearAndReload() { hasImpostorVision = CustomOptionHolder.jesterHasImpostorVision.getBool(); } } + + public static class Portalmaker { + public static PlayerControl portalmaker; + public static Color color = new Color32(69, 69, 169, byte.MaxValue); + + public static float cooldown; + public static float usePortalCooldown; + public static bool logOnlyHasColors; + public static bool logShowsTime; + + private static Sprite placePortalButtonSprite; + private static Sprite usePortalButtonSprite; + private static Sprite logSprite; + + public static Sprite getPlacePortalButtonSprite() { + if (placePortalButtonSprite) return placePortalButtonSprite; + placePortalButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.PlacePortalButton.png", 115f); + return placePortalButtonSprite; + } + + public static Sprite getUsePortalButtonSprite() { + if (usePortalButtonSprite) return usePortalButtonSprite; + usePortalButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.UsePortalButton.png", 115f); + return usePortalButtonSprite; + } + + public static Sprite getLogSprite() { + if (logSprite) return logSprite; + logSprite = HudManager.Instance.UseButton.fastUseSettings[ImageNames.DoorLogsButton].Image; + return logSprite; + } + + public static void clearAndReload() { + portalmaker = null; + cooldown = CustomOptionHolder.portalmakerCooldown.getFloat(); + usePortalCooldown = CustomOptionHolder.portalmakerUsePortalCooldown.getFloat(); + logOnlyHasColors = CustomOptionHolder.portalmakerLogOnlyColorType.getBool(); + logShowsTime = CustomOptionHolder.portalmakerLogHasTime.getBool(); + } + + + } public static class Mayor { public static PlayerControl mayor; public static Color color = new Color32(32, 77, 66, byte.MaxValue); + public static bool canSeeVoteColors = false; + public static int tasksNeededToSeeVoteColors; public static void clearAndReload() { mayor = null; + canSeeVoteColors = CustomOptionHolder.mayorCanSeeVoteColors.getBool(); + tasksNeededToSeeVoteColors = (int)CustomOptionHolder.mayorTasksNeededToSeeVoteColors.getFloat(); } } @@ -333,6 +389,8 @@ public static class Medic { public static bool showAttemptToShielded = false; public static bool showAttemptToMedic = false; public static bool setShieldAfterMeeting = false; + public static bool showShieldAfterMeeting = false; + public static bool meetingAfterShielding = false; public static Color shieldedColor = new Color32(0, 221, 255, byte.MaxValue); public static PlayerControl currentTarget; @@ -353,7 +411,9 @@ public static void clearAndReload() { showShielded = CustomOptionHolder.medicShowShielded.getSelection(); showAttemptToShielded = CustomOptionHolder.medicShowAttemptToShielded.getBool(); showAttemptToMedic = CustomOptionHolder.medicShowAttemptToMedic.getBool(); - setShieldAfterMeeting = CustomOptionHolder.medicSetShieldAfterMeeting.getBool(); + setShieldAfterMeeting = CustomOptionHolder.medicSetOrShowShieldAfterMeeting.getSelection() == 2; + showShieldAfterMeeting = CustomOptionHolder.medicSetOrShowShieldAfterMeeting.getSelection() == 1; + meetingAfterShielding = false; } } @@ -386,7 +446,10 @@ public static class Swapper { private static Sprite spriteCheck; public static bool canCallEmergency = false; public static bool canOnlySwapOthers = false; - + public static int charges; + public static float rechargeTasksNumber; + public static float rechargedTasks; + public static byte playerId1 = Byte.MaxValue; public static byte playerId2 = Byte.MaxValue; @@ -402,6 +465,9 @@ public static void clearAndReload() { playerId2 = Byte.MaxValue; canCallEmergency = CustomOptionHolder.swapperCanCallEmergency.getBool(); canOnlySwapOthers = CustomOptionHolder.swapperCanOnlySwapOthers.getBool(); + charges = Mathf.RoundToInt(CustomOptionHolder.swapperSwapsNumber.getFloat()); + rechargeTasksNumber = Mathf.RoundToInt(CustomOptionHolder.swapperRechargeTasksNumber.getFloat()); + rechargedTasks = Mathf.RoundToInt(CustomOptionHolder.swapperRechargeTasksNumber.getFloat()); } } @@ -439,8 +505,8 @@ public static void clearAndReload() { lover1 = null; lover2 = null; notAckedExiledIsLover = false; - bothDie = CustomOptionHolder.loversBothDie.getBool(); - enableChat = CustomOptionHolder.loversEnableChat.getBool(); + bothDie = CustomOptionHolder.modifierLoverBothDie.getBool(); + enableChat = CustomOptionHolder.modifierLoverEnableChat.getBool(); } public static PlayerControl getPartner(this PlayerControl player) { @@ -622,35 +688,6 @@ public static void clearAndReload() { } } - public static class Mini { - public static PlayerControl mini; - public static Color color = Color.white; - public const float defaultColliderRadius = 0.2233912f; - public const float defaultColliderOffset = 0.3636057f; - - public static float growingUpDuration = 400f; - public static DateTime timeOfGrowthStart = DateTime.UtcNow; - public static bool triggerMiniLose = false; - - public static void clearAndReload() { - mini = null; - triggerMiniLose = false; - growingUpDuration = CustomOptionHolder.miniGrowingUpDuration.getFloat(); - timeOfGrowthStart = DateTime.UtcNow; - } - - public static float growingProgress() { - if (timeOfGrowthStart == null) return 0f; - - float timeSinceStart = (float)(DateTime.UtcNow - timeOfGrowthStart).TotalMilliseconds; - return Mathf.Clamp(timeSinceStart/(growingUpDuration*1000), 0f, 1f); - } - - public static bool isGrownUp() { - return growingProgress() == 1f; - } - } - public static class Tracker { public static PlayerControl tracker; public static Color color = new Color32(100, 58, 220, byte.MaxValue); @@ -792,6 +829,9 @@ public static class Jackal { public static bool jackalPromotedFromSidekickCanCreateSidekick = true; public static bool canCreateSidekickFromImpostor = true; public static bool hasImpostorVision = false; + public static bool wasTeamRed; + public static bool wasImpostor; + public static bool wasSpy; public static Sprite getSidekickButtonSprite() { if (buttonSprite) return buttonSprite; @@ -820,6 +860,7 @@ public static void clearAndReload() { canCreateSidekickFromImpostor = CustomOptionHolder.jackalCanCreateSidekickFromImpostor.getBool(); formerJackals.Clear(); hasImpostorVision = CustomOptionHolder.jackalAndSidekickHaveImpostorVision.getBool(); + wasTeamRed = wasImpostor = wasSpy = false; } } @@ -830,6 +871,10 @@ public static class Sidekick { public static PlayerControl currentTarget; + public static bool wasTeamRed; + public static bool wasImpostor; + public static bool wasSpy; + public static float cooldown = 30f; public static bool canUseVents = true; public static bool canKill = true; @@ -844,6 +889,7 @@ public static void clearAndReload() { canKill = CustomOptionHolder.sidekickCanKill.getBool(); promotesToJackal = CustomOptionHolder.sidekickPromotesToJackal.getBool(); hasImpostorVision = CustomOptionHolder.jackalAndSidekickHaveImpostorVision.getBool(); + wasTeamRed = wasImpostor = wasSpy = false; } } @@ -1030,7 +1076,7 @@ public static Sprite getPlaceCameraButtonSprite() { private static Sprite animatedVentSealedSprite; public static Sprite getAnimatedVentSealedSprite() { if (animatedVentSealedSprite) return animatedVentSealedSprite; - animatedVentSealedSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.AnimatedVentSealed.png", 160f); + animatedVentSealedSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.AnimatedVentSealed.png", 185f); return animatedVentSealedSprite; } @@ -1209,25 +1255,6 @@ public static void clearAndReload() { } } - public static class Bait { - public static PlayerControl bait; - public static Color color = new Color32(0, 247, 255, byte.MaxValue); - - public static bool highlightAllVents = false; - public static float reportDelay = 0f; - public static bool showKillFlash = true; - - public static bool reported = false; - - public static void clearAndReload() { - bait = null; - reported = false; - highlightAllVents = CustomOptionHolder.baitHighlightAllVents.getBool(); - reportDelay = CustomOptionHolder.baitReportDelay.getFloat(); - showKillFlash = CustomOptionHolder.baitShowKillFlash.getBool(); - } - } - public static class Vulture { public static PlayerControl vulture; public static Color color = new Color32(139, 69, 19, byte.MaxValue); @@ -1313,11 +1340,11 @@ public static class Lawyer { 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 bool lawyerKnowsRole = false; + public static bool targetCanBeJester = false; public static Sprite getTargetSprite() { if (targetSprite) return targetSprite; @@ -1331,11 +1358,11 @@ public static void clearAndReload() { triggerLawyerWin = false; meetings = 0; - targetKnows = CustomOptionHolder.lawyerTargetKnows.getBool(); winsAfterMeetings = CustomOptionHolder.lawyerWinsAfterMeetings.getBool(); neededMeetings = Mathf.RoundToInt(CustomOptionHolder.lawyerNeededMeetings.getFloat()); vision = CustomOptionHolder.lawyerVision.getFloat(); lawyerKnowsRole = CustomOptionHolder.lawyerKnowsRole.getBool(); + targetCanBeJester = CustomOptionHolder.lawyerTargetCanBeJester.getBool(); } } @@ -1405,11 +1432,163 @@ public static void clearAndReload() { currentTarget = spellCastingTarget = null; cooldown = CustomOptionHolder.witchCooldown.getFloat(); cooldownAddition = CustomOptionHolder.witchAdditionalCooldown.getFloat(); - currentCooldownAddition = CustomOptionHolder.witchCooldown.getFloat(); + currentCooldownAddition = 0f; canSpellAnyone = CustomOptionHolder.witchCanSpellAnyone.getBool(); spellCastingDuration = CustomOptionHolder.witchSpellCastingDuration.getFloat(); triggerBothCooldowns = CustomOptionHolder.witchTriggerBothCooldowns.getBool(); witchVoteSavesTargets = CustomOptionHolder.witchVoteSavesTargets.getBool(); } } -} \ No newline at end of file + + public static class Ninja { + public static PlayerControl ninja; + public static Color color = Palette.ImpostorRed; + + public static PlayerControl ninjaMarked; + public static PlayerControl currentTarget; + public static float cooldown = 30f; + public static float traceTime = 1f; + public static bool knowsTargetLocation = false; + + private static Sprite markButtonSprite; + private static Sprite killButtonSprite; + public static Arrow arrow = new Arrow(Color.black); + public static Sprite getMarkButtonSprite() { + if (markButtonSprite) return markButtonSprite; + markButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.NinjaMarkButton.png", 115f); + return markButtonSprite; + } + + public static Sprite getKillButtonSprite() { + if (killButtonSprite) return killButtonSprite; + killButtonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.NinjaAssassinateButton.png", 115f); + return killButtonSprite; + } + + public static void clearAndReload() { + ninja = null; + currentTarget = ninjaMarked = null; + cooldown = CustomOptionHolder.ninjaCooldown.getFloat(); + knowsTargetLocation = CustomOptionHolder.ninjaKnowsTargetLocation.getBool(); + traceTime = CustomOptionHolder.ninjaTraceTime.getFloat(); + + if (arrow?.arrow != null) UnityEngine.Object.Destroy(arrow.arrow); + arrow = new Arrow(Color.black); + if (arrow.arrow != null) arrow.arrow.SetActive(false); + } + } + + // Modifier + public static class Bait { + public static List bait = new List(); + public static Dictionary active = new Dictionary(); + public static Color color = new Color32(0, 247, 255, byte.MaxValue); + + public static float reportDelayMin = 0f; + public static float reportDelayMax = 0f; + public static bool showKillFlash = true; + + public static void clearAndReload() { + bait = new List(); + active = new Dictionary(); + reportDelayMin = CustomOptionHolder.modifierBaitReportDelayMin.getFloat(); + reportDelayMax = CustomOptionHolder.modifierBaitReportDelayMax.getFloat(); + if (reportDelayMin > reportDelayMax) reportDelayMin = reportDelayMax; + showKillFlash = CustomOptionHolder.modifierBaitShowKillFlash.getBool(); + } + } + + public static class Bloody { + public static List bloody = new List(); + public static Dictionary active = new Dictionary(); + public static Dictionary bloodyKillerMap = new Dictionary(); + + public static float duration = 5f; + + public static void clearAndReload() { + bloody = new List(); + active = new Dictionary(); + bloodyKillerMap = new Dictionary(); + duration = CustomOptionHolder.modifierBloodyDuration.getFloat(); + } + } + + public static class AntiTeleport { + public static List antiTeleport = new List(); + public static Vector3 position; + + public static void clearAndReload() { + antiTeleport = new List(); + position = new Vector3(); + } + } + + public static class Tiebreaker { + public static PlayerControl tiebreaker; + + public static bool isTiebreak = false; + + public static void clearAndReload() { + tiebreaker = null; + isTiebreak = false; + } + } + + public static class Sunglasses { + public static List sunglasses = new List(); + public static int vision = 1; + + public static void clearAndReload() { + sunglasses = new List(); + vision = CustomOptionHolder.modifierSunglassesVision.getSelection() + 1; + } + } + public static class Mini { + public static PlayerControl mini; + public static Color color = Color.yellow; + public const float defaultColliderRadius = 0.2233912f; + public const float defaultColliderOffset = 0.3636057f; + + public static float growingUpDuration = 400f; + public static DateTime timeOfGrowthStart = DateTime.UtcNow; + public static bool triggerMiniLose = false; + + public static void clearAndReload() { + mini = null; + triggerMiniLose = false; + growingUpDuration = CustomOptionHolder.modifierMiniGrowingUpDuration.getFloat(); + timeOfGrowthStart = DateTime.UtcNow; + } + + public static float growingProgress() { + if (timeOfGrowthStart == null) return 0f; + + float timeSinceStart = (float)(DateTime.UtcNow - timeOfGrowthStart).TotalMilliseconds; + return Mathf.Clamp(timeSinceStart / (growingUpDuration * 1000), 0f, 1f); + } + + public static bool isGrownUp() { + return growingProgress() == 1f; + } + + } + public static class Vip { + public static List vip = new List(); + public static bool showColor = true; + + public static void clearAndReload() { + vip = new List(); + showColor = CustomOptionHolder.modifierVipShowColor.getBool(); + } + } + + public static class Invert { + public static List invert = new List(); + public static int meetings = 3; + + public static void clearAndReload() { + invert = new List(); + meetings = (int) CustomOptionHolder.modifierInvertDuration.getFloat(); + } + } +} diff --git a/TheOtherRoles/TheOtherRoles.csproj b/TheOtherRoles/TheOtherRoles.csproj index a59901799..8e471d80b 100644 --- a/TheOtherRoles/TheOtherRoles.csproj +++ b/TheOtherRoles/TheOtherRoles.csproj @@ -1,9 +1,10 @@  netstandard2.1 - 3.4.5 + 4.0.0 TheOtherRoles Eisbison + latest @@ -15,6 +16,7 @@ + @@ -22,6 +24,12 @@ + + + + + +