diff --git a/CustomRoles.cs b/CustomRoles.cs
index e00ea4312..f6865fb56 100644
--- a/CustomRoles.cs
+++ b/CustomRoles.cs
@@ -632,6 +632,7 @@ public enum CustomRoles
Tiebreaker,
Tired,
Torch,
+ Transfer,
Truant,
Unbound,
Undead,
diff --git a/Main.cs b/Main.cs
index a6a33e981..b4259be91 100644
--- a/Main.cs
+++ b/Main.cs
@@ -762,6 +762,7 @@ public class Main : BasePlugin
{ CustomRoles.Listener, "#060270" },
{ CustomRoles.Unbound, "#DFC57B" },
{ CustomRoles.AntiTP, "#fcba03" },
+ { CustomRoles.Transfer, "#737373" },
{ CustomRoles.Focused, "#A10E49" },
{ CustomRoles.Dizzy, "#de97a7" },
{ CustomRoles.Entombed, "#8c71de" },
diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs
index ccee61536..e8407efbd 100644
--- a/Patches/OnGameStartedPatch.cs
+++ b/Patches/OnGameStartedPatch.cs
@@ -305,6 +305,7 @@ internal static class ChangeRoleSettings
Circumvent.Init();
Committed.Init();
Reroll.Init();
+ Transfer.Init();
}
catch (Exception ex) { Logger.Exception(ex, "Init Roles"); }
diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs
index 3b753d70a..b4c18efb1 100644
--- a/Patches/PlayerControlPatch.cs
+++ b/Patches/PlayerControlPatch.cs
@@ -1768,7 +1768,12 @@ internal static class FixedUpdatePatch
}
bool checkPos = inTask && !ExileController.Instance && !AntiBlackout.SkipTasks && alive && !Pelican.IsEaten(playerId) && Main.IntroDestroyed;
- if (checkPos) Asthmatic.OnCheckPlayerPosition(player);
+
+ if (checkPos)
+ {
+ Asthmatic.OnCheckPlayerPosition(player);
+ Transfer.OnCheckPlayerPosition(player);
+ }
foreach (PlayerState state in Main.PlayerStates.Values)
{
diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json
index e9b5f7ca2..03ea76ecf 100644
--- a/Resources/Lang/en_US.json
+++ b/Resources/Lang/en_US.json
@@ -208,6 +208,7 @@
"Prefix.Tiebreaker": "Tiebreaker",
"Prefix.Tired": "Tired",
"Prefix.Torch": "Torch",
+ "Prefix.Transfer": "Transfer",
"Prefix.Trainee": "Trainee",
"Prefix.Truant": "Truant",
"Prefix.Unbound": "Unbound",
@@ -3981,6 +3982,9 @@
"TorchInfoLong": "(Add-ons):\\nOnly Crewmates will get Torch, who have increased vision and may not be affected by Lights Sabotage.",
"TorchAffectedByLights": "Torch's vision is affected by Lights Sabotage",
"TorchVision": "Torch Vision",
+ "Transfer": "Transfer",
+ "TransferInfo": "Transfer Between Vents",
+ "TransferInfoLong": "(Add-ons):\\nAs the Transfer, whenever you step on a vent, you teleport to a random vent.",
"Trainee": "Trainee",
"TraineeInfo": "You Can't Vent/Sabotage/SS/Pet/Vanish Round 1",
"TraineeInfoLong": "(Add-ons):\\nYou can't vent, sabotage, shapeshift, or pet on the first round.",
diff --git a/Roles/Standard/Addons/Common/Transfer.cs b/Roles/Standard/Addons/Common/Transfer.cs
new file mode 100644
index 000000000..6d9185185
--- /dev/null
+++ b/Roles/Standard/Addons/Common/Transfer.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Linq;
+using EHR.Modules;
+
+namespace EHR.Roles;
+
+public class Transfer : IAddon
+{
+ public AddonTypes Type => AddonTypes.Mixed;
+
+ private static Dictionary LastTP;
+ private static Vector2[] VentLocations;
+
+ public void SetupCustomOption()
+ {
+ Options.SetupAdtRoleOptions(659700, CustomRoles.Transfer, canSetNum: true, teamSpawnOptions: true);
+ }
+
+ public static void Init()
+ {
+ LastTP = null;
+ LateTask.New(() => VentLocations = ShipStatus.Instance && CustomRoles.Transfer.RoleExist() ? ShipStatus.Instance.AllVents.Select(x => new Vector2(x.transform.position.x, x.transform.position.y + 0.3636f)).ToArray() : null, 25f);
+ }
+
+ public static void OnCheckPlayerPosition(PlayerControl pc)
+ {
+ if (VentLocations == null || !PerSecondUpdateScheduler.ShouldRunUpdate(pc.PlayerId) || !pc.Is(CustomRoles.Transfer)) return;
+
+ long now = Utils.TimeStamp;
+ LastTP ??= [];
+
+ if (LastTP.TryGetValue(pc.PlayerId, out long ts) && ts + 3 >= now) return;
+ if (!FastVector2.TryGetClosestInRange(pc.Pos(), VentLocations, 0.8f, out _)) return;
+
+ pc.TPToRandomVent();
+ LastTP[pc.PlayerId] = now;
+ }
+}
\ No newline at end of file