diff --git a/Content.Shared/Actions/ConfirmableActionComponent.cs b/Content.Shared/Actions/ConfirmableActionComponent.cs new file mode 100644 index 0000000000..6c208f47c6 --- /dev/null +++ b/Content.Shared/Actions/ConfirmableActionComponent.cs @@ -0,0 +1,48 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Actions; + +/// +/// An action that must be confirmed before using it. +/// Using it for the first time primes it, after a delay you can then confirm it. +/// Used for dangerous actions that cannot be undone (unlike screaming). +/// +[RegisterComponent, NetworkedComponent, Access(typeof(ConfirmableActionSystem))] +[AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class ConfirmableActionComponent : Component +{ + /// + /// Warning popup shown when priming the action. + /// + [DataField(required: true)] + public LocId Popup = string.Empty; + + /// + /// If not null, this is when the action can be confirmed at. + /// This is the time of priming plus the delay. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoNetworkedField, AutoPausedField] + public TimeSpan? NextConfirm; + + /// + /// If not null, this is when the action will unprime at. + /// This is NextConfirm> plus PrimeTime + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoNetworkedField, AutoPausedField] + public TimeSpan? NextUnprime; + + /// + /// Forced delay between priming and confirming to prevent accidents. + /// + [DataField] + public TimeSpan ConfirmDelay = TimeSpan.FromSeconds(1); + + /// + /// Once you prime the action it will unprime after this length of time. + /// + [DataField] + public TimeSpan PrimeTime = TimeSpan.FromSeconds(5); +} diff --git a/Content.Shared/Actions/ConfirmableActionSystem.cs b/Content.Shared/Actions/ConfirmableActionSystem.cs new file mode 100644 index 0000000000..26cc7111d2 --- /dev/null +++ b/Content.Shared/Actions/ConfirmableActionSystem.cs @@ -0,0 +1,80 @@ +using Content.Shared.Actions.Events; +using Content.Shared.Popups; +using Robust.Shared.Timing; + +namespace Content.Shared.Actions; + +/// +/// Handles action priming, confirmation and automatic unpriming. +/// +public sealed class ConfirmableActionSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAttempt); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // handle automatic unpriming + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.NextUnprime is not {} time) + continue; + + if (now >= time) + Unprime((uid, comp)); + } + } + + private void OnAttempt(Entity ent, ref ActionAttemptEvent args) + { + if (args.Cancelled) + return; + + // if not primed, prime it and cancel the action + if (ent.Comp.NextConfirm is not {} confirm) + { + Prime(ent, args.User); + args.Cancelled = true; + return; + } + + // primed but the delay isnt over, cancel the action + if (_timing.CurTime < confirm) + { + args.Cancelled = true; + return; + } + + // primed and delay has passed, let the action go through + Unprime(ent); + } + + private void Prime(Entity ent, EntityUid user) + { + var (uid, comp) = ent; + comp.NextConfirm = _timing.CurTime + comp.ConfirmDelay; + comp.NextUnprime = comp.NextConfirm + comp.PrimeTime; + Dirty(uid, comp); + + _popup.PopupClient(Loc.GetString(comp.Popup), user, user, PopupType.LargeCaution); + } + + private void Unprime(Entity ent) + { + var (uid, comp) = ent; + comp.NextConfirm = null; + comp.NextUnprime = null; + Dirty(uid, comp); + } +} diff --git a/Content.Shared/Actions/Events/ActionAttemptEvent.cs b/Content.Shared/Actions/Events/ActionAttemptEvent.cs new file mode 100644 index 0000000000..26f23f9ec3 --- /dev/null +++ b/Content.Shared/Actions/Events/ActionAttemptEvent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Actions.Events; + +/// +/// Raised before an action is used and can be cancelled to prevent it. +/// Allowed to have side effects like modifying the action component. +/// +[ByRefEvent] +public record struct ActionAttemptEvent(EntityUid User, bool Cancelled = false); diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index a6c40c7ae3..a3bfa07130 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -352,6 +352,13 @@ public abstract class SharedActionsSystem : EntitySystem if (!action.Enabled) return; + // check for action use prevention + // TODO: make code below use this event with a dedicated component + var attemptEv = new ActionAttemptEvent(user); + RaiseLocalEvent(actionEnt, ref attemptEv); + if (attemptEv.Cancelled) + return; + var curTime = GameTiming.CurTime; // TODO: Check for charge recovery timer if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime) diff --git a/Resources/Locale/en-US/actions/actions/suicide.ftl b/Resources/Locale/en-US/actions/actions/suicide.ftl new file mode 100644 index 0000000000..a271790d07 --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/suicide.ftl @@ -0,0 +1 @@ +suicide-action-popup = THIS ACTION WILL KILL YOU! Use it again to confirm. diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index c63071551b..2d5ec9a678 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -1,3 +1,14 @@ +# base actions + +- type: entity + id: BaseSuicideAction + abstract: true + components: + - type: ConfirmableAction + popup: suicide-action-popup + +# actions + - type: entity id: ActionScream name: Scream @@ -47,6 +58,7 @@ event: !type:OpenStorageImplantEvent - type: entity + parent: BaseSuicideAction id: ActionActivateMicroBomb name: Activate Microbomb description: Activates your internal microbomb, completely destroying you and your equipment @@ -62,6 +74,7 @@ event: !type:ActivateImplantEvent - type: entity + parent: BaseSuicideAction id: ActionActivateDeathAcidifier name: Activate Death-Acidifier description: Activates your death-acidifier, completely melting you and your equipment