diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 4d7489b383..eaf2673392 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -45,6 +45,7 @@ namespace Content.Client.Entry
"DiseaseDiagnoser",
"DiseaseVaccine",
"DiseaseVaccineCreator",
+ "ImmovableRod",
"DiseaseZombie",
"DiseaseBuildup",
"ZombieTransfer",
diff --git a/Content.Server/ImmovableRod/ImmovableRodComponent.cs b/Content.Server/ImmovableRod/ImmovableRodComponent.cs
new file mode 100644
index 0000000000..fec9a5b382
--- /dev/null
+++ b/Content.Server/ImmovableRod/ImmovableRodComponent.cs
@@ -0,0 +1,48 @@
+using Content.Shared.Sound;
+
+namespace Content.Server.ImmovableRod;
+
+[RegisterComponent]
+public sealed class ImmovableRodComponent : Component
+{
+ public int MobCount = 0;
+
+ [DataField("hitSound")]
+ public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/bang.ogg");
+
+ [DataField("hitSoundProbability")]
+ public float HitSoundProbability = 0.1f;
+
+ ///
+ /// The rod will be automatically cleaned up after this time.
+ ///
+ [DataField("lifetime")]
+ public TimeSpan Lifetime = TimeSpan.FromSeconds(30);
+
+ [DataField("minSpeed")]
+ public float MinSpeed = 10f;
+
+ [DataField("maxSpeed")]
+ public float MaxSpeed = 35f;
+
+ ///
+ /// Stuff like wizard rods might want to set this to false, so that they can set the velocity themselves.
+ ///
+ [DataField("randomizeVelocity")]
+ public bool RandomizeVelocity = true;
+
+ ///
+ /// Overrides the random direction for an immovable rod.
+ ///
+ [DataField("directionOverride")]
+ public Angle DirectionOverride = Angle.Zero;
+
+ ///
+ /// With this set to true, rods will automatically set the tiles under them to space.
+ ///
+ [DataField("destroyTiles")]
+ public bool DestroyTiles = true;
+
+ [DataField("accumulator")]
+ public float Accumulator = 0f;
+}
diff --git a/Content.Server/ImmovableRod/ImmovableRodSystem.cs b/Content.Server/ImmovableRod/ImmovableRodSystem.cs
new file mode 100644
index 0000000000..c4a56da233
--- /dev/null
+++ b/Content.Server/ImmovableRod/ImmovableRodSystem.cs
@@ -0,0 +1,119 @@
+using Content.Server.Body.Components;
+using Content.Server.Popups;
+using Content.Shared.Examine;
+using Robust.Shared.Audio;
+using Robust.Shared.Map;
+using Robust.Shared.Physics.Dynamics;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+
+namespace Content.Server.ImmovableRod;
+
+public sealed class ImmovableRodSystem : EntitySystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IMapManager _map = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ // we are deliberately including paused entities. rod hungers for all
+ foreach (var (rod, trans) in EntityManager.EntityQuery(true))
+ {
+ rod.Accumulator += frameTime;
+
+ if (rod.Accumulator > rod.Lifetime.TotalSeconds)
+ {
+ QueueDel(rod.Owner);
+ return;
+ }
+
+ if (!rod.DestroyTiles)
+ continue;
+ if (!_map.TryGetGrid(trans.GridID, out var grid))
+ continue;
+
+ grid.SetTile(trans.Coordinates, Tile.Empty);
+ }
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnCollide);
+ SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnExamined);
+ }
+
+ private void OnComponentInit(EntityUid uid, ImmovableRodComponent component, ComponentInit args)
+ {
+ if (EntityManager.TryGetComponent(uid, out PhysicsComponent? phys))
+ {
+ phys.LinearDamping = 0f;
+ phys.Friction = 0f;
+ phys.BodyStatus = BodyStatus.InAir;
+
+ if (!component.RandomizeVelocity)
+ return;
+
+ var xform = Transform(uid);
+ var vel = component.DirectionOverride.Degrees switch
+ {
+ 0f => _random.NextVector2(component.MinSpeed, component.MaxSpeed),
+ _ => xform.WorldRotation.RotateVec(component.DirectionOverride.ToVec()) * _random.NextFloat(component.MinSpeed, component.MaxSpeed)
+ };
+
+ phys.ApplyLinearImpulse(vel);
+ xform.LocalRotation = (vel - xform.WorldPosition).ToWorldAngle() + MathHelper.PiOver2;
+ }
+ }
+
+ private void OnCollide(EntityUid uid, ImmovableRodComponent component, StartCollideEvent args)
+ {
+ var ent = args.OtherFixture.Body.Owner;
+
+ if (_random.Prob(component.HitSoundProbability))
+ {
+ SoundSystem.Play(Filter.Pvs(uid), component.Sound.GetSound(), uid, component.Sound.Params);
+ }
+
+ if (HasComp(ent))
+ {
+ // oh god.
+ var coords = Transform(uid).Coordinates;
+ _popup.PopupCoordinates(Loc.GetString("immovable-rod-collided-rod-not-good"), coords, Filter.Pvs(uid));
+
+ Del(uid);
+ Del(ent);
+ Spawn("Singularity", coords);
+
+ return;
+ }
+
+ // gib em
+ if (TryComp(ent, out var body))
+ {
+ component.MobCount++;
+
+ _popup.PopupEntity(Loc.GetString("immovable-rod-penetrated-mob", ("rod", uid), ("mob", ent)), uid, Filter.Pvs(uid));
+ body.Gib();
+ }
+
+ QueueDel(ent);
+ }
+
+ private void OnExamined(EntityUid uid, ImmovableRodComponent component, ExaminedEvent args)
+ {
+ if (component.MobCount == 0)
+ {
+ args.PushText(Loc.GetString("immovable-rod-consumed-none", ("rod", uid)));
+ }
+ else
+ {
+ args.PushText(Loc.GetString("immovable-rod-consumed-souls", ("rod", uid), ("amount", component.MobCount)));
+ }
+ }
+}
diff --git a/Resources/Locale/en-US/immovable-rod/immovable-rod.ftl b/Resources/Locale/en-US/immovable-rod/immovable-rod.ftl
new file mode 100644
index 0000000000..6843e350b0
--- /dev/null
+++ b/Resources/Locale/en-US/immovable-rod/immovable-rod.ftl
@@ -0,0 +1,5 @@
+immovable-rod-collided-rod-not-good = Oh fuck, that can't be good.
+immovable-rod-penetrated-mob = {CAPITALIZE(THE($rod))} cleanly eviscerates {THE($mob)}!
+
+immovable-rod-consumed-none = {CAPITALIZE(THE($rod))} has consumed zero souls.
+immovable-rod-consumed-souls = {CAPITALIZE(THE($rod))} has consumed {$amount} souls.
diff --git a/Resources/Prototypes/Entities/Objects/Fun/immovable_rod.yml b/Resources/Prototypes/Entities/Objects/Fun/immovable_rod.yml
new file mode 100644
index 0000000000..0eebc02258
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Fun/immovable_rod.yml
@@ -0,0 +1,41 @@
+ # Immovable rod
+
+- type: entity
+ id: ImmovableRod
+ name: immovable rod
+ description: You can sense that it's hungry. That's usually a bad sign.
+ components:
+ - type: Clickable
+ - type: InteractionOutline
+ - type: MovementIgnoreGravity
+ - type: Sprite
+ sprite: Objects/Fun/immovable_rod.rsi
+ state: icon
+ noRot: false
+ - type: ImmovableRod
+ - type: Physics
+ bodyType: Dynamic
+ linearDamping: 0
+ - type: PointLight
+ radius: 3
+ color: red
+ energy: 2.0
+ - type: Fixtures
+ fixtures:
+ - shape:
+ !type:PhysShapeCircle
+ radius: 0.5
+ mass: 1
+ hard: false
+ layer:
+ - Impassable
+ - Opaque
+
+- type: entity
+ id: ImmovableRodSlow
+ suffix: Slow
+ parent: ImmovableRod
+ components:
+ - type: ImmovableRod
+ minSpeed: 1
+ maxSpeed: 5
diff --git a/Resources/Textures/Objects/Fun/immovable_rod.rsi/icon.png b/Resources/Textures/Objects/Fun/immovable_rod.rsi/icon.png
new file mode 100644
index 0000000000..6508b8897a
Binary files /dev/null and b/Resources/Textures/Objects/Fun/immovable_rod.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Fun/immovable_rod.rsi/meta.json b/Resources/Textures/Objects/Fun/immovable_rod.rsi/meta.json
new file mode 100644
index 0000000000..85047cd29b
--- /dev/null
+++ b/Resources/Textures/Objects/Fun/immovable_rod.rsi/meta.json
@@ -0,0 +1,14 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/20e4add48712b59e9bcadd187beee54c02f98e38, modified by mirrorcult to be 1-dir",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ }
+ ]
+}