]> git.deb.at Git - rhonda/theotherroles.git/commitdiff
Version 2.9.0
authorEisbison <eisbison.creator@gmail.com>
Thu, 4 Nov 2021 17:51:17 +0000 (18:51 +0100)
committerEisbison <eisbison.creator@gmail.com>
Thu, 4 Nov 2021 17:51:17 +0000 (18:51 +0100)
22 files changed:
.gitignore
Images/TOR_roles.jpg
README.md
TheOtherRoles/Buttons.cs
TheOtherRoles/CustomOptions.cs
TheOtherRoles/Helpers.cs
TheOtherRoles/Main.cs
TheOtherRoles/Patches/CredentialsPatch.cs
TheOtherRoles/Patches/EndGamePatch.cs
TheOtherRoles/Patches/ExileControllerPatch.cs
TheOtherRoles/Patches/IntroPatch.cs
TheOtherRoles/Patches/MeetingPatch.cs
TheOtherRoles/Patches/PlayerControlPatch.cs
TheOtherRoles/Patches/RoleAssignmentPatch.cs
TheOtherRoles/Patches/UpdatePatch.cs
TheOtherRoles/Patches/UsablesPatch.cs
TheOtherRoles/RPC.cs
TheOtherRoles/Resources/MediumButton.png [new file with mode: 0644]
TheOtherRoles/Resources/VultureButton.png [new file with mode: 0644]
TheOtherRoles/RoleInfo.cs
TheOtherRoles/TheOtherRoles.cs
TheOtherRoles/TheOtherRoles.csproj

index 16bb3c309c17ea4b183ce611eda1c9628919c0c1..a49b4f9aec12928bdf00a88ddd95200050d84b50 100644 (file)
@@ -1,2 +1,4 @@
 .vs
-.vscode/tasks.json
\ No newline at end of file
+.vscode/tasks.json
+TODO
+Images/.DS_Store
index b2c65d185733136db1194e791599c46d83749753..69babf88e0e99e7b8c7271dbd709b20968cdd0ff 100644 (file)
Binary files a/Images/TOR_roles.jpg and b/Images/TOR_roles.jpg differ
index 8716ba9c2e875fc560d691a41b73cf27155fffb7..e1eaace67aaeea64de7b7ec0ca78e3288677d9cd 100644 (file)
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Even more roles are coming soon :)
 | [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) |  |  |
