From f100926055311dd5d8cfe10cefe1500b6c5bd7ee Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Thu, 13 Aug 2026 13:49:47 +0200 Subject: [PATCH] Add Transposer --- CustomRoles.cs | 1 + Modules/CustomRolesHelper.cs | 4 + Patches/HudSpritePatch.cs | 1 + Patches/PlayerControlPatch.cs | 2 +- Resources/Lang/en_US.json | 3 + .../Standard/Crewmate/Support/Transporter.cs | 8 +- .../Impostor/Concealing/Fakeshifter.cs | 4 +- .../Impostor/Concealing/Transposer.cs | 134 ++++++++++++++++++ Roles/Standard/Impostor/Support/Swapster.cs | 13 +- 9 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 Roles/Standard/Impostor/Concealing/Transposer.cs diff --git a/CustomRoles.cs b/CustomRoles.cs index 180eee4a2..e00ea4312 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -118,6 +118,7 @@ public enum CustomRoles Swooper, Stealth, TimeThief, + Transposer, Trapster, Trickster, Twister, diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index bd06fe8dc..01ffc18c3 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -186,6 +186,7 @@ internal static class CustomRolesHelper CustomRoles.Freezer => CustomRoles.Shapeshifter, CustomRoles.Changeling => CustomRoles.Phantom, CustomRoles.Swapster => CustomRoles.Shapeshifter, + CustomRoles.Transposer => CustomRoles.Shapeshifter, CustomRoles.Kamikaze => CustomRoles.Impostor, CustomRoles.Librarian => CustomRoles.Shapeshifter, CustomRoles.Cantankerous => CustomRoles.Impostor, @@ -802,6 +803,7 @@ internal static class CustomRolesHelper CustomRoles.Freezer or CustomRoles.Changeling or CustomRoles.Swapster or + CustomRoles.Transposer or CustomRoles.Loner or CustomRoles.Kamikaze or CustomRoles.Librarian or @@ -1071,6 +1073,7 @@ internal static class CustomRolesHelper CustomRoles.Glitch or CustomRoles.Illusionist or CustomRoles.Swapster or + CustomRoles.Transposer or CustomRoles.Echo or CustomRoles.Hangman or CustomRoles.Generator; @@ -1313,6 +1316,7 @@ internal static class CustomRolesHelper CustomRoles.Swiftclaw => RoleOptionType.Impostor_Concealing, CustomRoles.Swooper => RoleOptionType.Impostor_Concealing, CustomRoles.Stealth => RoleOptionType.Impostor_Concealing, + CustomRoles.Transposer => RoleOptionType.Impostor_Concealing, CustomRoles.Trickster => RoleOptionType.Impostor_Concealing, CustomRoles.Undertaker => RoleOptionType.Impostor_Concealing, CustomRoles.Vampire => RoleOptionType.Impostor_Concealing, diff --git a/Patches/HudSpritePatch.cs b/Patches/HudSpritePatch.cs index 3de72dae3..744095fc4 100644 --- a/Patches/HudSpritePatch.cs +++ b/Patches/HudSpritePatch.cs @@ -570,6 +570,7 @@ public static class HudSpritePatch } case CustomRoles.Transporter when player.GetRoleTypes() == RoleTypes.Shapeshifter: case CustomRoles.Swapster: + case CustomRoles.Transposer: { newAbilityButton = CustomButton.Get("Transport"); break; diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index d58fe5d8f..3b753d70a 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -931,7 +931,7 @@ internal static class ShapeshiftPatch if (!shapeshifting && !shapeshifter.Is(CustomRoles.Glitch) && isSSneeded) LateTask.New(() => Main.Instance.StartCoroutine(NotifyEveryoneAsync()), 1.2f, "ShapeShiftNotify"); - if (!(shapeshifting && doSSwithoutAnim) && !isSSneeded && !Swapster.FirstSwapTarget.ContainsKey(shapeshifter.PlayerId) && !Transporter.FirstSwapTarget.ContainsKey(shapeshifter.PlayerId)) + if (!(shapeshifting && doSSwithoutAnim) && !isSSneeded && !Swapster.FirstSwapTarget.ContainsKey(shapeshifter.PlayerId) && !Transporter.FirstSwapTarget.ContainsKey(shapeshifter.PlayerId) && !Transposer.FirstSwapTarget.ContainsKey(shapeshifter.PlayerId)) LateTask.New(shapeshifter.RpcResetAbilityCooldown, 0.01f, log: false); if (!isSSneeded) diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 81ec27fa8..e9b5f7ca2 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -2283,6 +2283,9 @@ "TimeThiefDecreaseMeetingTime": "Lower Meeting Time by", "TimeThiefLowerLimitVotingTime": "Minimum Voting Time", "TimeThiefReturnStolenTimeUponDeath": "Return Stolen Time Upon Death", + "Transposer": "Transposer", + "TransposerInfo": "Swap Players' Appearance", + "TransposerInfoLong": "(Impostors):\\nAs the Transposer, you can select 2 players using your shapeshift menu to forcefully shapeshift them into each other for some time.", "Trapster": "Trapster", "TrapsterInfo": "Start a killing chain", "TrapsterInfoLong": "(Impostors):\\nAs the Trapster, you can vanish/pet to create a fake dead body. Anyone who reports this body will die, and depending on settings, the body will be reported.\\n\\n(Legacy Version):\\nIn the game, the Trapster kills a player and then puts a trap on their body. If another player reports the dead body and it has the Trapster's trap on it, the player who reports it will die instantly without the body getting reported. The player killed by the Trapster's trap will also become a Trapster. This chain reaction continues until no more players are left to become Trapsters. However, the Trapster will die immediately after making a kill. It's worth noting that the Trapster's own dead body cannot trigger their trap.", diff --git a/Roles/Standard/Crewmate/Support/Transporter.cs b/Roles/Standard/Crewmate/Support/Transporter.cs index 376324264..4bd29a556 100644 --- a/Roles/Standard/Crewmate/Support/Transporter.cs +++ b/Roles/Standard/Crewmate/Support/Transporter.cs @@ -55,11 +55,7 @@ internal class Transporter : RoleBase { if (!shapeshifting) return true; - if (shapeshifter == null || target == null || shapeshifter == target || !shapeshifter.IsAlive() || !target.IsAlive()) - return false; - - if (shapeshifter.GetAbilityUseLimit() < 1f) - return false; + if (!target.IsAlive() || shapeshifter.GetAbilityUseLimit() < 1f) return false; if (FirstSwapTarget.TryGetValue(shapeshifter.PlayerId, out byte firstTargetId)) { @@ -71,7 +67,7 @@ internal class Transporter : RoleBase PlayerControl firstTarget = firstTargetId.GetPlayer(); - if (firstTarget == null || !firstTarget.IsAlive()) + if (!firstTarget || !firstTarget.IsAlive()) { FirstSwapTarget.Remove(shapeshifter.PlayerId); shapeshifter.Notify(CustomRoles.Impostor.ColoredTextByRole(Translator.GetString("ErrorTeleport"))); diff --git a/Roles/Standard/Impostor/Concealing/Fakeshifter.cs b/Roles/Standard/Impostor/Concealing/Fakeshifter.cs index fd7656431..31027b271 100644 --- a/Roles/Standard/Impostor/Concealing/Fakeshifter.cs +++ b/Roles/Standard/Impostor/Concealing/Fakeshifter.cs @@ -1,7 +1,5 @@ -using System; -using AmongUs.GameOptions; +using AmongUs.GameOptions; using Hazel; -using InnerNet; namespace EHR.Roles; diff --git a/Roles/Standard/Impostor/Concealing/Transposer.cs b/Roles/Standard/Impostor/Concealing/Transposer.cs new file mode 100644 index 000000000..2c87e339c --- /dev/null +++ b/Roles/Standard/Impostor/Concealing/Transposer.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using AmongUs.GameOptions; +using Hazel; + +namespace EHR.Roles; + +public class Transposer : RoleBase +{ + public static bool On; + + public static Dictionary FirstSwapTarget = []; + + private static OptionItem AbilityCooldown; + private static OptionItem AbilityDuration; + private static OptionItem AbilityUseLimit; + private static OptionItem AbilityUseGainWithEachKill; + + public override bool IsEnable => On; + + public override void SetupCustomOption() + { + StartSetup(659600) + .AutoSetupOption(ref AbilityCooldown, 30f, new FloatValueRule(0f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref AbilityDuration, 15f, new FloatValueRule(0f, 120f, 0.5f), OptionFormat.Seconds) + .AutoSetupOption(ref AbilityUseLimit, 1f, new FloatValueRule(0, 20, 0.05f), OptionFormat.Times) + .AutoSetupOption(ref AbilityUseGainWithEachKill, 0.5f, new FloatValueRule(0f, 5f, 0.1f), OptionFormat.Times); + } + + public override void Init() + { + On = false; + FirstSwapTarget = []; + } + + public override void Add(byte playerId) + { + On = true; + playerId.SetAbilityUseLimit(AbilityUseLimit.GetFloat()); + } + + public override void Remove(byte playerId) + { + FirstSwapTarget.Remove(playerId); + } + + public override void ApplyGameOptions(IGameOptions opt, byte playerId) + { + AURoleOptions.ShapeshifterCooldown = AbilityCooldown.GetFloat(); + AURoleOptions.ShapeshifterDuration = 1f; + } + + public override bool OnShapeshift(PlayerControl transposer, PlayerControl target, bool shapeshifting) + { + if (!shapeshifting) return true; + + if (FirstSwapTarget.TryGetValue(transposer.PlayerId, out byte firstTargetId)) + { + PlayerControl firstTarget = Utils.GetPlayerById(firstTargetId); + + if (firstTarget && firstTarget.IsAlive() && target.IsAlive()) + { + transposer.RpcRemoveAbilityUse(notify: false); + + CustomRpcSender sender = CustomRpcSender.Create("Transposer", SendOption.Reliable); + sender.StartMessage(); + sender.StartRpc(target.NetId, RpcCalls.Shapeshift) + .WriteNetObject(firstTarget) + .Write(false) + .EndRpc(); + sender.StartRpc(firstTarget.NetId, RpcCalls.Shapeshift) + .WriteNetObject(target) + .Write(false) + .EndRpc(); + sender.SendMessage(); + + try + { + firstTarget.Shapeshift(target, false); + target.Shapeshift(firstTarget, false); + } + catch { } + + LateTask.New(() => + { + if (!GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || ReportDeadBodyPatch.MeetingStarted) return; + + if (transposer && transposer.IsAlive()) + transposer.RpcResetAbilityCooldown(); + + bool hasValue = false; + CustomRpcSender writer = CustomRpcSender.Create("Transposer Revert", SendOption.Reliable); + writer.StartMessage(); + + if (firstTarget && firstTarget.IsAlive() && firstTarget.IsShifted()) + { + hasValue = true; + try { firstTarget.Shapeshift(firstTarget, false); } catch { } + writer.StartRpc(firstTarget.NetId, RpcCalls.Shapeshift) + .WriteNetObject(firstTarget) + .Write(false) + .EndRpc(); + } + + if (target && target.IsAlive() && target.IsShifted()) + { + hasValue = true; + try { target.Shapeshift(target, false); } catch { } + writer.StartRpc(target.NetId, RpcCalls.Shapeshift) + .WriteNetObject(target) + .Write(false) + .EndRpc(); + } + + writer.SendMessage(dispose: !hasValue); + + }, AbilityDuration.GetFloat(), "Transposer ability"); + } + + FirstSwapTarget.Remove(transposer.PlayerId); + } + else + { + if (transposer.GetAbilityUseLimit() < 1f) return false; + FirstSwapTarget[transposer.PlayerId] = target.PlayerId; + } + + return false; + } + + public override void SetButtonTexts(HudManager hud, byte id) + { + hud.AbilityButton?.OverrideText(Translator.GetString("BountyHunterChangeButtonText")); + } +} \ No newline at end of file diff --git a/Roles/Standard/Impostor/Support/Swapster.cs b/Roles/Standard/Impostor/Support/Swapster.cs index f483543d5..407ec7117 100644 --- a/Roles/Standard/Impostor/Support/Swapster.cs +++ b/Roles/Standard/Impostor/Support/Swapster.cs @@ -44,14 +44,19 @@ internal class Swapster : RoleBase public override bool OnShapeshift(PlayerControl swapster, PlayerControl target, bool shapeshifting) { - if (swapster == null || target == null || swapster == target || !shapeshifting) return true; + if (!shapeshifting) return true; if (FirstSwapTarget.TryGetValue(swapster.PlayerId, out byte firstTargetId)) { PlayerControl firstTarget = Utils.GetPlayerById(firstTargetId); - Vector2 pos = firstTarget.Pos(); - firstTarget.TP(target); - target.TP(pos); + + if (firstTarget && firstTarget.IsAlive()) + { + Vector2 pos = firstTarget.Pos(); + firstTarget.TP(target); + target.TP(pos); + } + FirstSwapTarget.Remove(swapster.PlayerId); } else