From 4c24db9d9c137471cb5be9453c18e89d14fa286c Mon Sep 17 00:00:00 2001 From: kosticia Date: Mon, 28 Jul 2025 21:53:07 +0300 Subject: [PATCH] Predict mimepowers (#38859) * predict * fix * fix * aa * oops * TOTALFIX * some more cleanup --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- .../Abilities/Mime/MimePowersComponent.cs | 70 ------- .../Abilities/Mime/MimePowersSystem.cs | 171 ---------------- Content.Server/Speech/Muting/MutingSystem.cs | 2 +- .../Abilities/Mime/MimePowersComponent.cs | 76 +++++++ .../Abilities/Mime/MimePowersSystem.cs | 187 ++++++++++++++++++ Resources/Locale/en-US/abilities/mime.ftl | 3 +- 6 files changed, 266 insertions(+), 243 deletions(-) delete mode 100644 Content.Server/Abilities/Mime/MimePowersComponent.cs delete mode 100644 Content.Server/Abilities/Mime/MimePowersSystem.cs create mode 100644 Content.Shared/Abilities/Mime/MimePowersComponent.cs create mode 100644 Content.Shared/Abilities/Mime/MimePowersSystem.cs diff --git a/Content.Server/Abilities/Mime/MimePowersComponent.cs b/Content.Server/Abilities/Mime/MimePowersComponent.cs deleted file mode 100644 index 24ffe5d023..0000000000 --- a/Content.Server/Abilities/Mime/MimePowersComponent.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Content.Shared.Alert; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Abilities.Mime -{ - /// - /// Lets its owner entity use mime powers, like placing invisible walls. - /// - [RegisterComponent] - public sealed partial class MimePowersComponent : Component - { - /// - /// Whether this component is active or not. - /// - [DataField("enabled")] - public bool Enabled = true; - - /// - /// The wall prototype to use. - /// - [DataField("wallPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string WallPrototype = "WallInvisible"; - - [DataField("invisibleWallAction", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? InvisibleWallAction = "ActionMimeInvisibleWall"; - - [DataField("invisibleWallActionEntity")] public EntityUid? InvisibleWallActionEntity; - - // 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; - - /// - /// Time when the mime can repent their vow - /// - [DataField("vowRepentTime", customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan VowRepentTime = TimeSpan.Zero; - - /// - /// How long it takes the mime to get their powers back - /// - [DataField("vowCooldown")] - public TimeSpan VowCooldown = TimeSpan.FromMinutes(5); - - [DataField] - public ProtoId VowAlert = "VowOfSilence"; - - [DataField] - public ProtoId VowBrokenAlert = "VowBroken"; - - /// - /// Does this component prevent the mime from writing on paper while their vow is active? - /// - [DataField] - public bool PreventWriting = false; - - /// - /// What message is displayed when the mime fails to write? - /// - [DataField] - public LocId FailWriteMessage = "paper-component-illiterate-mime"; - } -} diff --git a/Content.Server/Abilities/Mime/MimePowersSystem.cs b/Content.Server/Abilities/Mime/MimePowersSystem.cs deleted file mode 100644 index 29c7b1710c..0000000000 --- a/Content.Server/Abilities/Mime/MimePowersSystem.cs +++ /dev/null @@ -1,171 +0,0 @@ -using Content.Server.Popups; -using Content.Shared.Abilities.Mime; -using Content.Shared.Actions; -using Content.Shared.Actions.Events; -using Content.Shared.Alert; -using Content.Shared.Coordinates.Helpers; -using Content.Shared.Maps; -using Content.Shared.Paper; -using Content.Shared.Physics; -using Robust.Shared.Containers; -using Robust.Shared.Map; -using Robust.Shared.Timing; -using Content.Shared.Speech.Muting; - -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 TurfSystem _turf = default!; - [Dependency] private readonly IMapManager _mapMan = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly IGameTiming _timing = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(OnInvisibleWall); - - SubscribeLocalEvent(OnBreakVowAlert); - SubscribeLocalEvent(OnRetakeVowAlert); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - // Queue to track whether mimes can retake vows yet - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var mime)) - { - if (!mime.VowBroken || mime.ReadyToRepent) - continue; - - if (_timing.CurTime < mime.VowRepentTime) - continue; - - mime.ReadyToRepent = true; - _popupSystem.PopupEntity(Loc.GetString("mime-ready-to-repent"), uid, uid); - } - } - - private void OnComponentInit(EntityUid uid, MimePowersComponent component, ComponentInit args) - { - EnsureComp(uid); - if (component.PreventWriting) - { - EnsureComp(uid, out var illiterateComponent); - illiterateComponent.FailWriteMessage = component.FailWriteMessage; - Dirty(uid, illiterateComponent); - } - - _alertsSystem.ShowAlert(uid, component.VowAlert); - _actionsSystem.AddAction(uid, ref component.InvisibleWallActionEntity, component.InvisibleWallAction, uid); - } - - /// - /// Creates an invisible wall in a free space after some checks. - /// - private void OnInvisibleWall(EntityUid uid, MimePowersComponent component, InvisibleWallActionEvent args) - { - if (!component.Enabled) - return; - - if (_container.IsEntityOrParentInContainer(uid)) - return; - - var xform = Transform(uid); - // Get the tile in front of the mime - var offsetValue = xform.LocalRotation.ToWorldVec(); - var coords = xform.Coordinates.Offset(offsetValue).SnapToGrid(EntityManager, _mapMan); - var tile = _turf.GetTileRef(coords); - if (tile == null) - return; - - // Check if the tile is blocked by a wall or mob, and don't create the wall if so - if (_turf.IsTileBlocked(tile.Value, CollisionGroup.Impassable | CollisionGroup.Opaque)) - { - _popupSystem.PopupEntity(Loc.GetString("mime-invisible-wall-failed"), uid, uid); - return; - } - - _popupSystem.PopupEntity(Loc.GetString("mime-invisible-wall-popup", ("mime", uid)), uid); - // Make sure we set the invisible wall to despawn properly - Spawn(component.WallPrototype, _turf.GetTileCenter(tile.Value)); - // Handle args so cooldown works - args.Handled = true; - } - - private void OnBreakVowAlert(Entity ent, ref BreakVowAlertEvent args) - { - if (args.Handled) - return; - BreakVow(ent, ent); - args.Handled = true; - } - - private void OnRetakeVowAlert(Entity ent, ref RetakeVowAlertEvent args) - { - if (args.Handled) - return; - RetakeVow(ent, ent); - 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; - mimePowers.VowRepentTime = _timing.CurTime + mimePowers.VowCooldown; - RemComp(uid); - if (mimePowers.PreventWriting) - RemComp(uid); - _alertsSystem.ClearAlert(uid, mimePowers.VowAlert); - _alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert); - _actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallActionEntity); - } - - /// - /// 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, uid); - return; - } - - mimePowers.Enabled = true; - mimePowers.ReadyToRepent = false; - mimePowers.VowBroken = false; - AddComp(uid); - if (mimePowers.PreventWriting) - { - EnsureComp(uid, out var illiterateComponent); - illiterateComponent.FailWriteMessage = mimePowers.FailWriteMessage; - Dirty(uid, illiterateComponent); - } - - _alertsSystem.ClearAlert(uid, mimePowers.VowBrokenAlert); - _alertsSystem.ShowAlert(uid, mimePowers.VowAlert); - _actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid); - } - } -} diff --git a/Content.Server/Speech/Muting/MutingSystem.cs b/Content.Server/Speech/Muting/MutingSystem.cs index edf82bbfb2..f588e2238d 100644 --- a/Content.Server/Speech/Muting/MutingSystem.cs +++ b/Content.Server/Speech/Muting/MutingSystem.cs @@ -1,4 +1,4 @@ -using Content.Server.Abilities.Mime; +using Content.Shared.Abilities.Mime; using Content.Server.Chat.Systems; using Content.Server.Popups; using Content.Server.Speech.Components; diff --git a/Content.Shared/Abilities/Mime/MimePowersComponent.cs b/Content.Shared/Abilities/Mime/MimePowersComponent.cs new file mode 100644 index 0000000000..8414dd7a0d --- /dev/null +++ b/Content.Shared/Abilities/Mime/MimePowersComponent.cs @@ -0,0 +1,76 @@ +using Content.Shared.Alert; +using Robust.Shared.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Abilities.Mime; + +/// +/// Lets its owner entity use mime powers, like placing invisible walls. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[AutoGenerateComponentPause] +public sealed partial class MimePowersComponent : Component +{ + /// + /// Whether this component is active or not. + /// + [DataField, AutoNetworkedField] + public bool Enabled = true; + + /// + /// The wall prototype to use. + /// + [DataField, AutoNetworkedField] + public EntProtoId WallPrototype = "WallInvisible"; + + [DataField] + public EntProtoId? InvisibleWallAction = "ActionMimeInvisibleWall"; + + [DataField, AutoNetworkedField] + public EntityUid? InvisibleWallActionEntity; + + // The vow zone lies below + [DataField, AutoNetworkedField] + 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. + /// + [DataField, AutoNetworkedField] + public bool ReadyToRepent = false; + + /// + /// Time when the mime can repent their vow + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoNetworkedField, AutoPausedField] + public TimeSpan VowRepentTime = TimeSpan.Zero; + + /// + /// How long it takes the mime to get their powers back + /// + [DataField, AutoNetworkedField] + public TimeSpan VowCooldown = TimeSpan.FromMinutes(5); + + [DataField] + public ProtoId VowAlert = "VowOfSilence"; + + [DataField] + public ProtoId VowBrokenAlert = "VowBroken"; + + /// + /// Does this component prevent the mime from writing on paper while their vow is active? + /// + [DataField, AutoNetworkedField] + public bool PreventWriting = false; + + /// + /// What message is displayed when the mime fails to write? + /// + [DataField] + public LocId FailWriteMessage = "paper-component-illiterate-mime"; + + public override bool SendOnlyToOwner => true; +} diff --git a/Content.Shared/Abilities/Mime/MimePowersSystem.cs b/Content.Shared/Abilities/Mime/MimePowersSystem.cs new file mode 100644 index 0000000000..22ba7a3591 --- /dev/null +++ b/Content.Shared/Abilities/Mime/MimePowersSystem.cs @@ -0,0 +1,187 @@ +using Content.Shared.Popups; +using Content.Shared.Actions; +using Content.Shared.Actions.Events; +using Content.Shared.Alert; +using Content.Shared.Coordinates.Helpers; +using Content.Shared.IdentityManagement; +using Content.Shared.Maps; +using Content.Shared.Paper; +using Content.Shared.Physics; +using Content.Shared.Speech.Muting; +using Robust.Shared.Containers; +using Robust.Shared.Map; +using Robust.Shared.Timing; + +namespace Content.Shared.Abilities.Mime; + +public sealed class MimePowersSystem : EntitySystem +{ + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly AlertsSystem _alertsSystem = default!; + [Dependency] private readonly TurfSystem _turf = default!; + [Dependency] private readonly IMapManager _mapMan = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnComponentShutdown); + SubscribeLocalEvent(OnInvisibleWall); + + SubscribeLocalEvent(OnBreakVowAlert); + SubscribeLocalEvent(OnRetakeVowAlert); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + // Queue to track whether mimes can retake vows yet + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var mime)) + { + if (!mime.VowBroken || mime.ReadyToRepent) + continue; + + if (_timing.CurTime < mime.VowRepentTime) + continue; + + mime.ReadyToRepent = true; + Dirty(uid, mime); + _popupSystem.PopupClient(Loc.GetString("mime-ready-to-repent"), uid, uid); + } + } + + private void OnComponentInit(Entity ent, ref ComponentInit args) + { + EnsureComp(ent); + + if (ent.Comp.PreventWriting) + { + EnsureComp(ent, out var illiterateComponent); + illiterateComponent.FailWriteMessage = ent.Comp.FailWriteMessage; + Dirty(ent, illiterateComponent); + } + + _alertsSystem.ShowAlert(ent, ent.Comp.VowAlert); + _actionsSystem.AddAction(ent, ref ent.Comp.InvisibleWallActionEntity, ent.Comp.InvisibleWallAction); + } + + private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) + { + _actionsSystem.RemoveAction(ent.Owner, ent.Comp.InvisibleWallActionEntity); + } + + /// + /// Creates an invisible wall in a free space after some checks. + /// + private void OnInvisibleWall(Entity ent, ref InvisibleWallActionEvent args) + { + if (!ent.Comp.Enabled) + return; + + if (_container.IsEntityOrParentInContainer(ent)) + return; + + var xform = Transform(ent); + // Get the tile in front of the mime + var offsetValue = xform.LocalRotation.ToWorldVec(); + var coords = xform.Coordinates.Offset(offsetValue).SnapToGrid(EntityManager, _mapMan); + var tile = _turf.GetTileRef(coords); + if (tile == null) + return; + + // Check if the tile is blocked by a wall or mob, and don't create the wall if so + if (_turf.IsTileBlocked(tile.Value, CollisionGroup.Impassable | CollisionGroup.Opaque)) + { + _popupSystem.PopupClient(Loc.GetString("mime-invisible-wall-failed"), ent, ent); + return; + } + + var messageSelf = Loc.GetString("mime-invisible-wall-popup-self", ("mime", Identity.Entity(ent.Owner, EntityManager))); + var messageOthers = Loc.GetString("mime-invisible-wall-popup-others", ("mime", Identity.Entity(ent.Owner, EntityManager))); + _popupSystem.PopupPredicted(messageSelf, messageOthers, ent, ent); + + // Make sure we set the invisible wall to despawn properly + PredictedSpawnAtPosition(ent.Comp.WallPrototype, _turf.GetTileCenter(tile.Value)); + // Handle args so cooldown works + args.Handled = true; + } + + private void OnBreakVowAlert(Entity ent, ref BreakVowAlertEvent args) + { + if (args.Handled) + return; + + BreakVow(ent, ent); + args.Handled = true; + } + + private void OnRetakeVowAlert(Entity ent, ref RetakeVowAlertEvent args) + { + if (args.Handled) + return; + + RetakeVow(ent, ent); + 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; + mimePowers.VowRepentTime = _timing.CurTime + mimePowers.VowCooldown; + Dirty(uid, mimePowers); + RemComp(uid); + if (mimePowers.PreventWriting) + RemComp(uid); + + _alertsSystem.ClearAlert(uid, mimePowers.VowAlert); + _alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert); + _actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallActionEntity); + } + + /// + /// 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.PopupClient(Loc.GetString("mime-not-ready-repent"), uid, uid); + return; + } + + mimePowers.Enabled = true; + mimePowers.ReadyToRepent = false; + mimePowers.VowBroken = false; + Dirty(uid, mimePowers); + AddComp(uid); + if (mimePowers.PreventWriting) + { + EnsureComp(uid, out var illiterateComponent); + illiterateComponent.FailWriteMessage = mimePowers.FailWriteMessage; + Dirty(uid, illiterateComponent); + } + + _alertsSystem.ClearAlert(uid, mimePowers.VowBrokenAlert); + _alertsSystem.ShowAlert(uid, mimePowers.VowAlert); + _actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid); + } +} diff --git a/Resources/Locale/en-US/abilities/mime.ftl b/Resources/Locale/en-US/abilities/mime.ftl index 4fd960d89e..3957283f80 100644 --- a/Resources/Locale/en-US/abilities/mime.ftl +++ b/Resources/Locale/en-US/abilities/mime.ftl @@ -1,5 +1,6 @@ mime-cant-speak = Your vow of silence prevents you from speaking. -mime-invisible-wall-popup = {CAPITALIZE(THE($mime))} brushes up against an invisible wall! +mime-invisible-wall-popup-self = You brush up against an invisible wall! +mime-invisible-wall-popup-others = {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.