diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index a9a5a765b8..9e03181b48 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -57,6 +57,7 @@ namespace Content.Client.Entry
"ResearchPointSource",
"ResearchClient",
"IdCardConsole",
+ "MimePowers",
"ThermalRegulator",
"DiseaseMachineRunning",
"DiseaseMachine",
diff --git a/Content.Server/Abilities/Mime/InvisibleWallComponent.cs b/Content.Server/Abilities/Mime/InvisibleWallComponent.cs
new file mode 100644
index 0000000000..fafa2c8e83
--- /dev/null
+++ b/Content.Server/Abilities/Mime/InvisibleWallComponent.cs
@@ -0,0 +1,12 @@
+namespace Content.Server.Abilities.Mime
+{
+ // Tracks invisible wall despawning
+ [RegisterComponent]
+ public sealed class InvisibleWallComponent : Component
+ {
+ [DataField("accumulator")]
+ public float Accumulator = 0f;
+ [DataField("despawnTime")]
+ public TimeSpan DespawnTime = TimeSpan.FromSeconds(30);
+ }
+}
diff --git a/Content.Server/Abilities/Mime/MimePowersComponent.cs b/Content.Server/Abilities/Mime/MimePowersComponent.cs
new file mode 100644
index 0000000000..3b56a07a6d
--- /dev/null
+++ b/Content.Server/Abilities/Mime/MimePowersComponent.cs
@@ -0,0 +1,63 @@
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Prototypes;
+using Content.Shared.Actions.ActionTypes;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Abilities.Mime
+{
+ ///
+ /// Lets its owner entity use mime powers, like placing invisible walls.
+ ///
+ [RegisterComponent]
+ public sealed class MimePowersComponent : Component
+ {
+ ///
+ /// Whether this component is active or not.
+ ///
+ [ViewVariables]
+ [DataField("enabled")]
+ public bool Enabled = true;
+
+ ///
+ /// The wall prototype to use.
+ ///
+ [DataField("wallPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string WallPrototype = "WallInvisible";
+
+ [DataField("invisibleWallAction")]
+ public InstantAction InvisibleWallAction = new()
+ {
+ UseDelay = TimeSpan.FromSeconds(30),
+ Icon = new SpriteSpecifier.Texture(new ResourcePath("Structures/Walls/solid.rsi/full.png")),
+ Name = "mime-invisible-wall",
+ Description = "mime-invisible-wall-desc",
+ Priority = -1,
+ Event = new InvisibleWallActionEvent(),
+ };
+
+
+ /// The vow zone lies below
+
+ public bool VowBroken = false;
+
+
+ ///
+ /// Whether this mime is ready to take the vow again.
+ /// Note that if they already have the vow, this is also false.
+ ///
+ public bool ReadyToRepent = false;
+
+ ///
+ /// Accumulator for when the mime breaks their vows
+ ///
+
+ [DataField("accumulator")]
+ public float Accumulator = 0f;
+
+ ///
+ /// How long it takes the mime to get their powers back
+
+ [DataField("vowCooldown")]
+ public TimeSpan VowCooldown = TimeSpan.FromMinutes(5);
+ }
+}
diff --git a/Content.Server/Abilities/Mime/MimePowersSystem.cs b/Content.Server/Abilities/Mime/MimePowersSystem.cs
new file mode 100644
index 0000000000..b21090f6f9
--- /dev/null
+++ b/Content.Server/Abilities/Mime/MimePowersSystem.cs
@@ -0,0 +1,147 @@
+using Content.Server.Popups;
+using Content.Server.Coordinates.Helpers;
+using Content.Shared.Speech;
+using Content.Shared.Actions;
+using Content.Shared.Alert;
+using Content.Shared.Physics;
+using Content.Shared.Doors.Components;
+using Content.Shared.Maps;
+using Content.Shared.MobState.Components;
+using Content.Shared.Tag;
+using Robust.Shared.Player;
+using Robust.Shared.Physics;
+
+namespace Content.Server.Abilities.Mime
+{
+ public sealed class MimePowersSystem : EntitySystem
+ {
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+ [Dependency] private readonly AlertsSystem _alertsSystem = default!;
+ [Dependency] private readonly TagSystem _tagSystem = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnSpeakAttempt);
+ SubscribeLocalEvent(OnInvisibleWall);
+ }
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ /// Queue to despawn invis walls
+ foreach (var invisWall in EntityQuery())
+ {
+ invisWall.Accumulator += frameTime;
+ if (invisWall.Accumulator < invisWall.DespawnTime.TotalSeconds)
+ {
+ continue;
+ }
+ EntityManager.QueueDeleteEntity(invisWall.Owner);
+ }
+ /// Queue to track whether mimes can retake vows yet
+ foreach (var mime in EntityQuery())
+ {
+ if (!mime.VowBroken || mime.ReadyToRepent)
+ return;
+
+ mime.Accumulator += frameTime;
+ if (mime.Accumulator < mime.VowCooldown.TotalSeconds)
+ {
+ continue;
+ }
+ mime.ReadyToRepent = true;
+ _popupSystem.PopupEntity(Loc.GetString("mime-ready-to-repent"), mime.Owner, Filter.Entities(mime.Owner));
+ }
+ }
+
+ private void OnComponentInit(EntityUid uid, MimePowersComponent component, ComponentInit args)
+ {
+ _actionsSystem.AddAction(uid, component.InvisibleWallAction, uid);
+ _alertsSystem.ShowAlert(uid, AlertType.VowOfSilence);
+ }
+ private void OnSpeakAttempt(EntityUid uid, MimePowersComponent component, SpeakAttemptEvent args)
+ {
+ if (!component.Enabled)
+ return;
+
+ _popupSystem.PopupEntity(Loc.GetString("mime-cant-speak"), uid, Filter.Entities(uid));
+ args.Cancel();
+ }
+
+ ///
+ /// Creates an invisible wall in a free space after some checks.
+ ///
+ private void OnInvisibleWall(EntityUid uid, MimePowersComponent component, InvisibleWallActionEvent args)
+ {
+ if (!component.Enabled)
+ return;
+
+ var xform = Transform(uid);
+ /// Get the tile in front of the mime
+ var offsetValue = xform.LocalRotation.ToWorldVec().Normalized;
+ var coords = xform.Coordinates.Offset(offsetValue).SnapToGrid();
+ /// Check there are no walls or mobs there
+ foreach (var entity in coords.GetEntitiesInTile())
+ {
+ IPhysBody? physics = null; /// We use this to check if it's impassable
+ if ((HasComp(entity) && entity != uid) || /// Is it a mob?
+ ((Resolve(entity, ref physics, false) && (physics.CollisionLayer & (int) CollisionGroup.Impassable) != 0) // Is it impassable?
+ && !(TryComp(entity, out var door) && door.State != DoorState.Closed))) // Is it a door that's open and so not actually impassable?
+ {
+ _popupSystem.PopupEntity(Loc.GetString("mime-invisible-wall-failed"), uid, Filter.Entities(uid));
+ return;
+ }
+ }
+ _popupSystem.PopupEntity(Loc.GetString("mime-invisible-wall-popup", ("mime", uid)), uid, Filter.Pvs(uid));
+ /// Make sure we set the invisible wall to despawn properly
+ var wall = EntityManager.SpawnEntity(component.WallPrototype, coords);
+ EnsureComp(wall);
+ /// Handle args so cooldown works
+ args.Handled = true;
+ }
+
+ ///
+ /// Break this mime's vow to not speak.
+ ///
+ public void BreakVow(EntityUid uid, MimePowersComponent? mimePowers = null)
+ {
+ if (!Resolve(uid, ref mimePowers))
+ return;
+
+ if (mimePowers.VowBroken)
+ return;
+
+ mimePowers.Enabled = false;
+ mimePowers.VowBroken = true;
+ _alertsSystem.ClearAlert(uid, AlertType.VowOfSilence);
+ _alertsSystem.ShowAlert(uid, AlertType.VowBroken);
+ _actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallAction);
+ }
+
+ ///
+ /// Retake this mime's vow to not speak.
+ ///
+ public void RetakeVow(EntityUid uid, MimePowersComponent? mimePowers = null)
+ {
+ if (!Resolve(uid, ref mimePowers))
+ return;
+
+ if (!mimePowers.ReadyToRepent)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("mime-not-ready-repent"), uid, Filter.Entities(uid));
+ return;
+ }
+
+ mimePowers.Enabled = true;
+ mimePowers.ReadyToRepent = false;
+ mimePowers.VowBroken = false;
+ mimePowers.Accumulator = 0f;
+ _alertsSystem.ClearAlert(uid, AlertType.VowBroken);
+ _alertsSystem.ShowAlert(uid, AlertType.VowOfSilence);
+ _actionsSystem.AddAction(uid, mimePowers.InvisibleWallAction, uid);
+ }
+ }
+
+ public sealed class InvisibleWallActionEvent : InstantActionEvent {}
+}
diff --git a/Content.Server/Alert/Click/BreakVow.cs b/Content.Server/Alert/Click/BreakVow.cs
new file mode 100644
index 0000000000..67ac15bea8
--- /dev/null
+++ b/Content.Server/Alert/Click/BreakVow.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Alert;
+using Content.Server.Abilities.Mime;
+
+namespace Content.Server.Alert.Click
+{
+ ///
+ /// Break your mime vows
+ ///
+ [DataDefinition]
+ public sealed class BreakVow : IAlertClick
+ {
+ public void AlertClicked(EntityUid player)
+ {
+ if (IoCManager.Resolve().TryGetComponent(player, out var mimePowers))
+ {
+ EntitySystem.Get().BreakVow(player, mimePowers);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Alert/Click/RetakeVow.cs b/Content.Server/Alert/Click/RetakeVow.cs
new file mode 100644
index 0000000000..06c97697fb
--- /dev/null
+++ b/Content.Server/Alert/Click/RetakeVow.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Alert;
+using Content.Server.Abilities.Mime;
+
+namespace Content.Server.Alert.Click
+{
+ ///
+ /// Retake your mime vows
+ ///
+ [DataDefinition]
+ public sealed class RetakeVow : IAlertClick
+ {
+ public void AlertClicked(EntityUid player)
+ {
+ if (IoCManager.Resolve().TryGetComponent(player, out var mimePowers))
+ {
+ EntitySystem.Get().RetakeVow(player, mimePowers);
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs
index b6a7ef8763..35409a2548 100644
--- a/Content.Shared/Alert/AlertType.cs
+++ b/Content.Shared/Alert/AlertType.cs
@@ -32,6 +32,8 @@
Pulling,
Magboots,
Toxins,
+ VowOfSilence,
+ VowBroken,
Debug1,
Debug2,
Debug3,
diff --git a/Resources/Locale/en-US/abilities/mime.ftl b/Resources/Locale/en-US/abilities/mime.ftl
new file mode 100644
index 0000000000..da03cd76b7
--- /dev/null
+++ b/Resources/Locale/en-US/abilities/mime.ftl
@@ -0,0 +1,7 @@
+mime-cant-speak = Your vow of silence prevents you from speaking.
+mime-invisible-wall = Create Invisible Wall
+mime-invisible-wall-desc = Create an invisible wall in front of you, if placeable there.
+mime-invisible-wall-popup = {CAPITALIZE(THE($mime))} brushes up against an invisible wall!
+mime-invisible-wall-failed = You can't create an invisible wall there.
+mime-not-ready-repent = You aren't ready to repent for your broken vow yet.
+mime-ready-to-repent = You feel ready to take your vows again.
diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml
index 73c16846c2..f4668347ed 100644
--- a/Resources/Prototypes/Alerts/alerts.yml
+++ b/Resources/Prototypes/Alerts/alerts.yml
@@ -191,6 +191,20 @@
name: "[color=red]Parched[/color]"
description: You're severely thirsty. The thirst makes moving around a chore.
+- type: alert
+ id: VowOfSilence
+ icon: /Textures/Interface/Alerts/Abilities/silenced.png
+ name: Vow of Silence
+ onClick: !type:BreakVow { }
+ description: You have taken a vow of silence as part of initiation into the Mystiko Tagma Mimon. Click to break your vow.
+
+- type: alert
+ id: VowBroken
+ icon: /Textures/Interface/Actions/scream.png
+ name: Vow Broken
+ onClick: !type:RetakeVow { }
+ description: You've broken your vows to Mimes everywhere. You can speak, but you've lost your powers for at least 5 entire minutes!!! Click to try and retake your vow.
+
- type: alert
id: Pulled
icon: /Textures/Interface/Alerts/Pull/pulled.png
diff --git a/Resources/Prototypes/Entities/Structures/Walls/walls.yml b/Resources/Prototypes/Entities/Structures/Walls/walls.yml
index eb978ac692..4d247fc014 100644
--- a/Resources/Prototypes/Entities/Structures/Walls/walls.yml
+++ b/Resources/Prototypes/Entities/Structures/Walls/walls.yml
@@ -649,3 +649,25 @@
- type: IconSmooth
key: walls
base: wood
+
+- type: entity
+ id: WallInvisible
+ name: Invisible Wall
+ components:
+ - type: Tag
+ tags:
+ - Wall
+ - type: Physics
+ bodyType: Static
+ - type: Fixtures
+ fixtures:
+ - shape:
+ !type:PhysShapeAabb
+ bounds: "-0.5,-0.5,0.5,0.5"
+ layer:
+ - Impassable
+ - VaultImpassable
+ - SmallImpassable
+ mask:
+ - Impassable
+ - type: Airtight
diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
index 43ab456936..9571cdedaa 100644
--- a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
+++ b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
@@ -9,6 +9,10 @@
access:
- Theatre
- Maintenance
+ special:
+ - !type:AddComponentSpecial
+ components:
+ - type: MimePowers
- type: startingGear
id: MimeGear
diff --git a/Resources/Textures/Interface/Alerts/Abilities/silenced.png b/Resources/Textures/Interface/Alerts/Abilities/silenced.png
new file mode 100644
index 0000000000..373a90abe6
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/Abilities/silenced.png differ