+| [Cleaner](#cleaner) | [Hacker](#hacker) | [Vulture](#vulture) |  |
 | [Eraser](#eraser) | [Lighter](#lighter) |  |  |
 | [Godfather (Mafia)](#mafia) | [Mayor](#mayor) |  |  |
 | [Mafioso (Mafia)](#mafia) | [Medic](#medic) |  |  |
@@ -31,12 +31,14 @@ Even more roles are coming soon :)
 |  | [Time Master](#time-master) |  |  |
 |  |  [Tracker](#tracker) |  |  |
 |  |  [Bait](#bait) |  |
+|  |  [Medium](#medium) |  |
 
 The [Role Assignment](#role-assignment) sections explains how the roles are being distributed among the players.
 
 # Releases
 | Among Us - Version| Mod Version | Link |
 |----------|-------------|-----------------|
+| 2021.6.30s| v2.9.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.9.0/TheOtherRoles.zip)
 | 2021.6.30s| v2.8.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.8.1/TheOtherRoles.zip)
 | 2021.6.30s| v2.8.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.8.0/TheOtherRoles.zip)
 | 2021.6.30s| v2.7.3| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v2.7.3/TheOtherRoles.zip)
@@ -75,14 +77,21 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 <details>
   <summary>Click to show the Changelog</summary>
 
+**Version 2.9.0**
+- **New Role:** [Medium](#medium)
+- **New Role:** [Vulture](#vulture)
+- Added Jackal Option: "Jackal Can See If Engineer Is In A Vent"
+- Added Guesser Option: "Guesser Can Shoot Multiple Times Per Meeting"
+- Fixed a bug that occured when the Shifter shifted the Bait
+
 **Hotfix 2.8.1**
 - Fixed a game breaking bug where killing the Bait resulted in a ban of the Bait
 
 **Version 2.8.0**
-- **New Role:** [Bait](#bait) created by [Mallöris](https://github.com/Mallaris) 
+- **New Role:** [Bait](#bait)
 - Added Tracker Option: "Tracker Reset Target After Meeting" (feature created by [MaximeGillot](https://github.com/MaximeGillot))
-- Added Snitch Options: "Include Team Jackal" and "Use Different Arrow Color For Team Jackal" (feature created by [Mallöris](https://github.com/Mallaris))
-- Added Medic Option: "Shield Will Be Set After Next Meeting" (feature created by [Mallöris](https://github.com/Mallaris))
+- Added Snitch Options: "Include Team Jackal" and "Use Different Arrow Color For Team Jackal"
+- Added Medic Option: "Shield Will Be Set After Next Meeting"
 
 **Version 2.7.3**
 - Updated to Among Us v2021.6.30
@@ -99,7 +108,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 - Fixed a bug where the [garlics](#vampire) were not displayed properly
 
 **Version 2.7.0**
-- **New Role:** [Bounty Hunter](#bounty-hunter) created by [Mallöris](https://github.com/Mallaris) 
+- **New Role:** [Bounty Hunter](#bounty-hunter)
 - Added more new [colors](#colors) (Thanks to [Drakoni](https://twitter.com/Drakoni13) for sorting them)
 - Added a setting to the [Shifter](#shifter), that will prevent [Medic Shield](#medic) & [Lover](#lovers) Roles to be shifted
 - Changed [Jackal](#jackal) & [Sidekick](#sidekick) to always be killable by [Sheriff](#sheriff)
@@ -134,7 +143,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 - New option: Ghosts can see votes
 - New option: [Jackal](#jackal) and [Sidekick](#sidekick) have Impostor vision
 - New option: [Jester](#jester) can sabotage
-- Changed FreePlay mode to not assign custom roles anymore
+- Changed Freeplay mode to not assign custom roles anymore
 - Fixed a bug with directional hats not using their flip image after a while
 
 **Version 2.6.3**
@@ -154,7 +163,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 
 **Version 2.6.0**
 - **New Role:** [Arsonist](#arsonist)
-- Added an Ingame Updater, to make it easier to update the Mod
+- Added an In-Game Updater, to make it easier to update the Mod
 - Added synchronization for Airship toilet doors. Doors now open/close for everyone
 - Changed Shifter to also die when shifting a neutral role (Jester, Arsonist, Jackal, ...)
 - Changed the option "Jester Can Die To Sheriff" to "Neutrals Can Die To Sheriff"
@@ -167,10 +176,10 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 
 **Version 2.5.1**
 - **New Hats:** We added the support for custom hats and there are already a few hats inside the game. We can add new hats without updating the mod and we're awaiting your hat designs on our discord server.
-- Changed Lovers to ignore Lover's Tasks for Taskwin, while an ImpLover is alive
+- Changed Lovers to ignore Lover's Tasks for task win, while an ImpLover is alive
 - Fixed a bug where garlic was not visible in some places
 - The Security Guard can't place cameras on MiraHQ anymore
-- Fixed a bug on there Airship, where the view of the cameras that the Security Guard placed wasn't centered on the camera.
+- Fixed a bug on the Airship, where the view of the cameras that the Security Guard placed wasn't centered on the camera.
 
 **Version 2.5.0**
 - **New Role:** [Security Guard](#security-guard)
@@ -197,11 +206,11 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 - Added Trickster Box vent animation. Thanks to [Drakoni](https://twitter.com/Drakoni13)
 - You can now change the custom server ip/port right inside the game
 - The Jackal, the Sidekick and the Jester now have fake tasks
-- Added outlines, to show who you're targetting with your ability. Thanks to [Sihaack](https://github.com/sihaack) for part of the code.
+- Added outlines, to show who you're targeting with your ability. Thanks to [Sihaack](https://github.com/sihaack) for part of the code.
 - Added a streamer mode to Among Us, which hides lobby codes, the ip of your custom server and the port of your custom server. You can also modify the text that replaces the lobby code, check [Settings](#settings) for more details.
-- Changed Meeting Hud Layout when playing with more than 10 players
+- Changed Meeting HUD Layout when playing with more than 10 players
 - Fixed a bug where **ImpLovers** would hardly spawn
-- Fixed a bug where players could get stuck on ladders/platforms when being rewinded
+- Fixed a bug where players could get stuck on ladders/platforms when being rewound
 - Fixed a bug where players could only use quickchat
 - Fixed a bug which prevented to play in Freeplay mode
 - Fixed a bug which moved the Ping info off-screen
@@ -220,7 +229,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 **Version 2.2.0**
 - **Works with the latest Among Us version (2021.4.12s)**
 - **Added support for 10+ player lobbies on custom servers:** Check the [Custom Servers and 10+ Players](#Custom-Servers-and-10+-Players) section. During meetings use the up/down keys, on vitals use the left/right keys.
-- **Added a new Impostor role: The Trickster** check the [Trickster](#trickster) section for more infos
+- **Added a new Impostor role: The Trickster** check the [Trickster](#trickster) section for more info
 - You can now set how long the Time Master shield lasts
 - The host now sees for how long the lobby will remain open
 - We changed the look/layout of the settings
@@ -228,7 +237,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 - You can now choose whether the Eraser is able to erase the Spy/Impostors or not
 - Fixed a bug where a Lovers win wasn't displayed properly
 - Fixed the Among Us bug where people were unable to move after meetings
-- We added a version checking system: The host can only start the game if everyone in his lobby has the same version of the mod installed (he will see, who is using a wrong version). This prevents hacking in public lobbies and bugs because of version missmatches.
+- We added a version checking system: The host can only start the game if everyone in his lobby has the same version of the mod installed (he will see, who is using a wrong version). This prevents hacking in public lobbies and bugs because of version mismatches.
 - Fixed a bug where the Mini Impostor had the same cooldowns as normal Impostors
 - Fixed a bug where the Vampire/Janitor/Mafioso would lose their kill button after being erased
 - The Mini is now able to use ladders and it can do all the tasks right away
@@ -240,7 +249,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 
 **Hotfix 2.0.1**
 - Fixed a bug where camouflaged players would get stuck on ladders/platforms on the airship
-- Introduced a one second cooldown after the Morphling sampled another player
+- Introduced a one-second cooldown after the Morphling sampled another player
 - The Mini can now always reach all usables (ladders, tasks, ...)
 - We removed a bug, where some footprints remained on the ground forever
 - We removed a bug, where the Detective didn't see the right color type when reporting a player
@@ -268,7 +277,7 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 - Add map and Impostor count to lobby settings.
 - Fixed bugs where changing players to be the Sidekick didn't reset all the effects of their previous role.
 
-**Hotfix 1.8.1** Resolves bugs that occured when the Jackal recruited a Medic, Swapper and Tracker\
+**Hotfix 1.8.1** Resolves bugs that occurred when the Jackal recruited a Medic, Swapper and Tracker\
 \
 **Changes in v1.8:**
 - **New Roles:** Added the Jackal and Sidekick roles
@@ -294,21 +303,21 @@ The [Role Assignment](#role-assignment) sections explains how the roles are bein
 
 \
 **Changes in v1.5:**
-- Time Master - Buff: He is not affected by his rewind anymore, which gives him more utility. Players will now be rewinded out of vents.
+- Time Master - Buff: He is not affected by his rewind anymore, which gives him more utility. Players will now be rewound out of vents.
 - Mini - Nerf: The Mini now grows up (see [Mini](#mini)) and becomes a normal Crewmate at some point. A growing Mini is not killable anymore. Some tasks are still not doable for the small Mini, we are working on that. But eventually when growing up it can do all the tasks as it's size increases.
 - Seer - Nerf: Added an option that sets how often the Seer mistakes the player for another.
 - Hacker - Nerf: The Hacker now only sees the additional information when he activates his "Hacker mode". That should stops the Hacker from camping the admin table/vitals.
 - Other: Camouflager/Morphling cooldowns were fixed. Custom regions code was removed to enable 3rd party tools. Some minor bugfixes.
 
 **Changes in v1.4:**
-- Fixing a Camoflager/Morphling animation bug
+- Fixing a Camouflager/Morphling animation bug
 - Fixing a bug where the Swapper could swap votes even if they are dead
 - The custom cooldown buttons now render the cooldown progress (the grey overlay) in the right way (v1.3 introduced the bug)
 - Players in vents are not targetable anymore by the role actions, the button does not activate (e.g. Seer revealing, Morphling sample). Exception: Impostor killing an Engineer in a vent
 
 **Changes in v1.3:**
 - Adds support for the Among Us version **2021.3.5s**
-- Fixes a bug where a an edge case caused all players to start the game with the camouflaged look
+- Fixes a bug where an edge case caused all players to start the game with the camouflaged look
 - There might be a few bugs, since I focused on getting the update out fast. A new version resolving the bugs will be published tomorrow.
 
 **Changes in v1.1:**
@@ -346,7 +355,7 @@ Not working? You might want to install the dependency [vc_redist](https://aka.ms
 4. Launch the game via Steam
 
 # Custom Servers and 10+ Players
-We always recommend you to play on custom servers rather than on the official ones. If you want to play with more than 10 players in one lobby, you're required to use a custom server. Credits for the original implementation that allowed 10+ player lobbies go to the creaters of the [CrowdedMod](https://github.com/CrowdedMods/CrowdedMod).
+We always recommend you to play on custom servers rather than on the official ones. If you want to play with more than 10 players in one lobby, you're required to use a custom server. Credits for the original implementation that allowed 10+ player lobbies go to the creators of the [CrowdedMod](https://github.com/CrowdedMods/CrowdedMod).
 
 **Setup the Clients:**
 1. Open the region menu where you can choose between the different regions
@@ -415,9 +424,9 @@ 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)
+- **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, he'll vote himself.
-- **Hide Player Names:** Hides the names of all players that have role which is unknown to you. Team Lovers/Impostors/Jackal still see the names of their teammates. Impostors can alse see the name of the Spy and everyone can still see the age of the mini.
+- **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.
 - **Ghosts Can See Roles**
 - **Ghosts Can See Votes**
 - **Ghosts Can See The Number Of Remaining Tasks**
@@ -467,7 +476,7 @@ Here are a few instructions, on how to create a custom hat:
   - `Climb texture (optional)`:
     - This texture will be rendered in front of the player, when he's climbing.
     - The name of the texture needs to follow the pattern `hatname_climb.png`.
-- **Testing:** You can test your hat design by putting all the files in the `\TheOtherHats\Test` subfolder of your mod folder. Then whenever you start a freeplay game, you and all the dummies will be wearing the new hat. You don't need to restart Among Us if you change the hat files, just exit and reenter the freeplay mode.
+- **Testing:** You can test your hat design by putting all the files in the `\TheOtherHats\Test` subfolder of your mod folder. Then whenever you start a Freeplay game, you and all the dummies will be wearing the new hat. You don't need to restart Among Us if you change the hat files, just exit and reenter the Freeplay mode.
 
 - **Submission:** If you got a hat design, you can submit it on our [Discord server](https://discord.gg/77RkMJHWsM). We'll look at all the hats and add all the good ones to the game.
 
@@ -482,7 +491,7 @@ if you're using it right.
 
 First you need to choose how many special roles of each kind (Impostor/Neutral/Crewmate) you want in the game.
 The count you set will only be reached, if there are enough Crewmates/Impostors in the game and if enough roles are set to be in the game (i.e. they are set to > 0%). The roles are then being distributed as follows:
-- First all roles that are set to 100% are being assigned to arbitrary players
+- First all roles that are set to 100% are being assigned to arbitrary players.
 - After that each role that has 10%-90% selected adds 1-9 tickets to a ticket pool (there exists a ticket pool for Crewmates, Neutrals and Impostors). Then the roles will be selected randomly from the pools as long it's possible (until the selected number is reached, until there are no more Crewmates/Impostors or until there are no more tickets). If a role is selected from the pool, obviously all the tickets of that role are being removed.
 - The Mafia, Lovers and Mini are being selected independently (without using the ticket system) according to the spawn chance you selected. After that the Crewmate, Neutral and Impostor roles are selected and assigned in a random order.
 
@@ -514,11 +523,11 @@ The Janitor is an Impostor who cannot kill nor sabotage, but they can hide dead
 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 copies its look.
+- They shrink to the size of the Mini when they copy its look.
 - The Hacker sees the new color on the admin table.
 - The color of the footprints changes accordingly (also the ones that were already on the ground).
 - The other Impostor still sees that they are an Impostor (the name remains red).
-- The shield indicator changes accordingly (the Morphling gains or loses the shield indicator)
+- The shield indicator changes accordingly (the Morphling gains or loses the shield indicator).
 - Tracker and Snitch arrows keep working.
 
 ### Game Options
@@ -559,7 +568,7 @@ If a victim is near a garlic, the "Bite Button" turns into the default "Kill But
 **NOTE:**
 - If a bitten player is still alive when a meeting is being called, he dies at the start of the meeting.
 - The cooldown is the same as the default kill cooldown (+ the kill delay if the Vampire bites the target).
-- If there is a Vampire in the game, there can't be a Warlock
+- If there is a Vampire in the game, there can't be a Warlock.
 
 ### Game Options
 | Name | Description |
@@ -580,17 +589,17 @@ By default the Eraser can erase everyone but the Spy and other Impostors. Depend
 he 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 his target, depending on who the Eraser erased)
+- 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 his target, depending on whom the Eraser erased)
 - Erasing a Lover automatically erases the other Lover as well (if the second Lover is an ImpLover, he will turn into an Impostor)
 - Erasing a Jackal that has a Sidekick, triggers the Sidekick promotion if it's activated in the settings
-- As the erasing is being triggered before the eject of a player, erasing and voting out a Lover in the same round, would result in the
-ex Lover surviving as the partnership was erased before. Also a Jester win would not happen, as the erase will be triggered before.
+- As the erase is being triggered before the ejection of a player, erasing and voting out a Lover in the same round, would result in the
+ex-Lover surviving, as the partnership was erased before. Also a Jester win would not happen, as the erase will be triggered before.
 
 ### Game Options
 | Name | Description |
 |----------|:-------------:|
 | Eraser Spawn Chance | -
-| Eraser Cooldown | The Erasers cooldown will increase by 10 seconds after every erase.
+| Eraser Cooldown | The Eraser's cooldown will increase by 10 seconds after every erase.
 | Eraser Can Erase Anyone | If set to false, he can't erase the Spy and other Impostors
 -----------------------
 
@@ -598,7 +607,7 @@ ex Lover surviving as the partnership was erased before. Also a Jester win would
 ### **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 his boxes they will be converted into a vent network usable only by the Trickster himself, 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. \
+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:**
@@ -615,7 +624,7 @@ 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. \
+The Cleaner is an Impostor who has the ability to clean up dead bodies.\
 
 \
 **NOTE:**
@@ -634,11 +643,11 @@ The Cleaner is an Impostor who has the ability to clean up dead bodies. \
 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 he is).\
 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.
+The Warlock can still perform normal kills, but the two buttons share the same cooldown.\
 
 \
 **NOTE:**
-- The Warlock can always kill his Impostor mates (and even himselfs) using the "cursed kill"
+- The Warlock can always kill his Impostor mates (and even himself) using the "cursed kill"
 - If there is a Warlock in the game, there can't be a Vampire
 - Performing a normal kill, doesn't lift the curse
 
@@ -656,11 +665,11 @@ The Warlock can still perform normal kills, but the two buttons share the same c
 \
 Created by  [Mallöris](https://github.com/Mallaris)\
 \
-The Bounty Hunter is an Impostor, that continuously get bountys (the targeted player doesn't get notified).\
-The target of the Bounty Hunter swaps after every meeting and after a configuarble amount of time.\
+The Bounty Hunter is an Impostor, that continuously get bounties (the targeted player doesn't get notified).\
+The target of the Bounty Hunter swaps after every meeting and after a configurable amount of time.\
 If the Bounty Hunter kills his target, his kill cooldown will be a lot less than usual.\
 Killing a player that's not his current target results in an increased kill cooldown.\
-Depending on the options, there'll be an arrow pointing towards the current target.
+Depending on the options, there'll be an arrow pointing towards the current target.\
 
 \
 **NOTE:**
@@ -675,18 +684,17 @@ Depending on the options, there'll be an arrow pointing towards the current targ
 | Cooldown After Killing Bounty | -
 | Additional Cooldown After Killing Others | Time will be added to the normal impostor cooldown if the Bounty Hunter kills a not-bounty player
 | Show Arrow Pointing Towards The Bounty | If set to true an arrow will appear (only visiable for the Bounty Hunter)
-| Bounty Hunter Arrow Update Intervall | Sets how often the position is being updated
+| Bounty Hunter Arrow Update Interval | Sets how often the position is being updated
 -----------------------
 
 
 ## Guesser
 ### **Team: Crewmates or Impostors**
 The Guesser can be a Crewmate or an Impostor (depending on the settings).\
-The Guesser can shoot a player during the meeting, by guessing its role. If the guess is wrong, the Guesser dies instead.\
-You can select how many players can be shot per meeting and how many players can be shot per game.\
+The Guesser can shoot players during the meeting, by guessing its role. If the guess is wrong, the Guesser dies instead.\
+You can select how many players can be shot per game and if multiple players can be shot during a single meeting.\
 The guesses Impostor and Crewmate are only right, if the player is part of the corresponding team and has no special role.\
-You can only shoot during the voting time.
-
+You can only shoot during the voting time.\
 \
 **NOTE:**
 - The vote of a shot player, won't be counted
@@ -701,6 +709,7 @@ You can only shoot during the voting time.
 | Chance That The Guesser Is An Impostor | -
 | Guesser Number Of Shots Per Game| -
 | Guesser Number Of Shots Per Meeting| -
+| Guesser Can Shoot Multiple Times Per Meeting| -
 
 -----------------------
 
@@ -715,7 +724,7 @@ The Lovers never know the role of their partner, they only see who their partner
 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 team Impostor/Jackal Lover in the game, the tasks of a Crewmate Lover won't be counted (for a task win) as long as he's alive. If the Lover dies, his tasks will also be counted.\
+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 he's alive. If the Lover dies, his tasks will also be counted.\
 \
 **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.
@@ -778,7 +787,7 @@ After dousing everyone alive the Arsonist can ignite all the players which resul
 ## 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 slowy fade away.
+The Seer sees the souls of players that died a round earlier, the souls slowly fade away.
 The Seer gets a blue flash on his screen, if a player dies somewhere on the map.
 
 ### Game Options
@@ -796,7 +805,7 @@ The Engineer (if alive) can fix one sabotage per game from anywhere on the map.\
 The Engineer can use vents. If the Engineer is inside a vent, the 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.
+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.
@@ -810,7 +819,7 @@ you can double click on the tasks instead.
 ## 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 recieve 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 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:**
 - 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.
@@ -822,7 +831,7 @@ The Detective's other feature shows when they report a corpse: they recieve clue
 |----------|:-------------:|
 | 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 Intervall | The intervall between two footprints
+| 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. |
@@ -830,7 +839,7 @@ The Detective's other feature shows when they report a corpse: they recieve clue
 
 ## Lighter
 ### **Team: Crewmates**
-The Lighter can turn on his Lighter every now and then, which increases his vision by a costumizable amount.
+The Lighter can turn on his Lighter every now and then, which increases his vision by a customizable amount.
 
 ### Game Options
 | Name | Description |
@@ -873,7 +882,7 @@ The shielded player can still be voted out and might also be an Impostor.\
 If set in the options, the shielded player 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.\
-The Medic's other feature shows when they report a corpse: they will see how long ago the player died
+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.
@@ -892,7 +901,7 @@ The Medic's other feature shows when they report a corpse: they will see how lon
 
 ## Mayor
 ### **Team: Crewmates**
-The Mayor leades the Crewmates by having a vote that counts twice.\
+The Mayor leads the Crewmates by having a vote that counts twice.\
 The Mayor can always use his meeting, even if the maximum number of meetings was reached.
 
 ### Game Options
@@ -909,7 +918,7 @@ Otherwise he sees the same information as everyone else.
 **Vitals**: The Hacker can see how long dead players have been dead for.\
 \
 **NOTE:**
-- If the Morphling morphes or the Camouflager camouflages, the colors on the admin table change accordingly
+- If the Morphling morphs or the Camouflager camouflages, the colors on the admin table change accordingly
 - More information about the [colors](#colors)
 
 ### Game Options
@@ -929,14 +938,14 @@ The Shift will always be performed at the end of the next meeting right before a
 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 possiblity to act like a Shifter.\
+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 his target, depending on who the Eraser erased)
+- 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 his 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 used them before)
+can only use them, if the previous player did not use them before)
 
 ### Game Options
 | Name | Description
@@ -948,18 +957,18 @@ can only use them, if the previous player did not used them before)
 ## Time Master
 ### **Team: Crewmates**
 The Time Master has a time shield which he can activate. The time shield remains active for a configurable amount of time.\
-If a player trys to kill the Time Master while the time shield is active, the kill won't happen and the
+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
+- Only the movement is affected by the rewind.
 - A Vampire bite will trigger the rewind. If the Time Master misses shielding the bite, he can still shield the kill which happens a few seconds later.
-- If the Time Master was bitten and has his shield active before when a meeting is called, he survives but the time won't be rewinded.
+- If the Time Master was bitten and has his shield active before when a meeting is called, he survives but the time won't be rewound.
 - If the Time Master has a Medic shield, he won't rewind.
-- The shield itself ends immediately when triggered. So the timemaster can be attacked again as soon as the rewind ends.
+- 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 |
@@ -974,7 +983,7 @@ The Time Master won't be affected by the rewind.\
 ### **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 Swappers strength in meetings, they might not start emergency meetings
+Because of the Swapper's strength in meetings, they might not start emergency meetings
 and can't fix lights and comms.
 
 ### Game Options
@@ -990,13 +999,13 @@ and can't fix lights and comms.
 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 it's position every few seconds (configurable).
+The arrow updates its position every few seconds (configurable).
 
 ### Game Options
 | Name | Description
 |----------|:-------------:|
 | Tracker Spawn Chance | -
-| Tracker Update Intervall | Sets how often the position is being updated
+| Tracker Update Interval | Sets how often the position is being updated
 | Tracker Reset Target After Meeting | -
 -----------------------
 
@@ -1021,11 +1030,11 @@ 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 his Sidekick.
 Creating a Sidekick removes all tasks of the Sidekick and adds him to the team Jackal. The Sidekick loses his current role (except if he's a Lover, then he plays 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 his Sidekick, but depending on the options the Impostor will either really turn into the Sidekick and leave the team Impostors or he will just look like the Sidekick to the Jackal an remain as he was.\
+The Jackal can also promote Impostors to be his Sidekick but, depending on the options the Impostor will either really turn into the Sidekick and leave the team Impostors or he will just look like the Sidekick to the Jackal and remain as he was.\
 \
 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 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
@@ -1036,14 +1045,14 @@ The priority of the win conditions is the following:
 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 Impostrs left)
+9. Team Crew wins by outnumbering (When there is no player of the team Jackal and the team Impostors left)
 
 **NOTE:**
 - The Jackal (and his Sidekick) may be killed by a Sheriff.
-- A Jackal cannot target the Mini, while it's growing up. After that he 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)
+- A Jackal cannot target the Mini, while it's growing up. After that he 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.
+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.
 
 
 
@@ -1056,8 +1065,9 @@ If both Impostors and Jackals are in the game the game continues even if all Cre
 | 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 his Sidekick | Yes/No (to prevent a Jackal from turning an Impostor into a Sidekick, if he uses the ability on an Impostor he sees 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 ) |
+| Jackals can make an Impostor to his Sidekick | Yes/No (to prevent a Jackal from turning an Impostor into a Sidekick, if he uses the ability on an Impostor he sees 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 | - |
+| Jackal and Sidekick Can See Engineer Vent | - | If set to true, team Jackal will see vents highlited like Impostors if an Engineer is inside one
 -----------------------
 
 ## Sidekick
@@ -1105,7 +1115,7 @@ Placing a new camera and sealing vents takes a configurable amount of screws. Th
 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.\
 **NOTE:**
-- Tickster boxes can't be sealed
+- Trickster boxes can't be sealed
 - The Security Guard can't place Cameras on MiraHQ
 - The remaining number of screws can be seen above his special button
 - On Skeld the four cameras will be replaced every 3 seconds (with the next four cameras). You can also navigate manually using the arrow keys.
@@ -1124,7 +1134,7 @@ The vents will be sealed after the next meeting, players can't enter or exit sea
 ## Bait
 ### **Team: Crewmates**
 \
-Created by  [Mallöris](https://github.com/Mallaris)\
+Created by [Mallöris](https://github.com/Mallaris)\
 \
 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
@@ -1136,10 +1146,54 @@ an outline or all vents do).
 | 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 | -
+-----------------------
+
+## Medium
+### **Team: Crewmates**
+\
+Created by [Mallöris](https://github.com/Mallaris)\
+\
+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 name?
+What is your Role?
+What is your killer's color type?
+When did you die?
+What is your killers role? (mini exluded)
+
+### Game Options
+| Name | Description
+|----------|:-------------:|
+| Bait Spawn Chance | -
+| Medium Cooldown | -
+| Medium Duration | The time it takes to question a soul
+| Medium Each Soul Can Only Be Questioned Once | If set to true, souls can only be questioned once and then disappear
+-----------------------
+
+## Vulture
+### **Team: Neutral**
+\
+Created by [Mallöris](https://github.com/Mallaris)\
+\
+The Vulture does not have any tasks, he has 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.\
+When a player dies, the Vulture gets an arrow pointing to the corpse.
+
+### 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 | -
+-----------------------
 
 # Source code
 It's bad I know, this is a side project and my second week of modding. So there are no best practices around here.
 You can use parts of the code but don't copy paste the whole thing. Make sure you give credits to the other developers, because some parts of the code are based on theirs.
 
 # Bugs, suggestions and requests
-If you found any bugs, have an idea for a new role or any other request, join our [Discord server](https://discord.gg/77RkMJHWsM)
+If you found any bugs, have an idea for a new role or any other request, join our [Discord server](https://discord.gg/77RkMJHWsM).
+
+# Copyedit
+Thanks for fixing our typos [jacktose](https://github.com/jacktose) ✌️
index a1a5887c96ef39423bc353bc11ba21ebdb00e552..9f6520c1227c5a87ec8e8cfb88f8df7504318337 100644 (file)
@@ -33,6 +33,8 @@ namespace TheOtherRoles
         public static CustomButton warlockCurseButton;
         public static CustomButton securityGuardButton;
         public static CustomButton arsonistButton;
+        public static CustomButton vultureEatButton;
+        public static CustomButton mediumButton;
         public static TMPro.TMP_Text securityGuardButtonScrewsText;
 
         public static void setCustomButtonCooldowns() {
@@ -59,6 +61,8 @@ namespace TheOtherRoles
             warlockCurseButton.MaxTimer = Warlock.cooldown;
             securityGuardButton.MaxTimer = SecurityGuard.cooldown;
             arsonistButton.MaxTimer = Arsonist.cooldown;
+            vultureEatButton.MaxTimer = Vulture.cooldown;
+            mediumButton.MaxTimer = Medium.cooldown;
 
             timeMasterShieldButton.EffectDuration = TimeMaster.shieldDuration;
             hackerButton.EffectDuration = Hacker.duration;
@@ -68,6 +72,7 @@ namespace TheOtherRoles
             morphlingButton.EffectDuration = Morphling.duration;
             lightsOutButton.EffectDuration = Trickster.lightsOutDuration;
             arsonistButton.EffectDuration = Arsonist.duration;
+            mediumButton.EffectDuration = Medium.duration;
 
             // Already set the timer to the max, as the button is enabled during the game and not available at the start
             lightsOutButton.Timer = lightsOutButton.MaxTimer;
@@ -175,7 +180,7 @@ namespace TheOtherRoles
                     byte targetId = 0;
                     if ((Sheriff.currentTarget.Data.IsImpostor && (Sheriff.currentTarget != Mini.mini || Mini.isGrownUp())) || 
                         (Sheriff.spyCanDieToSheriff && Spy.spy == Sheriff.currentTarget) ||
-                        (Sheriff.canKillNeutrals && (Arsonist.arsonist == Sheriff.currentTarget || Jester.jester == Sheriff.currentTarget)) ||
+                        (Sheriff.canKillNeutrals && (Arsonist.arsonist == Sheriff.currentTarget || Jester.jester == Sheriff.currentTarget || Vulture.vulture == Sheriff.currentTarget)) ||
                         (Jackal.jackal == Sheriff.currentTarget || Sidekick.sidekick == Sheriff.currentTarget)) {
                         targetId = Sheriff.currentTarget.PlayerId;
                     }
@@ -781,6 +786,127 @@ namespace TheOtherRoles
                 }
             );
 
+            // Vulture Eat
+            vultureEatButton = new CustomButton(
+                () => {
+                    foreach (Collider2D collider2D in Physics2D.OverlapCircleAll(PlayerControl.LocalPlayer.GetTruePosition(), PlayerControl.LocalPlayer.MaxReportDistance, Constants.PlayersOnlyMask)) {
+                        if (collider2D.tag == "DeadBody") {
+                            DeadBody component = collider2D.GetComponent<DeadBody>();
+                            if (component && !component.Reported) {
+                                Vector2 truePosition = PlayerControl.LocalPlayer.GetTruePosition();
+                                Vector2 truePosition2 = component.TruePosition;
+                                if (Vector2.Distance(truePosition2, truePosition) <= PlayerControl.LocalPlayer.MaxReportDistance && PlayerControl.LocalPlayer.CanMove && !PhysicsHelpers.AnythingBetween(truePosition, truePosition2, Constants.ShipAndObjectsMask, false)) {
+                                    GameData.PlayerInfo playerInfo = GameData.Instance.GetPlayerById(component.ParentId);
+
+                                    MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.CleanBody, Hazel.SendOption.Reliable, -1);
+                                    writer.Write(playerInfo.PlayerId);
+                                    AmongUsClient.Instance.FinishRpcImmediately(writer);
+                                    RPCProcedure.cleanBody(playerInfo.PlayerId);
+
+                                    Vulture.cooldown = vultureEatButton.Timer = vultureEatButton.MaxTimer;
+                                    Vulture.eatenBodies++;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    if (Vulture.eatenBodies == Vulture.vultureNumberToWin) {
+                        MessageWriter winWriter = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VultureWin, Hazel.SendOption.Reliable, -1);
+                        AmongUsClient.Instance.FinishRpcImmediately(winWriter);
+                        RPCProcedure.vultureWin();
+                        return;
+                    }
+                },
+                () => { return Vulture.vulture != null && Vulture.vulture == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => { return __instance.ReportButton.renderer.color == Palette.EnabledColor && PlayerControl.LocalPlayer.CanMove; },
+                () => { vultureEatButton.Timer = vultureEatButton.MaxTimer; },
+                Vulture.getButtonSprite(),
+                new Vector3(-1.3f, 0f, 0f),
+                __instance,
+                KeyCode.F
+            );
+
+            // Medium button
+            mediumButton = new CustomButton(
+                () => {
+                    if (Medium.target != null) {
+                        Medium.soulTarget = Medium.target;
+                        mediumButton.HasEffect = true;
+                    }
+                },
+                () => { return Medium.medium != null && Medium.medium == PlayerControl.LocalPlayer && !PlayerControl.LocalPlayer.Data.IsDead; },
+                () => {
+                    if (mediumButton.isEffectActive && Medium.target != Medium.soulTarget) {
+                        Medium.soulTarget = null;
+                        mediumButton.Timer = 0f;
+                        mediumButton.isEffectActive = false;
+                    }
+                    return Medium.target != null && PlayerControl.LocalPlayer.CanMove;
+                },
+                () => {
+                    mediumButton.Timer = mediumButton.MaxTimer;
+                    mediumButton.isEffectActive = false;
+                    Medium.soulTarget = null;
+                },
+                Medium.getQuestionSprite(),
+                new Vector3(-1.3f, 0f, 0f),
+                __instance,
+                KeyCode.Q,
+                true,
+                Medium.duration,
+                () => {
+                    mediumButton.Timer = mediumButton.MaxTimer;
+                    if (Medium.target == null || Medium.target.player == null) return;
+                    string msg = "";
+
+                    int randomNumber = Medium.target.player.PlayerId == Mini.mini?.PlayerId ? TheOtherRoles.rnd.Next(4) : TheOtherRoles.rnd.Next(5);
+                    string typeOfColor = Helpers.isLighterColor(Medium.target.killerIfExisting.Data.ColorId) ? "lighter" : "darker";
+                    float timeSinceDeath = ((float)(Medium.meetingStartTime - Medium.target.timeOfDeath).TotalMilliseconds);
+                
+                    if (randomNumber == 0) msg = "What is your Name? My name is " + Medium.target.player.Data.PlayerName;
+                    else if (randomNumber == 1) msg = "What is your role? My role is " + RoleInfo.GetRole(Medium.target.player);
+                    else if (randomNumber == 2) msg = "What is your killer`s color type? My killer is a " + typeOfColor + " color";
+                    else if (randomNumber == 3) msg = "When did you die? I have died " + Math.Round(timeSinceDeath / 1000) + "s before meeting started";
+                    else msg = "What is your killer`s role? My killer is " + RoleInfo.GetRole(Medium.target.killerIfExisting); //exlude mini 
+
+                    DestroyableSingleton<HudManager>.Instance.Chat.AddChat(PlayerControl.LocalPlayer, $"{msg}");
+
+                    // Remove soul
+                    if (Medium.oneTimeUse) {
+                        float closestDistance = float.MaxValue;
+                        SpriteRenderer target = null;
+
+                        foreach ((DeadPlayer db, Vector3 ps) in Medium.deadBodies) {
+                            if (db == Medium.target) {
+                                Tuple<DeadPlayer, Vector3> deadBody = Tuple.Create(db, ps);
+                                Medium.deadBodies.Remove(deadBody);
+                                break;
+                            }
+
+                        }
+                        foreach (SpriteRenderer rend in Medium.souls) {
+                            float distance = Vector2.Distance(rend.transform.position, PlayerControl.LocalPlayer.GetTruePosition());
+                            if (distance < closestDistance) {
+                                closestDistance = distance;
+                                target = rend;
+                            }
+                        }
+
+                        HudManager.Instance.StartCoroutine(Effects.Lerp(5f, new Action<float>((p) => {
+                            if (target != null) {
+                                var tmp = target.color;
+                                tmp.a = Mathf.Clamp01(1 - p);
+                                target.color = tmp;
+                            }
+                            if (p == 1f && target != null && target.gameObject != null) UnityEngine.Object.Destroy(target.gameObject);
+                        })));
+
+                        Medium.souls.Remove(target);
+                    }
+                }
+            );
+
+
             // Set the default (or settings from the previous game) timers/durations when spawning the buttons
             setCustomButtonCooldowns();
         }
index e63afc0b307bb4f664f718c4450a42a06d4cc58a..7813a0da8f86248b5ac41d76c365efe9e2798adc 100644 (file)
@@ -53,6 +53,7 @@ namespace TheOtherRoles {
         public static CustomOption guesserSpawnRate;
         public static CustomOption guesserIsImpGuesserRate;
         public static CustomOption guesserNumberOfShots;
+        public static CustomOption guesserHasMultipleShotsPerMeeting;
 
         public static CustomOption jesterSpawnRate;
         public static CustomOption jesterCanCallEmergency;
@@ -73,6 +74,7 @@ namespace TheOtherRoles {
         public static CustomOption jackalPromotedFromSidekickCanCreateSidekick;
         public static CustomOption jackalCanCreateSidekickFromImpostor;
         public static CustomOption jackalAndSidekickHaveImpostorVision;
+        public static CustomOption jackalCanSeeEngineerVent;
 
         public static CustomOption bountyHunterSpawnRate;
         public static CustomOption bountyHunterBountyDuration;
@@ -166,6 +168,16 @@ namespace TheOtherRoles {
         public static CustomOption baitHighlightAllVents;
         public static CustomOption baitReportDelay;
 
+        public static CustomOption vultureSpawnRate;
+        public static CustomOption vultureCooldown;
+        public static CustomOption vultureNumberToWin;
+        public static CustomOption vultureCanUseVents;
+
+        public static CustomOption mediumSpawnRate;
+        public static CustomOption mediumCooldown;
+        public static CustomOption mediumDuration;
+        public static CustomOption mediumOneTimeUse;
+
         public static CustomOption maxNumberOfMeetings;
         public static CustomOption blockSkippingInEmergencyMeetings;
         public static CustomOption noVoteIsSelfVote;
@@ -246,6 +258,7 @@ namespace TheOtherRoles {
             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);
 
             jesterSpawnRate = CustomOption.Create(60, cs(Jester.color, "Jester"), rates, null, true);
             jesterCanCallEmergency = CustomOption.Create(61, "Jester can call emergency meeting", true, jesterSpawnRate);
@@ -266,6 +279,12 @@ namespace TheOtherRoles {
             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);
+            jackalCanSeeEngineerVent = CustomOption.Create(431, "Jackal Can See If Engineer Is In A Vent", 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, 0f, 5f, 1f, vultureSpawnRate);
+            vultureCanUseVents = CustomOption.Create(343, "Vulture Can Use Vents", true, vultureSpawnRate);
 
             shifterSpawnRate = CustomOption.Create(70, cs(Shifter.color, "Shifter"), rates, null, true);
             shifterShiftsModifiers = CustomOption.Create(71, "Shifter Shifts Modifiers", false, shifterSpawnRate);
@@ -278,7 +297,6 @@ namespace TheOtherRoles {
             sheriffCooldown = CustomOption.Create(101, "Sheriff Cooldown", 30f, 10f, 60f, 2.5f, sheriffSpawnRate);
             sheriffCanKillNeutrals = CustomOption.Create(102, "Sheriff Can Kill Neutrals", false, sheriffSpawnRate);
 
-
             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);
@@ -340,6 +358,11 @@ namespace TheOtherRoles {
             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);
 
+            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);
+
             // 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);
@@ -658,7 +681,7 @@ namespace TheOtherRoles {
             var hudString = sb.ToString();
 
             int defaultSettingsLines = 19;
-            int roleSettingsLines = defaultSettingsLines + 35;
+            int roleSettingsLines = defaultSettingsLines + 37;
             int detailedSettingsP1 = roleSettingsLines + 37;
             int detailedSettingsP2 = detailedSettingsP1 + 38;
             int end1 = hudString.TakeWhile(c => (defaultSettingsLines -= (c == '\n' ? 1 : 0)) > 0).Count();
@@ -680,7 +703,7 @@ namespace TheOtherRoles {
                 gap = 18;
                 index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
                 hudString = hudString.Insert(index + 1, "\n");
-                gap = 22;
+                gap = 23;
                 index = hudString.TakeWhile(c => (gap -= (c == '\n' ? 1 : 0)) > 0).Count();
                 hudString = hudString.Insert(index + 1, "\n");
             } else if (counter == 2) {
index 6c0ee82aa865d7871c605745929faed408b324cf..52c2a2f42ab3da3832bae6f10b8497503ff44b10 100644 (file)
@@ -191,7 +191,7 @@ namespace TheOtherRoles {
         }
 
         public static bool hasFakeTasks(this PlayerControl player) {
-            return (player == Jester.jester || player == Jackal.jackal || player == Sidekick.sidekick || player == Arsonist.arsonist || Jackal.formerJackals.Contains(player));
+            return (player == Jester.jester || player == Jackal.jackal || player == Sidekick.sidekick || player == Arsonist.arsonist || player == Vulture.vulture || Jackal.formerJackals.Contains(player));
         }
 
         public static bool canBeErased(this PlayerControl player) {
index 87054a5e4b512646226d871339aac785fd819b63..10129cc42b4babb7359888f85e34e7c3b96ef6fa 100644 (file)
@@ -21,7 +21,7 @@ namespace TheOtherRoles
     public class TheOtherRolesPlugin : BasePlugin
     {
         public const string Id = "me.eisbison.theotherroles";
-        public const string VersionString = "2.8.1";
+        public const string VersionString = "2.9.0";
         public static System.Version Version = System.Version.Parse(VersionString);
 
         public Harmony Harmony { get; } = new Harmony(Id);
index ac4cefe940efcbc6674291d3e822bf0bd10b899f..bf04743acc71bd88ee9c2bfec334545ff817e151 100644 (file)
@@ -11,12 +11,12 @@ namespace TheOtherRoles.Patches {
     public static class CredentialsPatch {
         public static string fullCredentials = 
 $@"<size=130%><color=#ff351f>TheOtherRoles</color></size> v{TheOtherRolesPlugin.Version.ToString()}
-<size=80%>Modded by <color=#FCCE03FF>Eisbison</color>,
-<color=#FCCE03FF>Thunderstorm584</color> & <color=#FCCE03FF>EndOfFile</color>
+<size=80%>Modded by <color=#FCCE03FF>Eisbison</color>, <color=#FCCE03FF>EndOfFile</color>
+<color=#FCCE03FF>Thunderstorm584</color> & <color=#FCCE03FF>Mallöris</color>
 Button design by <color=#FCCE03FF>Bavari</color></size>";
 
     public static string mainMenuCredentials = 
-$@"Modded by <color=#FCCE03FF>Eisbison</color>, <color=#FCCE03FF>Thunderstorm584</color> & <color=#FCCE03FF>EndOfFile</color>
+$@"Modded by <color=#FCCE03FF>Eisbison</color>, <color=#FCCE03FF>Thunderstorm584</color>, <color=#FCCE03FF>EndOfFile</color> & <color=#FCCE03FF>Mallöris</color>
 Design by <color=#FCCE03FF>Bavari</color>";
 
         [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))]
index f53b07229a33e33a079ac2d28fd76b4b6f1bae0a..097af82e7a12856b1b9780983b272e0ade92b4c6 100644 (file)
@@ -17,7 +17,8 @@ namespace TheOtherRoles.Patches {
         TeamJackalWin = 11,
         MiniLose = 12,
         JesterWin = 13,
-        ArsonistWin = 14
+        ArsonistWin = 14,
+        VultureWin = 15
     }
 
     enum WinCondition {
@@ -27,7 +28,8 @@ namespace TheOtherRoles.Patches {
         JesterWin,
         JackalWin,
         MiniLose,
-        ArsonistWin
+        ArsonistWin,
+        VultureWin
     }
 
     static class AdditionalTempData {
@@ -66,12 +68,13 @@ namespace TheOtherRoles.Patches {
                 AdditionalTempData.playerRoles.Add(new AdditionalTempData.PlayerRoleInfo() { PlayerName = playerControl.Data.PlayerName, Roles = roles, TasksTotal = tasksTotal, TasksCompleted = tasksCompleted });
             }
 
-            // Remove Jester, Arsonist, Jackal, former Jackals and Sidekick from winners (if they win, they'll be readded)
+            // Remove Jester, Arsonist, Vulture, Jackal, former Jackals and Sidekick from winners (if they win, they'll be readded)
             List<PlayerControl> notWinners = new List<PlayerControl>();
             if (Jester.jester != null) notWinners.Add(Jester.jester);
             if (Sidekick.sidekick != null) notWinners.Add(Sidekick.sidekick);
             if (Jackal.jackal != null) notWinners.Add(Jackal.jackal);
             if (Arsonist.arsonist != null) notWinners.Add(Arsonist.arsonist);
+            if (Vulture.vulture != null) notWinners.Add(Vulture.vulture);
             notWinners.AddRange(Jackal.formerJackals);
 
             List<WinningPlayerData> winnersToRemove = new List<WinningPlayerData>();
@@ -85,6 +88,7 @@ namespace TheOtherRoles.Patches {
             bool miniLose = Mini.mini != null && gameOverReason == (GameOverReason)CustomGameOverReason.MiniLose;
             bool loversWin = Lovers.existingAndAlive() && (gameOverReason == (GameOverReason)CustomGameOverReason.LoversWin || (TempData.DidHumansWin(gameOverReason) && !Lovers.existingWithKiller())); // Either they win if they are among the last 3 players, or they win if they are both Crewmates and both alive and the Crew wins (Team Imp/Jackal Lovers can only win solo wins)
             bool teamJackalWin = gameOverReason == (GameOverReason)CustomGameOverReason.TeamJackalWin && ((Jackal.jackal != null && !Jackal.jackal.Data.IsDead) || (Sidekick.sidekick != null && !Sidekick.sidekick.Data.IsDead));
+            bool vultureWin = Vulture.vulture != null && gameOverReason == (GameOverReason)CustomGameOverReason.VultureWin;
 
             // Mini lose
             if (miniLose) {
@@ -111,6 +115,14 @@ namespace TheOtherRoles.Patches {
                 AdditionalTempData.winCondition = WinCondition.ArsonistWin;
             }
 
+            // Vulture win
+            else if (vultureWin) {
+                TempData.winners = new Il2CppSystem.Collections.Generic.List<WinningPlayerData>();
+                WinningPlayerData wpd = new WinningPlayerData(Vulture.vulture.Data);
+                TempData.winners.Add(wpd);
+                AdditionalTempData.winCondition = WinCondition.VultureWin;
+            }
+
             // Lovers win conditions
             else if (loversWin) {
                 // Double win for lovers, crewmates also win
@@ -177,6 +189,10 @@ namespace TheOtherRoles.Patches {
                 textRenderer.text = "Arsonist Wins";
                 textRenderer.color = Arsonist.color;
             }
+            else if (AdditionalTempData.winCondition == WinCondition.VultureWin) {
+                textRenderer.text = "Vulture Wins";
+                textRenderer.color = Vulture.color;
+            }
             else if (AdditionalTempData.winCondition == WinCondition.LoversTeamWin) {
                 textRenderer.text = "Lovers And Crewmates Win";
                 textRenderer.color = Lovers.color;
@@ -234,6 +250,7 @@ namespace TheOtherRoles.Patches {
             if (CheckAndEndGameForMiniLose(__instance)) return false;
             if (CheckAndEndGameForJesterWin(__instance)) return false;
             if (CheckAndEndGameForArsonistWin(__instance)) return false;
+            if (CheckAndEndGameForVultureWin(__instance)) return false;
             if (CheckAndEndGameForSabotageWin(__instance)) return false;
             if (CheckAndEndGameForTaskWin(__instance)) return false;
             if (CheckAndEndGameForLoverWin(__instance, statistics)) return false;
@@ -270,6 +287,15 @@ namespace TheOtherRoles.Patches {
             return false;
         }
 
+        private static bool CheckAndEndGameForVultureWin(ShipStatus __instance) {
+            if (Vulture.triggerVultureWin) {
+                __instance.enabled = false;
+                ShipStatus.RpcEndGame((GameOverReason)CustomGameOverReason.VultureWin, false);
+                return true;
+            }
+            return false;
+        }
+
         private static bool CheckAndEndGameForSabotageWin(ShipStatus __instance) {
             if (__instance.Systems == null) return false;
             ISystemType systemType = __instance.Systems.ContainsKey(SystemTypes.LifeSupp) ? __instance.Systems[SystemTypes.LifeSupp] : null;
index b8b0b87c8a81220701cc3996560da060c6146671..2d519c18102d54cd2bf013375aa0e7ee50fcb65c 100644 (file)
@@ -151,6 +151,28 @@ namespace TheOtherRoles.Patches {
             // Force Bounty Hunter Bounty Update
             if (BountyHunter.bountyHunter != null && BountyHunter.bountyHunter == PlayerControl.LocalPlayer)
                 BountyHunter.bountyUpdateTimer = 0f;
+
+            // Medium spawn souls
+            if (Medium.medium != null && PlayerControl.LocalPlayer == Medium.medium) {
+                if (Medium.souls != null) {
+                    foreach (SpriteRenderer sr in Medium.souls) UnityEngine.Object.Destroy(sr.gameObject);
+                    Medium.souls = new List<SpriteRenderer>();
+                }
+
+                if (Medium.featureDeadBodies != null) {
+                    foreach ((DeadPlayer db, Vector3 ps) in Medium.featureDeadBodies) {
+                        GameObject s = new GameObject();
+                        s.transform.position = ps;
+                        s.layer = 5;
+                        var rend = s.AddComponent<SpriteRenderer>();
+                        rend.sprite = Medium.getSoulSprite();
+                        Medium.souls.Add(rend);
+                    }
+                    Medium.deadBodies = Medium.featureDeadBodies;
+                    Medium.featureDeadBodies = new List<Tuple<DeadPlayer, Vector3>>();
+                }
+            }
+
         }
     }
 
index 03b03219a205a664c8b3ab1b5e5130ea2e879d69..282fbb6e0aa932a9771b1c808f3e75c058038cc0 100644 (file)
@@ -58,7 +58,7 @@ namespace TheOtherRoles.Patches {
     class IntroPatch {
         public static void setupIntroTeam(IntroCutscene __instance, ref  Il2CppSystem.Collections.Generic.List<PlayerControl> yourTeam) {
             // Intro solo teams
-            if (PlayerControl.LocalPlayer == Jester.jester || PlayerControl.LocalPlayer == Jackal.jackal || PlayerControl.LocalPlayer == Arsonist.arsonist) {
+            if (PlayerControl.LocalPlayer == Jester.jester || PlayerControl.LocalPlayer == Jackal.jackal || PlayerControl.LocalPlayer == Arsonist.arsonist || PlayerControl.LocalPlayer == Vulture.vulture) {
                 var soloTeam = new Il2CppSystem.Collections.Generic.List<PlayerControl>();
                 soloTeam.Add(PlayerControl.LocalPlayer);
                 yourTeam = soloTeam;
index 052a87e6f77af7095763351ca9f974666b698582..764b6a6ff8991b2045a3009b66fb5f512d39a6ce 100644 (file)
@@ -284,14 +284,19 @@ namespace TheOtherRoles.Patches {
 
                         target = (mainRoleInfo == roleInfo) ? target : PlayerControl.LocalPlayer;
 
+                        // Reset the GUI
+                        __instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(true));
+                        UnityEngine.Object.Destroy(container.gameObject);
+                        if (Guesser.hasMultipleShotsPerMeeting && Guesser.remainingShots > 1 && target != PlayerControl.LocalPlayer)
+                            __instance.playerStates.ToList().ForEach(x => { if (x.TargetPlayerId == target.PlayerId && x.transform.FindChild("ShootButton") != null) UnityEngine.Object.Destroy(x.transform.FindChild("ShootButton").gameObject); });
+                        else
+                            __instance.playerStates.ToList().ForEach(x => { if (x.transform.FindChild("ShootButton") != null) UnityEngine.Object.Destroy(x.transform.FindChild("ShootButton").gameObject); });
+
+                        // Shoot player
                         MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.GuesserShoot, Hazel.SendOption.Reliable, -1);
                         writer.Write(target.PlayerId);
                         AmongUsClient.Instance.FinishRpcImmediately(writer);
                         RPCProcedure.guesserShoot(target.PlayerId);
-
-                        __instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(true));
-                        UnityEngine.Object.Destroy(container.gameObject);
-                        __instance.playerStates.ToList().ForEach(x => { if (x.transform.FindChild("ShootButton") != null) UnityEngine.Object.Destroy(x.transform.FindChild("ShootButton").gameObject); });
                     }
                 }));
 
@@ -338,7 +343,7 @@ namespace TheOtherRoles.Patches {
             }
 
             // Add Guesser Buttons
-            if (Guesser.guesser != null && PlayerControl.LocalPlayer == Guesser.guesser && !Guesser.guesser.Data.IsDead && Guesser.remainingShots >= 0) {
+            if (Guesser.guesser != null && PlayerControl.LocalPlayer == Guesser.guesser && !Guesser.guesser.Data.IsDead && Guesser.remainingShots > 0) {
                 for (int i = 0; i < __instance.playerStates.Length; i++) {
                     PlayerVoteArea playerVoteArea = __instance.playerStates[i];
                     if (playerVoteArea.AmDead || playerVoteArea.TargetPlayerId == Guesser.guesser.PlayerId) continue;
@@ -379,6 +384,8 @@ namespace TheOtherRoles.Patches {
         [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CoStartMeeting))]
         class StartMeetingPatch {
             public static void Prefix(PlayerControl __instance, [HarmonyArgument(0)]GameData.PlayerInfo meetingTarget) {
+                // Medium meeting start time
+                Medium.meetingStartTime = DateTime.UtcNow;
                 // Reset vampire bitten
                 Vampire.bitten = null;
                 // Count meetings
index 96f07872adb64e37f62acf8d6cd6bb4691fe6bef..ea66773269ba56b81f043dcb0eaefe60273c7a52 100644 (file)
@@ -232,7 +232,7 @@ namespace TheOtherRoles.Patches {
         }
 
         static void engineerUpdate() {
-            if (PlayerControl.LocalPlayer.Data.IsImpostor && ShipStatus.Instance?.AllVents != null) {
+            if ((Jackal.canSeeEngineerVent && (PlayerControl.LocalPlayer == Jackal.jackal || PlayerControl.LocalPlayer == Sidekick.sidekick)) || PlayerControl.LocalPlayer.Data.IsImpostor && ShipStatus.Instance?.AllVents != null) {
                 foreach (Vent vent in ShipStatus.Instance.AllVents) {
                     try {
                         if (vent?.myRend?.material != null) {
@@ -558,6 +558,49 @@ namespace TheOtherRoles.Patches {
                 }
             }
         }
+        static void vultureUpdate() {
+            if (Vulture.vulture == null || PlayerControl.LocalPlayer != Vulture.vulture || Vulture.localArrows == null) return;
+            if (Vulture.vulture.Data.IsDead) {
+                foreach (Arrow arrow in Vulture.localArrows) UnityEngine.Object.Destroy(arrow.arrow);
+                Vulture.localArrows = new List<Arrow>();
+                return; 
+            }
+
+            DeadBody[] deadBodies = UnityEngine.Object.FindObjectsOfType<DeadBody>();
+            bool arrowUpdate = Vulture.localArrows.Count != deadBodies.Count();
+            int index = 0;
+
+            if (arrowUpdate) {
+                foreach (Arrow arrow in Vulture.localArrows) UnityEngine.Object.Destroy(arrow.arrow);
+                Vulture.localArrows = new List<Arrow>();
+            }
+            
+            foreach (DeadBody db in deadBodies) {
+                if (arrowUpdate) {
+                    Vulture.localArrows.Add(new Arrow(Vulture.color));
+                    Vulture.localArrows[index].arrow.SetActive(true);
+                }
+                if (Vulture.localArrows[index] != null) Vulture.localArrows[index].Update(db.transform.position);
+                index++;
+            }
+        }
+
+        public static void mediumSetTarget() {
+            if (Medium.medium == null || Medium.medium != PlayerControl.LocalPlayer || Medium.medium.Data.IsDead || Medium.deadBodies == null || ShipStatus.Instance?.AllVents == null) return;
+
+            DeadPlayer target = null;
+            Vector2 truePosition = PlayerControl.LocalPlayer.GetTruePosition();
+            float closestDistance = float.MaxValue;
+            float usableDistance = ShipStatus.Instance.AllVents.FirstOrDefault().UsableDistance;
+            foreach ((DeadPlayer dp, Vector3 ps) in Medium.deadBodies) {
+                float distance = Vector2.Distance(ps, truePosition);
+                if (distance <= usableDistance && distance < closestDistance) {
+                    closestDistance = distance;
+                    target = dp;
+                }
+            }
+            Medium.target = target;
+        }
 
         public static void Postfix(PlayerControl __instance) {
             if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return;
@@ -618,6 +661,10 @@ namespace TheOtherRoles.Patches {
                 bountyHunterUpdate();
                 // Bait
                 baitUpdate();
+                // Vulture
+                vultureUpdate();
+                // Medium
+                mediumSetTarget();
             } 
         }
     }
@@ -749,8 +796,8 @@ namespace TheOtherRoles.Patches {
 
             // Cleaner Button Sync
             if (Cleaner.cleaner != null && PlayerControl.LocalPlayer == Cleaner.cleaner && __instance == Cleaner.cleaner && HudManagerStartPatch.cleanerCleanButton != null) 
-                HudManagerStartPatch.cleanerCleanButton.Timer = Cleaner.cleaner.killTimer; 
-            
+                HudManagerStartPatch.cleanerCleanButton.Timer = Cleaner.cleaner.killTimer;
+
             // Warlock Button Sync
             if (Warlock.warlock != null && PlayerControl.LocalPlayer == Warlock.warlock && __instance == Warlock.warlock && HudManagerStartPatch.warlockCurseButton != null) {
                 if(Warlock.warlock.killTimer > HudManagerStartPatch.warlockCurseButton.Timer) {
@@ -775,6 +822,11 @@ namespace TheOtherRoles.Patches {
             }
             if (Seer.deadBodyPositions != null) Seer.deadBodyPositions.Add(target.transform.position);
 
+            // Medium add body
+            if (Medium.deadBodies != null) {
+                Medium.featureDeadBodies.Add(new Tuple<DeadPlayer, Vector3>(deadPlayer, target.transform.position));
+            }
+
             // Mini set adapted kill cooldown
             if (Mini.mini != null && PlayerControl.LocalPlayer == Mini.mini && Mini.mini.Data.IsImpostor && Mini.mini == __instance) {
                 var multiplier = Mini.isGrownUp() ? 0.66f : 2f;
index 94ede4934f7e57b6d36eac75f68837a766b35329..6be6579f79a56e358edc397687fcc4f0f3ac990a 100644 (file)
@@ -76,6 +76,7 @@ namespace TheOtherRoles.Patches {
             neutralSettings.Add((byte)RoleId.Jester, CustomOptionHolder.jesterSpawnRate.getSelection());
             neutralSettings.Add((byte)RoleId.Arsonist, CustomOptionHolder.arsonistSpawnRate.getSelection());
             neutralSettings.Add((byte)RoleId.Jackal, CustomOptionHolder.jackalSpawnRate.getSelection());
+            neutralSettings.Add((byte)RoleId.Vulture, CustomOptionHolder.vultureSpawnRate.getSelection());
 
             crewSettings.Add((byte)RoleId.Mayor, CustomOptionHolder.mayorSpawnRate.getSelection());
             crewSettings.Add((byte)RoleId.Engineer, CustomOptionHolder.engineerSpawnRate.getSelection());
@@ -91,6 +92,7 @@ namespace TheOtherRoles.Patches {
             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
                 crewSettings.Add((byte)RoleId.Spy, CustomOptionHolder.spySpawnRate.getSelection());
index ebb0da6a80b6faa55c3d3014543330175920b521..a2803b5097ec12882169d4ea8e3041037ebd36e9 100644 (file)
@@ -118,6 +118,10 @@ namespace TheOtherRoles.Patches {
                 setPlayerNameColor(Guesser.guesser, Guesser.guesser.Data.IsImpostor ? Palette.ImpostorRed : Guesser.color);
             } 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) {
+                setPlayerNameColor(Medium.medium, Medium.color);
             }
 
             // No else if here, as a Lover of team Jackal needs the colors
index bc6cfbd00ac172ddbdfc8f23f98b9cbc192598bc..083ef96c92583baa117ee75cc2626de50be5bf4e 100644 (file)
@@ -29,6 +29,8 @@ namespace TheOtherRoles.Patches {
                 roleCouldUse = true;
             else if (Spy.canEnterVents && Spy.spy != null && Spy.spy == @object)
                 roleCouldUse = true;
+            else if (Vulture.canUseVents && Vulture.vulture != null && Vulture.vulture == @object)
+                roleCouldUse = true;
             else if (pc.IsImpostor) {
                 if (Janitor.janitor != null && Janitor.janitor == PlayerControl.LocalPlayer)
                     roleCouldUse = false;
index 7f361c037663ca83006ed45735f7ca6bab4f4f30..2d974fb5fb5724389036ed39eb58f07f86ee80b7 100644 (file)
@@ -48,6 +48,8 @@ namespace TheOtherRoles
         Guesser,
         BountyHunter,
         Bait,
+        Vulture,
+        Medium,
         Crewmate,
         Impostor
     }
@@ -97,7 +99,8 @@ namespace TheOtherRoles
         PlaceCamera,
         SealVent,
         ArsonistWin,
-        GuesserShoot
+        GuesserShoot,
+        VultureWin
     }
 
     public static class RPCProcedure {
@@ -237,6 +240,12 @@ namespace TheOtherRoles
                     case RoleId.Bait:
                         Bait.bait = player;
                         break;
+                    case RoleId.Vulture:
+                        Vulture.vulture = player;
+                        break;
+                    case RoleId.Medium:
+                        Medium.medium = player;
+                        break;
                     }
                 }
         }
@@ -293,8 +302,9 @@ namespace TheOtherRoles
         public static void cleanBody(byte playerId) {
             DeadBody[] array = UnityEngine.Object.FindObjectsOfType<DeadBody>();
             for (int i = 0; i < array.Length; i++) {
-                if (GameData.Instance.GetPlayerById(array[i].ParentId).PlayerId == playerId)
+                if (GameData.Instance.GetPlayerById(array[i].ParentId).PlayerId == playerId) {
                     UnityEngine.Object.Destroy(array[i].gameObject);
+                }     
             }
         }
 
@@ -371,7 +381,7 @@ namespace TheOtherRoles
             Shifter.clearAndReload();
 
             // Suicide (exile) when impostor or impostor variants
-            if (player.Data.IsImpostor || player == Jackal.jackal || player == Sidekick.sidekick || Jackal.formerJackals.Contains(player) || player == Jester.jester || player == Arsonist.arsonist) {
+            if (player.Data.IsImpostor || player == Jackal.jackal || player == Sidekick.sidekick || Jackal.formerJackals.Contains(player) || player == Jester.jester || player == Arsonist.arsonist || player == Vulture.vulture) {
                 oldShifter.Exiled();
                 return;
             }
@@ -424,9 +434,14 @@ namespace TheOtherRoles
                 SecurityGuard.securityGuard = oldShifter;
             if (Guesser.guesser != null && Guesser.guesser == player)
                 Guesser.guesser = oldShifter;
-            if (Bait.bait != null && Bait.bait == player)
+            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;
+
             // Set cooldowns to max for both players
             if (PlayerControl.LocalPlayer == oldShifter || PlayerControl.LocalPlayer == player)
                 CustomButton.ResetAllCooldowns();
@@ -558,6 +573,7 @@ namespace TheOtherRoles
             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
             if (player == Morphling.morphling) Morphling.clearAndReload();
@@ -587,6 +603,7 @@ namespace TheOtherRoles
             }
             if (player == Sidekick.sidekick) Sidekick.clearAndReload();
             if (player == BountyHunter.bountyHunter) BountyHunter.clearAndReload();
+            if (player == Vulture.vulture) Vulture.clearAndReload();
         }
 
         public static void setFutureErased(byte playerId) {
@@ -679,6 +696,10 @@ namespace TheOtherRoles
             Arsonist.triggerArsonistWin = true;
         }
 
+        public static void vultureWin() {
+            Vulture.triggerVultureWin = true;
+        }
+
         public static void guesserShoot(byte playerId) {
             PlayerControl target = Helpers.playerById(playerId);
             if (target == null) return;
@@ -865,6 +886,9 @@ namespace TheOtherRoles
                 case (byte)CustomRPC.GuesserShoot:
                     RPCProcedure.guesserShoot(reader.ReadByte());
                     break;
+                case (byte)CustomRPC.VultureWin:
+                    RPCProcedure.vultureWin();
+                    break;
             }
         }
     }
diff --git a/TheOtherRoles/Resources/MediumButton.png b/TheOtherRoles/Resources/MediumButton.png
new file mode 100644 (file)
index 0000000..13e64a4
Binary files /dev/null and b/TheOtherRoles/Resources/MediumButton.png differ
diff --git a/TheOtherRoles/Resources/VultureButton.png b/TheOtherRoles/Resources/VultureButton.png
new file mode 100644 (file)
index 0000000..42e8e2d
Binary files /dev/null and b/TheOtherRoles/Resources/VultureButton.png differ
index afe05d470b4ebe88db1bda26b79474a0dc558a0c..b2d8679d075cea72f8fbb75f991a5036ebe24ab1 100644 (file)
@@ -57,6 +57,8 @@ namespace TheOtherRoles
         public static RoleInfo goodGuesser = new RoleInfo("Nice Guesser", Guesser.color, "Guess and shoot", "Guess and shoot", RoleId.Guesser);
         public static RoleInfo badGuesser = new RoleInfo("Evil Guesser", Palette.ImpostorRed, "Guess and shoot", "Guess and shoot", RoleId.Guesser);
         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);
+        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 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);
@@ -83,6 +85,7 @@ namespace TheOtherRoles
             arsonist,
             jackal,
             sidekick,
+            vulture,
             crewmate,
             shifter,
             mayor,
@@ -99,8 +102,8 @@ namespace TheOtherRoles
             snitch,
             spy,
             securityGuard,
-            bountyHunter,
-            bait
+            bait,
+            medium
         };
 
         public static List<RoleInfo> getRoleInfoForPlayer(PlayerControl p) {
@@ -141,6 +144,8 @@ namespace TheOtherRoles
             if (p == Guesser.guesser) infos.Add(p.Data.IsImpostor ? badGuesser : goodGuesser);
             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);
 
             // Default roles
             if (infos.Count == 0 && p.Data.IsImpostor) infos.Add(impostor); // Just Impostor
@@ -151,5 +156,12 @@ namespace TheOtherRoles
 
             return infos;
         }
+
+        public static String GetRole(PlayerControl p) {
+            string roleName;
+            roleName = String.Join("", getRoleInfoForPlayer(p).Select(x => x.name).ToArray());
+            if (roleName.Contains("Lover")) roleName.Replace("Lover", "");
+            return roleName;
+        }
     }
 }
index 53c697207996ad5eeab9f9eee47b87dfc9e6c48a..400e2494e0054dbf07a5b3258ae6bcabad338082 100644 (file)
@@ -54,6 +54,8 @@ namespace TheOtherRoles
             Guesser.clearAndReload();
             BountyHunter.clearAndReload();
             Bait.clearAndReload();
+            Vulture.clearAndReload();
+            Medium.clearAndReload();
         }
 
         public static class Jester {
@@ -651,6 +653,7 @@ namespace TheOtherRoles
         public static bool jackalPromotedFromSidekickCanCreateSidekick = true;
         public static bool canCreateSidekickFromImpostor = true;
         public static bool hasImpostorVision = false;
+        public static bool canSeeEngineerVent = false;
 
         public static Sprite getSidekickButtonSprite() {
             if (buttonSprite) return buttonSprite;
@@ -679,6 +682,7 @@ namespace TheOtherRoles
             canCreateSidekickFromImpostor = CustomOptionHolder.jackalCanCreateSidekickFromImpostor.getBool();
             formerJackals.Clear();
             hasImpostorVision = CustomOptionHolder.jackalAndSidekickHaveImpostorVision.getBool();
+            canSeeEngineerVent = CustomOptionHolder.jackalCanSeeEngineerVent.getBool();
         }
         
     }
@@ -957,6 +961,7 @@ namespace TheOtherRoles
         private static Sprite targetSprite;
 
         public static int remainingShots = 2;
+        public static bool hasMultipleShotsPerMeeting = false;
 
         public static Sprite getTargetSprite() {
             if (targetSprite) return targetSprite;
@@ -968,6 +973,7 @@ namespace TheOtherRoles
             guesser = null;
             
             remainingShots = Mathf.RoundToInt(CustomOptionHolder.guesserNumberOfShots.getFloat());
+            hasMultipleShotsPerMeeting = CustomOptionHolder.guesserHasMultipleShotsPerMeeting.getBool();
         }
     }
 
@@ -1026,4 +1032,79 @@ namespace TheOtherRoles
             reportDelay = CustomOptionHolder.baitReportDelay.getFloat();
         }
     }
+
+    public static class Vulture {
+        public static PlayerControl vulture;
+        public static Color color = new Color32(139, 69, 19, byte.MaxValue);
+        public static List<Arrow> localArrows = new List<Arrow>();
+        public static float cooldown = 30f;
+        public static int vultureNumberToWin = 4;
+        public static int eatenBodies = 0;
+        public static bool triggerVultureWin = false;
+        public static bool canUseVents = false;
+        private static Sprite buttonSprite;
+        public static Sprite getButtonSprite() {
+            if (buttonSprite) return buttonSprite;
+            buttonSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.VultureButton.png", 115f);
+            return buttonSprite;
+        }
+
+        public static void clearAndReload() {
+            vulture = null;
+            vultureNumberToWin = Mathf.RoundToInt(CustomOptionHolder.vultureNumberToWin.getFloat());
+            eatenBodies = 0;
+            cooldown = CustomOptionHolder.vultureCooldown.getFloat();
+            triggerVultureWin = false;
+            canUseVents = CustomOptionHolder.vultureCanUseVents.getBool();
+            if (localArrows != null) {
+                foreach (Arrow arrow in localArrows)
+                    if (arrow?.arrow != null)
+                        UnityEngine.Object.Destroy(arrow.arrow);
+            }
+            localArrows = new List<Arrow>();
+        }
+    }
+
+
+    public static class Medium {
+        public static PlayerControl medium;
+        public static DeadPlayer target;
+        public static DeadPlayer soulTarget;
+        public static Color color = new Color32(98, 120, 115, byte.MaxValue);
+        public static List<Tuple<DeadPlayer, Vector3>> deadBodies = new List<Tuple<DeadPlayer, Vector3>>();
+        public static List<Tuple<DeadPlayer, Vector3>> featureDeadBodies = new List<Tuple<DeadPlayer, Vector3>>();
+        public static List<SpriteRenderer> souls = new List<SpriteRenderer>();
+        public static DateTime meetingStartTime = DateTime.UtcNow;
+
+        public static float cooldown = 30f;
+        public static float duration = 3f;
+        public static bool oneTimeUse = false;
+
+        private static Sprite soulSprite;
+        public static Sprite getSoulSprite() {
+            if (soulSprite) return soulSprite;
+            soulSprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.Soul.png", 500f);
+            return soulSprite;
+        }
+
+        private static Sprite question;
+        public static Sprite getQuestionSprite() {
+            if (question) return question;
+            question = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.MediumButton.png", 115f);
+            return question;
+        }
+
+        public static void clearAndReload() {
+            medium = null;
+            target = null;
+            soulTarget = null;
+            deadBodies = new List<Tuple<DeadPlayer, Vector3>>();
+            featureDeadBodies = new List<Tuple<DeadPlayer, Vector3>>();
+            souls = new List<SpriteRenderer>();
+            meetingStartTime = DateTime.UtcNow;
+            cooldown = CustomOptionHolder.mediumCooldown.getFloat();
+            duration = CustomOptionHolder.mediumDuration.getFloat();
+            oneTimeUse = CustomOptionHolder.mediumOneTimeUse.getBool();
+        }
+    }
 }
\ No newline at end of file
index c80314bfc141b1ba356310c209bd206ca1a74b1c..87a6e8615c53303f7cad258df770560988146dbd 100644 (file)
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <TargetFramework>netstandard2.1</TargetFramework>
-        <Version>2.8.1</Version>
+        <Version>2.9.0</Version>
         <Description>TheOtherRoles</Description>
         <Authors>Eisbison</Authors>
     </PropertyGroup>
     </ItemGroup>
 
     <ItemGroup>
-        <Reference Include="$(AmongUs)/BepInEx/core/*.dll"/>
-        <Reference Include="$(AmongUs)/BepInEx/unhollowed/*.dll"/>
+        <Reference Include="$(AmongUs)/BepInEx/core/*.dll" />
+        <Reference Include="$(AmongUs)/BepInEx/unhollowed/*.dll" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <None Remove="Resources\MediumButton.png" />
+      <None Remove="Resources\VultureButton.png" />
     </ItemGroup>
 
     <Target Name="CopyCustomContent" AfterTargets="AfterBuild">