From ce186e6cc1006cd33ed7f01448504d050210d259 Mon Sep 17 00:00:00 2001
From: Rane <60792108+Elijahrane@users.noreply.github.com>
Date: Thu, 28 Apr 2022 23:41:03 -0400
Subject: [PATCH] Mime Powers (Vow + Invisible Wall) (#7653)
---
Content.Client/Entry/IgnoredComponents.cs | 1 +
.../Abilities/Mime/InvisibleWallComponent.cs | 12 ++
.../Abilities/Mime/MimePowersComponent.cs | 63 ++++++++
.../Abilities/Mime/MimePowersSystem.cs | 147 ++++++++++++++++++
Content.Server/Alert/Click/BreakVow.cs | 20 +++
Content.Server/Alert/Click/RetakeVow.cs | 20 +++
Content.Shared/Alert/AlertType.cs | 2 +
Resources/Locale/en-US/abilities/mime.ftl | 7 +
Resources/Prototypes/Alerts/alerts.yml | 14 ++
.../Entities/Structures/Walls/walls.yml | 22 +++
.../Prototypes/Roles/Jobs/Civilian/mime.yml | 4 +
.../Interface/Alerts/Abilities/silenced.png | Bin 0 -> 6027 bytes
12 files changed, 312 insertions(+)
create mode 100644 Content.Server/Abilities/Mime/InvisibleWallComponent.cs
create mode 100644 Content.Server/Abilities/Mime/MimePowersComponent.cs
create mode 100644 Content.Server/Abilities/Mime/MimePowersSystem.cs
create mode 100644 Content.Server/Alert/Click/BreakVow.cs
create mode 100644 Content.Server/Alert/Click/RetakeVow.cs
create mode 100644 Resources/Locale/en-US/abilities/mime.ftl
create mode 100644 Resources/Textures/Interface/Alerts/Abilities/silenced.png
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 0000000000000000000000000000000000000000..373a90abe642c5317c4f347fddf74e73aa374d0c
GIT binary patch
literal 6027
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
YGTb|1Zd8XF3JwT_YVBxMX0a#sKMeYBR{#J2
literal 0
HcmV?d00001