# Releases
| Among Us - Version| Mod Version | Link |
|----------|-------------|-----------------|
+| 2023.03.28s| v4.3.2| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.2/TheOtherRoles.zip)
| 2023.03.28s| v4.3.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.1/TheOtherRoles.zip)
| 2023.02.28s| v4.3.0| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.3.0/TheOtherRoles.zip)
| 2022.12.14s| v4.2.1| [Download](https://github.com/Eisbison/TheOtherRoles/releases/download/v4.2.1/TheOtherRoles.zip)
# Changelog
<details>
<summary>Click to show the Changelog</summary>
+
+**Version 4.3.2**
+- Added new option to Thief: Can Guess To Steal Role
+- Added new option to Guesser Mode: Force Thief Guesser
+- Added the death reason to the additional infos for ghosts.
+- Added the possibility to copy and paste the settings for the current preset to / from the clipboard (buttons)
+- Added a feature to the swapper: The swap resets if one of the target players is guessed or disconnects
+- Fixed the Security Guard Mobile Cams and the Hacker Vitals Panel when playing on LevelImposter maps (don't rename the panels for now, please)
+- Fixed several bugs in Vanilla Hide'n'Seek
+- Fixed an issue where Crewmate "Fill" did not work correcty if min neutrals was set bigger than max neutrals
+- Fixed a bug where the colorblind-text of the ninja remained visible when using the ninja-ability
+- Fixed a bug where the bomb could explode in a meeting due to desync and bad timing
+- Fixed a bug where the host Eraser could guess erased players
**Version 4.3.1**
-- Update to Among Us 2023.3.28
- Added new options (Information mode - Chat/Map/Both, Roles - Evil Killing / Evil) to the snitch and fixed bugs affecting the snitch
- Changed the maximum amount per modifier to 15 (where applicable)
- Changed the colorblind text and player name to be behind certain objects again (rock on Polus etc.)
- If the option "Thief Can Kill Sheriff" is On, the Thief has tasks which will ONLY begin to count, if they kill
the Sheriff. While the Thief hasn't fired, their tasks do not count towards the taskwin.
- If the option "Thief Can Kill Sheriff" is Off, the Thief will not have tasks.
+<<<<<<< HEAD
+=======
+- If the Thief kills the witch, already witched players stay witched (except for the Thief).
+- If the Thief can guess to steal the role, guessing the witch will either save all targets or none of the targets (depending on the setting for Witch: Voting The Witch Saves All The Targets)
+>>>>>>> dev/main
### Game Options
| Name | Description |
| Thief Can Kill Sheriff | -
| Thief Has Impostor Vision | -
| Thief Can Use Vents | -
+<<<<<<< HEAD
+=======
+| Thief Can Guess To Steal Role | -
+>>>>>>> dev/main
-----------------------
## Mayor
| Number of Neutral Guessers | -
| Number of Impostor Guessers | -
| Force Jackal Guesser | If set to "On", the first neutral role who will be Guesser is the Jackal.
+<<<<<<< HEAD
+=======
+| Force Thief Guesser | If set to "On", the first (or second if Force Jackal Guesser) neutral role who will be Guesser is the Thief.
+>>>>>>> dev/main
| Guessers Can Have A Modifier | -
| Guesser Number Of Shots | -
| Guesser Can Shoot Multiple Times Per Meeting | -
medicShieldButton = new CustomButton(
() => {
medicShieldButton.Timer = 0f;
-
+
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, Medic.setShieldAfterMeeting ? (byte)CustomRPC.SetFutureShielded : (byte)CustomRPC.MedicSetShielded, Hazel.SendOption.Reliable, -1);
writer.Write(Medic.currentTarget.PlayerId);
AmongUsClient.Instance.FinishRpcImmediately(writer);
Medic.meetingAfterShielding = false;
SoundEffectsManager.play("medicShield");
- },
+ },
() => { return Medic.medic != null && Medic.medic == CachedPlayer.LocalPlayer.PlayerControl && !CachedPlayer.LocalPlayer.Data.IsDead; },
() => { return !Medic.usedShield && Medic.currentTarget && CachedPlayer.LocalPlayer.PlayerControl.CanMove; },
() => {},
() => {
if (GameOptionsManager.Instance.currentNormalGameOptions.MapId != 1) {
if (Hacker.vitals == null) {
- var e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("panel_vitals"));
+ var e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("panel_vitals") || x.gameObject.name.Contains("Vitals"));
if (e == null || Camera.main == null) return;
Hacker.vitals = UnityEngine.Object.Instantiate(e.MinigamePrefab, Camera.main.transform, false);
}
if (GameOptionsManager.Instance.currentNormalGameOptions.MapId != 1) {
if (SecurityGuard.minigame == null) {
byte mapId = GameOptionsManager.Instance.currentNormalGameOptions.MapId;
- var e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("Surv_Panel"));
+ var e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("Surv_Panel") || x.name.Contains("Cam"));
if (mapId == 0 || mapId == 3) e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("SurvConsole"));
else if (mapId == 4) e = UnityEngine.Object.FindObjectsOfType<SystemConsole>().FirstOrDefault(x => x.gameObject.name.Contains("task_cams"));
if (e == null || Camera.main == null) return;
public static CustomOption thiefHasImpVision;
public static CustomOption thiefCanUseVents;
public static CustomOption thiefCanKillSheriff;
+ public static CustomOption thiefCanStealWithGuess;
public static CustomOption trapperSpawnRate;
public static CustomOption guesserGamemodeNeutralNumber;
public static CustomOption guesserGamemodeImpNumber;
public static CustomOption guesserForceJackalGuesser;
+ public static CustomOption guesserForceThiefGuesser;
public static CustomOption guesserGamemodeHaveModifier;
public static CustomOption guesserGamemodeNumberOfShots;
public static CustomOption guesserGamemodeHasMultipleShotsPerMeeting;
thiefCanKillSheriff = CustomOption.Create(402, Types.Neutral, "Thief Can Kill Sheriff", true, thiefSpawnRate);
thiefHasImpVision = CustomOption.Create(403, Types.Neutral, "Thief Has Impostor Vision", true, thiefSpawnRate);
thiefCanUseVents = CustomOption.Create(404, Types.Neutral, "Thief Can Use Vents", true, thiefSpawnRate);
+ thiefCanStealWithGuess = CustomOption.Create(405, Types.Neutral, "Thief Can Guess To Steal A Role (If Guesser)", false, thiefSpawnRate);
trapperSpawnRate = CustomOption.Create(410, Types.Crewmate, cs(Trapper.color, "Trapper"), rates, null, true);
trapperCooldown = CustomOption.Create(420, Types.Crewmate, "Trapper Cooldown", 30f, 5f, 120f, 5f, trapperSpawnRate);
guesserGamemodeNeutralNumber = CustomOption.Create(2002, Types.Guesser, cs(Guesser.color, "Number of Neutral Guessers"), 15f, 1f, 15f, 1f, null, true);
guesserGamemodeImpNumber = CustomOption.Create(2003, Types.Guesser, cs(Guesser.color, "Number of Impostor Guessers"), 15f, 1f, 15f, 1f, null, true);
guesserForceJackalGuesser = CustomOption.Create(2007, Types.Guesser, "Force Jackal Guesser", false, null, true);
+ guesserForceThiefGuesser = CustomOption.Create(2011, Types.Guesser, "Force Thief Guesser", false, null, true);
guesserGamemodeHaveModifier = CustomOption.Create(2004, Types.Guesser, "Guessers Can Have A Modifier", true, null);
guesserGamemodeNumberOfShots = CustomOption.Create(2005, Types.Guesser, "Guesser Number Of Shots", 3f, 1f, 15f, 1f, null);
guesserGamemodeHasMultipleShotsPerMeeting = CustomOption.Create(2006, Types.Guesser, "Guesser Can Shoot Multiple Times Per Meeting", false, null);
using System.Collections.Generic;
using System;
using UnityEngine;
+using System.Linq;
namespace TheOtherRoles {
public class DeadPlayer
{
+ public enum CustomDeathReason {
+ Exile,
+ Kill,
+ Disconnect,
+ Guess,
+ Shift,
+ LawyerSuicide,
+ LoverSuicide, // not necessary
+ WitchExile,
+ Bomb
+ };
+
public PlayerControl player;
public DateTime timeOfDeath;
- public DeathReason deathReason;
+ public CustomDeathReason deathReason;
public PlayerControl killerIfExisting;
public bool wasCleaned;
- public DeadPlayer(PlayerControl player, DateTime timeOfDeath, DeathReason deathReason, PlayerControl killerIfExisting) {
+ public DeadPlayer(PlayerControl player, DateTime timeOfDeath, CustomDeathReason deathReason, PlayerControl killerIfExisting) {
this.player = player;
this.timeOfDeath = timeOfDeath;
this.deathReason = deathReason;
this.killerIfExisting = killerIfExisting;
this.wasCleaned = false;
}
+
}
static class GameHistory {
localPlayerPositions = new List<Tuple<Vector3, bool>>();
deadPlayers = new List<DeadPlayer>();
}
+
+ public static void overrideDeathReasonAndKiller(PlayerControl player, DeadPlayer.CustomDeathReason deathReason, PlayerControl killer = null) {
+ var target = deadPlayers.FirstOrDefault(x => x.player.PlayerId == player.PlayerId);
+ if (target != null) {
+ target.deathReason = deathReason;
+ if (killer != null) {
+ target.killerIfExisting = killer;
+ }
+ } else if (player != null) { // Create dead player if needed:
+ var dp = new DeadPlayer(player, DateTime.UtcNow, deathReason, killer);
+ deadPlayers.Add(dp);
+ }
+ }
}
}
\ No newline at end of file
using System.Net;
using TheOtherRoles.CustomGameModes;
using Reactor.Utilities.Extensions;
+using AmongUs.GameOptions;
namespace TheOtherRoles {
}
public static bool shouldShowGhostInfo() {
- return CachedPlayer.LocalPlayer.PlayerControl != null && CachedPlayer.LocalPlayer.PlayerControl.Data.IsDead && TORMapOptions.ghostsSeeInformation;
+ return CachedPlayer.LocalPlayer.PlayerControl != null && CachedPlayer.LocalPlayer.PlayerControl.Data.IsDead && TORMapOptions.ghostsSeeInformation || AmongUsClient.Instance.GameState == InnerNet.InnerNetClient.GameStates.Ended;
}
public static void clearAllTasks(this PlayerControl player) {
if (killer == null || killer.Data == null || (killer.Data.IsDead && !ignoreIfKillerIsDead) || killer.Data.Disconnected) return MurderAttemptResult.SuppressKill; // Allow non Impostor kills compared to vanilla code
if (target == null || target.Data == null || target.Data.IsDead || target.Data.Disconnected) return MurderAttemptResult.SuppressKill; // Allow killing players in vents compared to vanilla code
+ if (GameOptionsManager.Instance.currentGameOptions.GameMode == GameModes.HideNSeek) return MurderAttemptResult.PerformKill;
+
// Handle first kill attempt
if (TORMapOptions.shieldFirstKill && TORMapOptions.firstKillPlayer == target) return MurderAttemptResult.SuppressKill;
}
// Thief if hit crew only kill if setting says so, but also kill the thief.
- else if (killer == Thief.thief && !target.Data.Role.IsImpostor && !new List<RoleInfo> {RoleInfo.jackal, Thief.canKillSheriff ? RoleInfo.sheriff : null, RoleInfo.sidekick }.Contains(targetRole)) {
+ else if (Thief.isFailedThiefKill(target, killer, targetRole)) {
Thief.suicideFlag = true;
return MurderAttemptResult.SuppressKill;
}
public class TheOtherRolesPlugin : BasePlugin
{
public const string Id = "me.eisbison.theotherroles";
- public const string VersionString = "4.3.1";
+ public const string VersionString = "4.3.2";
public static uint betaDays = 0; // amount of days for the build to be usable (0 for infinite!)
public static Version Version = Version.Parse(VersionString);
using UnityEngine;
using BepInEx.Configuration;
using System;
+using System.IO;
using System.Linq;
using HarmonyLib;
using Hazel;
ShareOptionSelections();// Share all selections
}
}
+
+ public static byte[] serializeOptions() {
+ using (MemoryStream memoryStream = new MemoryStream()) {
+ using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream)) {
+ int lastId = -1;
+ foreach (var option in CustomOption.options.OrderBy(x => x.id)) {
+ if (option.id == 0) continue;
+ bool consecutive = lastId + 1 == option.id;
+ lastId = option.id;
+
+ binaryWriter.Write((byte)(option.selection + (consecutive ? 128 : 0)));
+ if (!consecutive) binaryWriter.Write((ushort)option.id);
+ }
+ binaryWriter.Flush();
+ memoryStream.Position = 0L;
+ return memoryStream.ToArray();
+ }
+ }
+ }
+
+ public static void deserializeOptions(byte[] inputValues) {
+ BinaryReader reader = new BinaryReader(new MemoryStream(inputValues));
+ int lastId = -1;
+ while (reader.BaseStream.Position < inputValues.Length) {
+ try {
+ int selection = reader.ReadByte();
+ int id = -1;
+ bool consecutive = selection >= 128;
+ if (consecutive) {
+ selection -= 128;
+ id = lastId + 1;
+ } else {
+ id = reader.ReadUInt16();
+ }
+ if (id == 0) continue;
+ lastId = id;
+ CustomOption option = CustomOption.options.First(option => option.id == id);
+ option.updateSelection(selection);
+ } catch (Exception e) {
+ TheOtherRolesPlugin.Logger.LogWarning($"{e}: while deserializing - tried to paste invalid settings!");
+ }
+ }
+ }
+
+ // Copy to or paste from clipboard (as string)
+ public static void copyToClipboard() {
+ GUIUtility.systemCopyBuffer = $"{TheOtherRolesPlugin.VersionString}!{Convert.ToBase64String(serializeOptions())}!{vanillaSettings.Value}";
+ }
+
+ public static bool pasteFromClipboard() {
+ string allSettings = GUIUtility.systemCopyBuffer;
+ try {
+ var settingsSplit = allSettings.Split("!");
+ string versionInfo = settingsSplit[0];
+ string torSettings = settingsSplit[1];
+ string vanillaSettingsSub = settingsSplit[2];
+ deserializeOptions(Convert.FromBase64String(torSettings));
+
+ vanillaSettings.Value = vanillaSettingsSub;
+ loadVanillaOptions();
+ return true;
+ } catch (Exception e) {
+ TheOtherRolesPlugin.Logger.LogWarning($"{e}: tried to paste invalid settings!");
+ SoundEffectsManager.Load();
+ SoundEffectsManager.play("fail");
+ return false;
+ }
+ }
}
[HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.Start))]
case CustomGamemodes.HideNSeek:
createHideNSeekTabs(__instance);
break;
- }
+ }
+
+ // create copy to clipboard and paste from clipboard buttons.
+ var template = GameObject.Find("CloseButton");
+ var copyButton = GameObject.Instantiate(template, template.transform.parent);
+ copyButton.transform.localPosition += Vector3.down * 0.8f;
+ var copyButtonPassive = copyButton.GetComponent<PassiveButton>();
+ var copyButtonRenderer = copyButton.GetComponent<SpriteRenderer>();
+ copyButtonRenderer.sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.CopyButton.png", 175f);
+ copyButtonPassive.OnClick.RemoveAllListeners();
+ copyButtonPassive.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
+ copyButtonPassive.OnClick.AddListener((System.Action)(() => {
+ copyToClipboard();
+ copyButtonRenderer.color = Color.green;
+ __instance.StartCoroutine(Effects.Lerp(1f, new System.Action<float>((p) => {
+ if (p > 0.95)
+ copyButtonRenderer.color = Color.white;
+ })));
+ }));
+ var pasteButton = GameObject.Instantiate(template, template.transform.parent);
+ pasteButton.transform.localPosition += Vector3.down * 1.6f;
+ var pasteButtonPassive = pasteButton.GetComponent<PassiveButton>();
+ var pasteButtonRenderer = pasteButton.GetComponent<SpriteRenderer>();
+ pasteButtonRenderer.sprite = Helpers.loadSpriteFromResources("TheOtherRoles.Resources.PasteButton.png", 175f);
+ pasteButtonPassive.OnClick.RemoveAllListeners();
+ pasteButtonPassive.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
+ pasteButtonPassive.OnClick.AddListener((System.Action)(() => {
+ pasteButtonRenderer.color = Color.yellow;
+ bool success = pasteFromClipboard();
+ pasteButtonRenderer.color = success ? Color.green : Color.red;
+ __instance.StartCoroutine(Effects.Lerp(1f, new System.Action<float>((p) => {
+ if (p > 0.95)
+ pasteButtonRenderer.color = Color.white;
+ })));
+ }));
+
}
private static void createClassicTabs(GameOptionsMenu __instance) {
string optionValue = "";
if (CustomOptionHolder.crewmateRolesFill.getBool()) {
var crewCount = PlayerControl.AllPlayerControls.Count - GameOptionsManager.Instance.currentGameOptions.NumImpostors;
- min = crewCount - CustomOptionHolder.neutralRolesCountMax.getSelection();
- max = crewCount - CustomOptionHolder.neutralRolesCountMin.getSelection();
+ int minNeutral = CustomOptionHolder.neutralRolesCountMin.getSelection();
+ int maxNeutral = CustomOptionHolder.neutralRolesCountMax.getSelection();
+ if (minNeutral > maxNeutral) minNeutral = maxNeutral;
+ min = crewCount - maxNeutral;
+ max = crewCount - minNeutral;
if (min < 0) min = 0;
if (max < 0) max = 0;
optionValue = "Fill: ";
var optionName = CustomOptionHolder.cs(new Color(204f / 255f, 204f / 255f, 0, 1f), "Impostor Roles");
var min = CustomOptionHolder.impostorRolesCountMin.getSelection();
var max = CustomOptionHolder.impostorRolesCountMax.getSelection();
+ if (max > GameOptionsManager.Instance.currentGameOptions.NumImpostors) max = GameOptionsManager.Instance.currentGameOptions.NumImpostors;
if (min > max) min = max;
var optionValue = (min == max) ? $"{max}" : $"{min} - {max}";
sb.AppendLine($"{optionName}: {optionValue}");
toggleSettingsButtonObject.transform.localPosition = __instance.MapButton.transform.localPosition + new Vector3(0, -0.66f, -500f);
}
}
-}
+}
\ No newline at end of file
var distance = Vector2.Distance(position, CachedPlayer.LocalPlayer.transform.position); // every player only checks that for their own client (desynct with positions sucks)
if (distance < Bomber.destructionRange && !CachedPlayer.LocalPlayer.Data.IsDead) {
Helpers.checkMurderAttemptAndKill(Bomber.bomber, CachedPlayer.LocalPlayer.PlayerControl, false, false, true, true);
+
+ MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.ShareGhostInfo, Hazel.SendOption.Reliable, -1);
+ writer.Write(CachedPlayer.LocalPlayer.PlayerId);
+ writer.Write((byte)RPCProcedure.GhostInfoTypes.DeathReasonAndKiller);
+ writer.Write(CachedPlayer.LocalPlayer.PlayerId);
+ writer.Write((byte)DeadPlayer.CustomDeathReason.Bomb);
+ writer.Write(Bomber.bomber.PlayerId);
+ AmongUsClient.Instance.FinishRpcImmediately(writer);
+ GameHistory.overrideDeathReasonAndKiller(CachedPlayer.LocalPlayer, DeadPlayer.CustomDeathReason.Bomb, killer: Bomber.bomber);
}
SoundEffectsManager.playAtPosition("bombExplosion", position, range: Bomber.hearRange) ;
}
}
Bomber.bomb.background.transform.Rotate(Vector3.forward * 50 * Time.fixedDeltaTime);
+ if (MeetingHud.Instance && Bomber.bomb != null) {
+ Bomber.clearBomb();
+ }
+
if (Vector2.Distance(CachedPlayer.LocalPlayer.PlayerControl.GetTruePosition(), Bomber.bomb.bomb.transform.position) > 1f) canDefuse = false;
else canDefuse = true;
}
backgroundSprite = null;
}
}
-}
\ No newline at end of file
+}
internal class PlayerRoleInfo {
public string PlayerName { get; set; }
public List<RoleInfo> Roles {get;set;}
+ public string RoleNames { get; set; }
public int TasksCompleted {get;set;}
public int TasksTotal {get;set;}
public bool IsGuesser {get; set;}
public int? Kills {get; set;}
+ public bool IsAlive { get; set; }
}
}
if (killCount == 0 && !(new List<RoleInfo>() { RoleInfo.sheriff, RoleInfo.jackal, RoleInfo.sidekick, RoleInfo.thief }.Contains(RoleInfo.getRoleInfoForPlayer(playerControl, false).FirstOrDefault()) || playerControl.Data.Role.IsImpostor)) {
killCount = null;
}
- AdditionalTempData.playerRoles.Add(new AdditionalTempData.PlayerRoleInfo() { PlayerName = playerControl.Data.PlayerName, Roles = roles, TasksTotal = tasksTotal, TasksCompleted = tasksCompleted, IsGuesser = isGuesser, Kills = killCount });
+ string roleString = RoleInfo.GetRolesString(playerControl, true, true, false);
+ AdditionalTempData.playerRoles.Add(new AdditionalTempData.PlayerRoleInfo() { PlayerName = playerControl.Data.PlayerName, Roles = roles, RoleNames = roleString, TasksTotal = tasksTotal, TasksCompleted = tasksCompleted, IsGuesser = isGuesser, Kills = killCount, IsAlive = !playerControl.Data.IsDead });
}
// Remove Jester, Arsonist, Vulture, Jackal, former Jackals and Sidekick from winners (if they win, they'll be readded)
}
roleSummaryText.AppendLine("Players and roles at the end of the game:");
foreach(var data in AdditionalTempData.playerRoles) {
- var roles = string.Join(" ", data.Roles.Select(x => Helpers.cs(x.color, x.name)));
- if (data.IsGuesser) roles += " (Guesser)";
+ //var roles = string.Join(" ", data.Roles.Select(x => Helpers.cs(x.color, x.name)));
+ string roles = data.RoleNames;
+ //if (data.IsGuesser) roles += " (Guesser)";
var taskInfo = data.TasksTotal > 0 ? $" - <color=#FAD934FF>({data.TasksCompleted}/{data.TasksTotal})</color>" : "";
if (data.Kills != null) taskInfo += $" - <color=#FF0000FF>(Kills: {data.Kills})</color>";
- roleSummaryText.AppendLine($"{data.PlayerName} - {roles}{taskInfo}");
+ roleSummaryText.AppendLine($"{Helpers.cs(data.IsAlive ? Color.white : new Color(.7f,.7f,.7f), data.PlayerName)} - {roles}{taskInfo}");
}
TMPro.TMP_Text roleSummaryTextMesh = roleSummary.GetComponent<TMPro.TMP_Text>();
roleSummaryTextMesh.alignment = TMPro.TextAlignmentOptions.TopLeft;
[HarmonyPriority(Priority.First)]
class ExileControllerBeginPatch {
public static GameData.PlayerInfo lastExiled;
- public static void Prefix(ExileController __instance, [HarmonyArgument(0)]ref GameData.PlayerInfo exiled, [HarmonyArgument(1)]bool tie) {
+ public static void Prefix(ExileController __instance, [HarmonyArgument(0)] ref GameData.PlayerInfo exiled, [HarmonyArgument(1)] bool tie) {
lastExiled = exiled;
// Medic shield
writer.Write(target.PlayerId);
AmongUsClient.Instance.FinishRpcImmediately(writer);
RPCProcedure.erasePlayerRoles(target.PlayerId);
+ Eraser.alreadyErased.Add(target.PlayerId);
}
}
}
if ((witchDiesWithExiledLover || exiledIsWitch) && Witch.witchVoteSavesTargets) Witch.futureSpelled = new List<PlayerControl>();
foreach (PlayerControl target in Witch.futureSpelled) {
- if (target != null && !target.Data.IsDead && Helpers.checkMuderAttempt(Witch.witch, target, true) == MurderAttemptResult.PerformKill)
- {
+ if (target != null && !target.Data.IsDead && Helpers.checkMuderAttempt(Witch.witch, target, true) == MurderAttemptResult.PerformKill) {
if (exiled != null && Lawyer.lawyer != null && (target == Lawyer.lawyer || target == Lovers.otherLover(Lawyer.lawyer)) && Lawyer.target != null && Lawyer.isProsecutor && Lawyer.target.PlayerId == exiled.PlayerId)
continue;
- MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.UncheckedExilePlayer, Hazel.SendOption.Reliable, -1);
- writer.Write(target.PlayerId);
- AmongUsClient.Instance.FinishRpcImmediately(writer);
- RPCProcedure.uncheckedExilePlayer(target.PlayerId);
if (target == Lawyer.target && Lawyer.lawyer != null) {
MessageWriter writer2 = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.LawyerPromotesToPursuer, Hazel.SendOption.Reliable, -1);
AmongUsClient.Instance.FinishRpcImmediately(writer2);
RPCProcedure.lawyerPromotesToPursuer();
}
+ MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.UncheckedExilePlayer, Hazel.SendOption.Reliable, -1);
+ writer.Write(target.PlayerId);
+ AmongUsClient.Instance.FinishRpcImmediately(writer);
+ RPCProcedure.uncheckedExilePlayer(target.PlayerId);
+
+ MessageWriter writer3 = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.ShareGhostInfo, Hazel.SendOption.Reliable, -1);
+ writer3.Write(CachedPlayer.LocalPlayer.PlayerId);
+ writer3.Write((byte)RPCProcedure.GhostInfoTypes.DeathReasonAndKiller);
+ writer3.Write(target.PlayerId);
+ writer3.Write((byte)DeadPlayer.CustomDeathReason.WitchExile);
+ writer3.Write(Witch.witch.PlayerId);
+ AmongUsClient.Instance.FinishRpcImmediately(writer3);
+
+ GameHistory.overrideDeathReasonAndKiller(target, DeadPlayer.CustomDeathReason.WitchExile, killer: Witch.witch);
}
}
}
TORMapOptions.camerasToAdd = new List<SurvCamera>();
foreach (Vent vent in TORMapOptions.ventsToSeal) {
- PowerTools.SpriteAnim animator = vent.GetComponent<PowerTools.SpriteAnim>();
+ PowerTools.SpriteAnim animator = vent.GetComponent<PowerTools.SpriteAnim>();
animator?.Stop();
vent.EnterVentAnim = vent.ExitVentAnim = null;
vent.myRend.sprite = animator == null ? SecurityGuard.getStaticVentSealedSprite() : SecurityGuard.getAnimatedVentSealedSprite();
else if (exiled != null && Jester.jester != null && Jester.jester.PlayerId == exiled.PlayerId) {
Jester.triggerJesterWin = true;
}
- // Prosecutor win condition
- else if (exiled != null && Lawyer.lawyer != null && Lawyer.target != null && Lawyer.isProsecutor && Lawyer.target.PlayerId == exiled.PlayerId && !Lawyer.lawyer.Data.IsDead)
- Lawyer.triggerProsecutorWin = true;
// Reset custom button timers where necessary
var rend = soul.AddComponent<SpriteRenderer>();
soul.AddSubmergedComponent(SubmergedCompatibility.Classes.ElevatorMover);
rend.sprite = Seer.getSoulSprite();
-
- if(Seer.limitSoulDuration) {
+
+ if (Seer.limitSoulDuration) {
FastDestroyableSingleton<HudManager>.Instance.StartCoroutine(Effects.Lerp(Seer.soulDuration, new Action<float>((p) => {
if (rend != null) {
var tmp = rend.color;
tmp.a = Mathf.Clamp01(1 - p);
rend.color = tmp;
- }
+ }
if (p == 1f && rend != null && rend.gameObject != null) UnityEngine.Object.Destroy(rend.gameObject);
})));
}
}
// Deputy check Promotion, see if the sheriff still exists. The promotion will be after the meeting.
- if (Deputy.deputy != null)
- {
+ if (Deputy.deputy != null) {
PlayerControlFixedUpdatePatch.deputyCheckPromotion(isMeeting: true);
}
Medium.souls = new List<SpriteRenderer>();
}
- if (Medium.featureDeadBodies != null) {
- foreach ((DeadPlayer db, Vector3 ps) in Medium.featureDeadBodies) {
+ if (Medium.futureDeadBodies != null) {
+ foreach ((DeadPlayer db, Vector3 ps) in Medium.futureDeadBodies) {
GameObject s = new GameObject();
//s.transform.position = ps;
s.transform.position = new Vector3(ps.x, ps.y, ps.y / 1000 - 1f);
rend.sprite = Medium.getSoulSprite();
Medium.souls.Add(rend);
}
- Medium.deadBodies = Medium.featureDeadBodies;
- Medium.featureDeadBodies = new List<Tuple<DeadPlayer, Vector3>>();
+ Medium.deadBodies = Medium.futureDeadBodies;
+ Medium.futureDeadBodies = new List<Tuple<DeadPlayer, Vector3>>();
}
}
foreach (Trap trap in Trap.traps) trap.triggerable = false;
FastDestroyableSingleton<HudManager>.Instance.StartCoroutine(Effects.Lerp(GameOptionsManager.Instance.currentNormalGameOptions.KillCooldown / 2 + 2, new Action<float>((p) => {
- if (p == 1f) foreach (Trap trap in Trap.traps) trap.triggerable = true;
+ if (p == 1f) foreach (Trap trap in Trap.traps) trap.triggerable = true;
})));
}
}
[HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), new Type[] { typeof(StringNames), typeof(Il2CppReferenceArray<Il2CppSystem.Object>) })]
class ExileControllerMessagePatch {
- static void Postfix(ref string __result, [HarmonyArgument(0)]StringNames id) {
+ static void Postfix(ref string __result, [HarmonyArgument(0)] StringNames id) {
try {
if (ExileController.Instance != null && ExileController.Instance.exiled != null) {
PlayerControl player = Helpers.playerById(ExileController.Instance.exiled.Object.PlayerId);
}
}
}
-}
+}
\ No newline at end of file
[HarmonyPostfix]
[HarmonyPatch(typeof(AbilityButton), nameof(AbilityButton.Update))]
public static void showOrHideAbilityButtonPostfix(AbilityButton __instance) {
- if (CachedPlayer.LocalPlayer.Data.IsDead && CustomOptionHolder.finishTasksBeforeHauntingOrZoomingOut.getBool()) {
+ if (CachedPlayer.LocalPlayer.Data.IsDead && (CustomOptionHolder.finishTasksBeforeHauntingOrZoomingOut.getBool() || GameOptionsManager.Instance.currentGameOptions.GameMode == GameModes.HideNSeek)) {
// player has haunt button.
var (playerCompleted, playerTotal) = TasksHandler.taskInfo(CachedPlayer.LocalPlayer.Data);
int numberOfLeftTasks = playerTotal - playerCompleted;
}
}
}
- HudManagerUpdate.CloseSettings();
- }
+ HudManagerUpdate.CloseSettings();
+ }
}
}
}
}
+ public static void swapperCheckAndReturnSwap(MeetingHud __instance, byte dyingPlayerId) {
+ // someone was guessed or dced in the meeting, check if this affects the swapper.
+ if (Swapper.swapper == null || __instance.state == MeetingHud.VoteStates.Results) return;
+
+ // reset swap.
+ bool reset = false;
+ if (dyingPlayerId == Swapper.playerId1 || dyingPlayerId == Swapper.playerId2) {
+ reset = true;
+ Swapper.playerId1 = Swapper.playerId2 = byte.MaxValue;
+ }
+
+
+ // Only for the swapper: Reset all the buttons and charges value to their original state.
+ if (CachedPlayer.LocalPlayer.PlayerControl != Swapper.swapper) return;
+
+
+ // check if dying player was a selected player (but not confirmed yet)
+ for (int i = 0; i < __instance.playerStates.Count; i++) {
+ reset = reset || selections[i] && __instance.playerStates[i].TargetPlayerId == dyingPlayerId;
+ if (reset) break;
+ }
+
+ if (!reset) return;
+
+
+ for (int i = 0; i < selections.Length; i++) {
+ selections[i] = false;
+ PlayerVoteArea playerVoteArea = __instance.playerStates[i];
+ if (playerVoteArea.AmDead || (playerVoteArea.TargetPlayerId == Swapper.swapper.PlayerId && Swapper.canOnlySwapOthers)) continue;
+ renderers[i].color = Color.red;
+ Swapper.charges++;
+ int copyI = i;
+ swapperButtonList[i].OnClick.RemoveAllListeners();
+ swapperButtonList[i].OnClick.AddListener((System.Action)(() => swapperOnClick(copyI, __instance)));
+ }
+ meetingExtraButtonText.text = $"Swaps: {Swapper.charges}";
+ meetingExtraButtonLabel.text = Helpers.cs(Color.red, "Confirm Swap");
+
+ }
+
static void mayorToggleVoteTwice(MeetingHud __instance) {
__instance.playerStates[0].Cancel(); // This will stop the underlying buttons of the template from showing up
if (__instance.state == MeetingHud.VoteStates.Results || Mayor.mayor.Data.IsDead) return;
else message += p.Data.PlayerName + "\n";
}
FastDestroyableSingleton<HudManager>.Instance.Chat.AddChat(Trapper.trapper, $"{message}");
+
}
}
}
}
+
if (CachedPlayer.LocalPlayer.Data.IsDead && output != "") FastDestroyableSingleton<HudManager>.Instance.Chat.AddChat(CachedPlayer.LocalPlayer, $"{output}");
Trapper.playersOnMap = new List<PlayerControl>();
}
}
+
+
public static void trapperUpdate() {
if (Trapper.trapper == null || CachedPlayer.LocalPlayer.PlayerControl != Trapper.trapper || Trapper.trapper.Data.IsDead) return;
var (playerCompleted, _) = TasksHandler.taskInfo(Trapper.trapper.Data);
}
}
}
-
public static void Postfix(PlayerControl __instance) {
if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started || GameOptionsManager.Instance.currentGameOptions.GameMode == GameModes.HideNSeek) return;
public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)]PlayerControl target)
{
// Collect dead player info
- DeadPlayer deadPlayer = new DeadPlayer(target, DateTime.UtcNow, DeathReason.Kill, __instance);
+ DeadPlayer deadPlayer = new DeadPlayer(target, DateTime.UtcNow, DeadPlayer.CustomDeathReason.Kill, __instance);
GameHistory.deadPlayers.Add(deadPlayer);
// Reset killer to crewmate if resetToCrewmate
PlayerControl otherLover = target == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1;
if (otherLover != null && !otherLover.Data.IsDead && Lovers.bothDie) {
otherLover.MurderPlayer(otherLover);
+ GameHistory.overrideDeathReasonAndKiller(otherLover, DeadPlayer.CustomDeathReason.LoverSuicide);
}
}
// Medium add body
if (Medium.deadBodies != null) {
- Medium.featureDeadBodies.Add(new Tuple<DeadPlayer, Vector3>(deadPlayer, target.transform.position));
+ Medium.futureDeadBodies.Add(new Tuple<DeadPlayer, Vector3>(deadPlayer, target.transform.position));
}
// Set bountyHunter cooldown
public static void Postfix(PlayerControl __instance)
{
// Collect dead player info
- DeadPlayer deadPlayer = new DeadPlayer(__instance, DateTime.UtcNow, DeathReason.Exile, null);
+ DeadPlayer deadPlayer = new DeadPlayer(__instance, DateTime.UtcNow, DeadPlayer.CustomDeathReason.Exile, null);
GameHistory.deadPlayers.Add(deadPlayer);
+
// Remove fake tasks when player dies
if (__instance.hasFakeTasks() || __instance == Lawyer.lawyer || __instance == Pursuer.pursuer || __instance == Thief.thief)
__instance.clearAllTasks();
// Lover suicide trigger on exile
if ((Lovers.lover1 != null && __instance == Lovers.lover1) || (Lovers.lover2 != null && __instance == Lovers.lover2)) {
PlayerControl otherLover = __instance == Lovers.lover1 ? Lovers.lover2 : Lovers.lover1;
- if (otherLover != null && !otherLover.Data.IsDead && Lovers.bothDie)
+ if (otherLover != null && !otherLover.Data.IsDead && Lovers.bothDie) {
otherLover.Exiled();
- }
-
+ GameHistory.overrideDeathReasonAndKiller(otherLover, DeadPlayer.CustomDeathReason.LoverSuicide);
+ }
+
+ }
// Sidekick promotion trigger on exile
if (Sidekick.promotesToJackal && Sidekick.sidekick != null && !Sidekick.sidekick.Data.IsDead && __instance == Jackal.jackal && Jackal.jackal == CachedPlayer.LocalPlayer.PlayerControl) {
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.SidekickPromotes, Hazel.SendOption.Reliable, -1);
// Pursuer promotion trigger on exile & suicide (the host sends the call such that everyone recieves the update before a possible game End)
if (Lawyer.lawyer != null && __instance == Lawyer.target) {
+ PlayerControl lawyer = Lawyer.lawyer;
if (AmongUsClient.Instance.AmHost && ((Lawyer.target != Jester.jester && !Lawyer.isProsecutor) || Lawyer.targetWasGuessed)) {
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.LawyerPromotesToPursuer, Hazel.SendOption.Reliable, -1);
AmongUsClient.Instance.FinishRpcImmediately(writer);
if (!Lawyer.targetWasGuessed && !Lawyer.isProsecutor) {
if (Lawyer.lawyer != null) Lawyer.lawyer.Exiled();
if (Pursuer.pursuer != null) Pursuer.pursuer.Exiled();
+
+ MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.ShareGhostInfo, Hazel.SendOption.Reliable, -1);
+ writer.Write(CachedPlayer.LocalPlayer.PlayerId);
+ writer.Write((byte)RPCProcedure.GhostInfoTypes.DeathReasonAndKiller);
+ writer.Write(lawyer.PlayerId);
+ writer.Write((byte)DeadPlayer.CustomDeathReason.LawyerSuicide);
+ writer.Write(lawyer.PlayerId);
+ AmongUsClient.Instance.FinishRpcImmediately(writer);
+ GameHistory.overrideDeathReasonAndKiller(lawyer, DeadPlayer.CustomDeathReason.LawyerSuicide, lawyer); // TODO: only executed on host?!
}
}
}
return false;
}
}
+
+ [HarmonyPatch(typeof(GameData), nameof(GameData.HandleDisconnect), new[] {typeof(PlayerControl), typeof(DisconnectReasons) })]
+ public static class GameDataHandleDisconnectPatch {
+ public static void Prefix(GameData __instance, PlayerControl player, DisconnectReasons reason) {
+ if (MeetingHud.Instance) {
+ MeetingHudPatch.swapperCheckAndReturnSwap(MeetingHud.Instance, player.PlayerId);
+ }
+ }
+ }
}
var impostorMin = CustomOptionHolder.impostorRolesCountMin.getSelection();
var impostorMax = CustomOptionHolder.impostorRolesCountMax.getSelection();
- // Automatically force everyone to get a role by setting crew Min / Max according to Neutral Settings
- if (CustomOptionHolder.crewmateRolesFill.getBool()) {
- crewmateMax = crewmates.Count - neutralMin;
- crewmateMin = crewmates.Count - neutralMax;
- }
-
// Make sure min is less or equal to max
if (crewmateMin > crewmateMax) crewmateMin = crewmateMax;
if (neutralMin > neutralMax) neutralMin = neutralMax;
if (impostorMin > impostorMax) impostorMin = impostorMax;
+ // Automatically force everyone to get a role by setting crew Min / Max according to Neutral Settings
+ if (CustomOptionHolder.crewmateRolesFill.getBool()) {
+ crewmateMax = crewmates.Count - neutralMin;
+ crewmateMin = crewmates.Count - neutralMax;
+ }
+
// Get the maximum allowed count of each role type based on the minimum and maximum option
int crewCountSettings = rnd.Next(crewmateMin, crewmateMax + 1);
int neutralCountSettings = rnd.Next(neutralMin, neutralMax + 1);
neutralPlayer.RemoveAll(x => !Helpers.isNeutral(x));
crewPlayer.RemoveAll(x => x.Data.Role.IsImpostor || Helpers.isNeutral(x));
assignGuesserGamemodeToPlayers(crewPlayer, Mathf.RoundToInt(CustomOptionHolder.guesserGamemodeCrewNumber.getFloat()));
- assignGuesserGamemodeToPlayers(neutralPlayer, Mathf.RoundToInt(CustomOptionHolder.guesserGamemodeNeutralNumber.getFloat()), CustomOptionHolder.guesserForceJackalGuesser.getBool());
+ assignGuesserGamemodeToPlayers(neutralPlayer, Mathf.RoundToInt(CustomOptionHolder.guesserGamemodeNeutralNumber.getFloat()), CustomOptionHolder.guesserForceJackalGuesser.getBool(), CustomOptionHolder.guesserForceThiefGuesser.getBool());
assignGuesserGamemodeToPlayers(impPlayer, Mathf.RoundToInt(CustomOptionHolder.guesserGamemodeImpNumber.getFloat()));
}
- private static void assignGuesserGamemodeToPlayers(List<PlayerControl> playerList, int count, bool forceJackal = false) {
+ private static void assignGuesserGamemodeToPlayers(List<PlayerControl> playerList, int count, bool forceJackal = false, bool forceThief = false) {
for (int i = 0; i < count && playerList.Count > 0; i++) {
var index = rnd.Next(0, playerList.Count);
+ if (forceThief && !forceJackal) {
+ if (Thief.thief != null)
+ index = playerList.FindIndex(x => x == Thief.thief);
+ forceThief = false;
+ }
if (forceJackal) {
if (Jackal.jackal != null)
index = playerList.FindIndex(x => x == Jackal.jackal);
using TheOtherRoles.CustomGameModes;
using AmongUs.Data;
using AmongUs.GameOptions;
+using UnityEngine.UIElements;
namespace TheOtherRoles
{
- enum RoleId {
+ public enum RoleId {
Jester,
Mayor,
Portalmaker,
}
public static void cleanBody(byte playerId, byte cleaningPlayerId) {
- if (Medium.featureDeadBodies != null) {
- var deadBody = Medium.featureDeadBodies.Find(x => x.Item1.player.PlayerId == playerId).Item1;
+ if (Medium.futureDeadBodies != null) {
+ var deadBody = Medium.futureDeadBodies.Find(x => x.Item1.player.PlayerId == playerId).Item1;
if (deadBody != null) deadBody.wasCleaned = true;
}
// Suicide (exile) when impostor or impostor variants
if (player.Data.Role.IsImpostor || Helpers.isNeutral(player)) {
oldShifter.Exiled();
+ GameHistory.overrideDeathReasonAndKiller(oldShifter, DeadPlayer.CustomDeathReason.Shift, player);
if (oldShifter == Lawyer.target && AmongUsClient.Instance.AmHost && Lawyer.lawyer != null) {
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(CachedPlayer.LocalPlayer.PlayerControl.NetId, (byte)CustomRPC.LawyerPromotesToPursuer, Hazel.SendOption.Reliable, -1);
AmongUsClient.Instance.FinishRpcImmediately(writer);
{
target.cosmetics.currentBodySprite.BodySprite.color = Color.white;
target.cosmetics.colorBlindText.gameObject.SetActive(DataManager.Settings.Accessibility.ColorBlindMode);
+ target.cosmetics.colorBlindText.color = target.cosmetics.colorBlindText.color.SetAlpha(1f);
+
if (Camouflager.camouflageTimer <= 0) target.setDefaultLook();
Ninja.isInvisble = false;
return;
}
target.setLook("", 6, "", "", "", "");
- Color color = Color.clear;
- if (CachedPlayer.LocalPlayer.Data.Role.IsImpostor || CachedPlayer.LocalPlayer.Data.IsDead) color.a = 0.1f;
+ Color color = Color.clear;
+ bool canSee = CachedPlayer.LocalPlayer.Data.Role.IsImpostor || CachedPlayer.LocalPlayer.Data.IsDead;
+ if (canSee) color.a = 0.1f;
target.cosmetics.currentBodySprite.BodySprite.color = color;
target.cosmetics.colorBlindText.gameObject.SetActive(false);
+ target.cosmetics.colorBlindText.color = target.cosmetics.colorBlindText.color.SetAlpha(canSee ? 0.1f : 0f);
Ninja.invisibleTimer = Ninja.invisibleDuration;
Ninja.isInvisble = true;
}
if (Lawyer.target != null && dyingTarget == Lawyer.target) Lawyer.targetWasGuessed = true; // Lawyer shouldn't be exiled with the client for guesses
PlayerControl dyingLoverPartner = Lovers.bothDie ? dyingTarget.getPartner() : null; // Lover check
if (Lawyer.target != null && dyingLoverPartner == Lawyer.target) Lawyer.targetWasGuessed = true; // Lawyer shouldn't be exiled with the client for guesses
+
+ PlayerControl guesser = Helpers.playerById(killerId);
+ if (Thief.thief != null && Thief.thief.PlayerId == killerId && Thief.canStealWithGuess) {
+ RoleInfo roleInfo = RoleInfo.allRoleInfos.FirstOrDefault(x => (byte)x.roleId == guessedRoleId);
+ if (!Thief.thief.Data.IsDead && !Thief.isFailedThiefKill(dyingTarget, guesser, roleInfo)) {
+ RPCProcedure.thiefStealsRole(dyingTarget.PlayerId);
+ }
+ }
+
dyingTarget.Exiled();
+ GameHistory.overrideDeathReasonAndKiller(dyingTarget, DeadPlayer.CustomDeathReason.Guess, guesser);
byte partnerId = dyingLoverPartner != null ? dyingLoverPartner.PlayerId : dyingTargetId;
HandleGuesser.remainingShots(killerId, true);
if (Constants.ShouldPlaySfx()) SoundManager.Instance.PlaySound(dyingTarget.KillSfx, false, 0.8f);
if (MeetingHud.Instance) {
+ MeetingHudPatch.swapperCheckAndReturnSwap(MeetingHud.Instance, dyingTargetId);
foreach (PlayerVoteArea pva in MeetingHud.Instance.playerStates) {
if (pva.TargetPlayerId == dyingTargetId || pva.TargetPlayerId == partnerId) {
pva.SetDead(pva.DidReport, true);
if (AmongUsClient.Instance.AmHost)
MeetingHud.Instance.CheckForEndVoting();
}
- PlayerControl guesser = Helpers.playerById(killerId);
if (FastDestroyableSingleton<HudManager>.Instance != null && guesser != null)
if (CachedPlayer.LocalPlayer.PlayerControl == dyingTarget) {
FastDestroyableSingleton<HudManager>.Instance.KillOverlay.ShowKillAnimation(guesser.Data, dyingTarget.Data);
}
}
-
+
PlayerControl guessedTarget = Helpers.playerById(guessedTargetId);
if (CachedPlayer.LocalPlayer.Data.IsDead && guessedTarget != null && guesser != null) {
RoleInfo roleInfo = RoleInfo.allRoleInfos.FirstOrDefault(x => (byte)x.roleId == guessedRoleId);
if (target == Cleaner.cleaner) Cleaner.cleaner = thief;
if (target == Warlock.warlock) Warlock.warlock = thief;
if (target == BountyHunter.bountyHunter) BountyHunter.bountyHunter = thief;
- if (target == Witch.witch) Witch.witch = thief;
+ if (target == Witch.witch) {
+ Witch.witch = thief;
+ if (MeetingHud.Instance)
+ if (Witch.witchVoteSavesTargets) // In a meeting, if the thief guesses the witch, all targets are saved or no target is saved.
+ Witch.futureSpelled = new();
+ else // If thief kills witch during the round, remove the thief from the list of spelled people, keep the rest
+ Witch.futureSpelled.RemoveAll(x => x.PlayerId == thief.PlayerId);
+ }
if (target == Ninja.ninja) Ninja.ninja = thief;
if (target == Bomber.bomber) Bomber.bomber = thief;
if (target.Data.Role.IsImpostor) {
BlankUsed,
DetectiveOrMedicInfo,
VampireTimer,
+ DeathReasonAndKiller,
}
public static void receiveGhostInfo (byte senderId, MessageReader reader) {
case GhostInfoTypes.VampireTimer:
HudManagerStartPatch.vampireKillButton.Timer = (float)reader.ReadByte();
break;
+ case GhostInfoTypes.DeathReasonAndKiller:
+ GameHistory.overrideDeathReasonAndKiller(Helpers.playerById(reader.ReadByte()), (DeadPlayer.CustomDeathReason)reader.ReadByte(), Helpers.playerById(reader.ReadByte()));
+ break;
}
}
namespace TheOtherRoles
{
- class RoleInfo {
+ public class RoleInfo {
public Color color;
public string name;
public string introDescription;
public bool isNeutral;
public bool isModifier;
- RoleInfo(string name, Color color, string introDescription, string shortDescription, RoleId roleId, bool isNeutral = false, bool isModifier = false) {
+ public RoleInfo(string name, Color color, string introDescription, string shortDescription, RoleId roleId, bool isNeutral = false, bool isModifier = false) {
this.color = color;
this.name = name;
this.introDescription = introDescription;
roleName = roleName + Helpers.cs(Arsonist.color, $" ({CachedPlayer.AllPlayers.Count(x => { return x.PlayerControl != Arsonist.arsonist && !x.Data.IsDead && !x.Data.Disconnected && !Arsonist.dousedPlayers.Any(y => y.PlayerId == x.PlayerId); })} left)");
if (p == Jackal.fakeSidekick)
roleName = Helpers.cs(Sidekick.color, $" (fake SK)") + roleName;
+ // Death Reason on Ghosts
+ if (p.Data.IsDead) {
+ string deathReasonString = "";
+ var deadPlayer = GameHistory.deadPlayers.FirstOrDefault(x => x.player.PlayerId == p.PlayerId);
+
+ Color killerColor = new();
+ if (deadPlayer != null && deadPlayer.killerIfExisting != null) {
+ killerColor = RoleInfo.getRoleInfoForPlayer(deadPlayer.killerIfExisting, false).FirstOrDefault().color;
+ }
+
+ if (deadPlayer != null) {
+ switch (deadPlayer.deathReason) {
+ case DeadPlayer.CustomDeathReason.Disconnect:
+ deathReasonString = " - disconnected";
+ break;
+ case DeadPlayer.CustomDeathReason.Exile:
+ deathReasonString = " - voted out";
+ break;
+ case DeadPlayer.CustomDeathReason.Kill:
+ deathReasonString = $" - killed by {Helpers.cs(killerColor, deadPlayer.killerIfExisting.Data.PlayerName)}";
+ break;
+ case DeadPlayer.CustomDeathReason.Guess:
+ if (deadPlayer.killerIfExisting.Data.PlayerName == p.Data.PlayerName)
+ deathReasonString = $" - failed guess";
+ else
+ deathReasonString = $" - guessed by {Helpers.cs(killerColor, deadPlayer.killerIfExisting.Data.PlayerName)}";
+ break;
+ case DeadPlayer.CustomDeathReason.Shift:
+ deathReasonString = $" - {Helpers.cs(Color.yellow, "shifted")} {Helpers.cs(killerColor, deadPlayer.killerIfExisting.Data.PlayerName)}";
+ break;
+ case DeadPlayer.CustomDeathReason.WitchExile:
+ deathReasonString = $" - {Helpers.cs(Witch.color, "witched")} by {Helpers.cs(killerColor, deadPlayer.killerIfExisting.Data.PlayerName)}";
+ break;
+ case DeadPlayer.CustomDeathReason.LoverSuicide:
+ deathReasonString = $" - {Helpers.cs(Lovers.color, "lover died")}";
+ break;
+ case DeadPlayer.CustomDeathReason.LawyerSuicide:
+ deathReasonString = $" - {Helpers.cs(Lawyer.color, "bad Lawyer")}";
+ break;
+ case DeadPlayer.CustomDeathReason.Bomb:
+ deathReasonString = $" - bombed by {Helpers.cs(killerColor, deadPlayer.killerIfExisting.Data.PlayerName)}";
+ break;
+ }
+ roleName = roleName + deathReasonString;
+ }
+ }
}
}
return roleName;
if (p == Ninja.ninja && Ninja.isInvisble)
continue;
p.setDefaultLook();
- }
+ }
}
public static void clearAndReload() {
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<Tuple<DeadPlayer, Vector3>> futureDeadBodies = new List<Tuple<DeadPlayer, Vector3>>();
public static List<SpriteRenderer> souls = new List<SpriteRenderer>();
public static DateTime meetingStartTime = DateTime.UtcNow;
target = null;
soulTarget = null;
deadBodies = new List<Tuple<DeadPlayer, Vector3>>();
- featureDeadBodies = new List<Tuple<DeadPlayer, Vector3>>();
+ futureDeadBodies = new List<Tuple<DeadPlayer, Vector3>>();
souls = new List<SpriteRenderer>();
meetingStartTime = DateTime.UtcNow;
cooldown = CustomOptionHolder.mediumCooldown.getFloat();
public static bool hasImpostorVision;
public static bool canUseVents;
public static bool canKillSheriff;
+ public static bool canStealWithGuess;
public static void clearAndReload() {
thief = null;
cooldown = CustomOptionHolder.thiefCooldown.getFloat();
canUseVents = CustomOptionHolder.thiefCanUseVents.getBool();
canKillSheriff = CustomOptionHolder.thiefCanKillSheriff.getBool();
+ canStealWithGuess = CustomOptionHolder.thiefCanStealWithGuess.getBool();
+ }
+
+ public static bool isFailedThiefKill(PlayerControl target, PlayerControl killer, RoleInfo targetRole) {
+ return killer == Thief.thief && !target.Data.Role.IsImpostor && !new List<RoleInfo> { RoleInfo.jackal, canKillSheriff ? RoleInfo.sheriff : null, RoleInfo.sidekick }.Contains(targetRole);
}
}
- public static class Trapper {
+ public static class Trapper {
public static PlayerControl trapper;
public static Color color = new Color32(110, 57, 105, byte.MaxValue);
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
- <Version>4.3.1</Version>
+ <Version>4.3.2</Version>
<Description>TheOtherRoles</Description>
<Authors>Eisbison</Authors>
<LangVersion>latest</LangVersion>
<ItemGroup>
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
-
- <ItemGroup>
+
+ <ItemGroup>
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2023.03.28" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.664" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.0.1" />