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