From a4dfe8beed17e17d4b7a8b18a7e8e2db2b9e9b65 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sun, 23 Apr 2023 12:25:12 +1000 Subject: [PATCH] Power cell slot QOL (#15373) --- .../ActivatableUIRequiresVisionSystem.cs | 86 ++-- .../Medical/HealthAnalyzerSystem.cs | 22 +- Content.Server/Pinpointer/StationMapSystem.cs | 1 - .../Power/Components/BatteryComponent.cs | 2 +- .../PowerCell/PowerCellDrawComponent.cs | 25 ++ .../PowerCell/PowerCellSlotEmptyEvent.cs | 7 + Content.Server/PowerCell/PowerCellSystem.cs | 111 +++++ ...ActivatableUIRequiresPowerCellComponent.cs | 13 + .../ActivatableUISystem.Power.cs | 77 ++++ .../UserInterface/ActivatableUISystem.cs | 391 +++++++++--------- .../components/power-cell-component.ftl | 4 +- .../components/stunbaton-component.ftl | 2 +- .../Entities/Objects/Devices/station_map.yml | 10 +- .../Medical/handheld_crew_monitor.yml | 8 +- .../Specific/Medical/healthanalyzer.yml | 8 +- .../Prototypes/Entities/Objects/base_item.yml | 33 ++ .../Structures/Wallmounts/station_map.yml | 8 +- 17 files changed, 544 insertions(+), 264 deletions(-) create mode 100644 Content.Server/PowerCell/PowerCellDrawComponent.cs create mode 100644 Content.Server/PowerCell/PowerCellSlotEmptyEvent.cs create mode 100644 Content.Server/UserInterface/ActivatableUIRequiresPowerCellComponent.cs create mode 100644 Content.Server/UserInterface/ActivatableUISystem.Power.cs diff --git a/Content.Server/Eye/Blinding/ActivatableUIRequiresVisionSystem.cs b/Content.Server/Eye/Blinding/ActivatableUIRequiresVisionSystem.cs index a1401f1745..4b5d3d3eef 100644 --- a/Content.Server/Eye/Blinding/ActivatableUIRequiresVisionSystem.cs +++ b/Content.Server/Eye/Blinding/ActivatableUIRequiresVisionSystem.cs @@ -4,57 +4,55 @@ using Content.Server.Popups; using Robust.Shared.Player; using Robust.Server.GameObjects; -namespace Content.Server.Eye.Blinding +namespace Content.Server.Eye.Blinding; + +public sealed class ActivatableUIRequiresVisionSystem : EntitySystem { - public sealed class ActivatableUIRequiresVisionSystem : EntitySystem + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; + + public override void Initialize() { - [Dependency] private readonly ActivatableUISystem _activatableUISystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; + base.Initialize(); + SubscribeLocalEvent(OnOpenAttempt); + SubscribeLocalEvent(OnBlindnessChanged); + } - public override void Initialize() + private void OnOpenAttempt(EntityUid uid, ActivatableUIRequiresVisionComponent component, ActivatableUIOpenAttemptEvent args) + { + if (args.Cancelled) + return; + + if (TryComp(args.User, out var blindable) && blindable.Sources > 0) { - base.Initialize(); - SubscribeLocalEvent(OnOpenAttempt); - SubscribeLocalEvent(OnBlindnessChanged); + _popupSystem.PopupCursor(Loc.GetString("blindness-fail-attempt"), args.User, Shared.Popups.PopupType.MediumCaution); + args.Cancel(); + } + } + + private void OnBlindnessChanged(EntityUid uid, BlindableComponent component, BlindnessChangedEvent args) + { + if (!args.Blind) + return; + + if (!TryComp(uid, out var actor)) + return; + + var uiList = _userInterfaceSystem.GetAllUIsForSession(actor.PlayerSession); + if (uiList == null) + return; + + Queue closeList = new(); // foreach collection modified moment + + foreach (var ui in uiList) + { + if (HasComp(ui.Owner)) + closeList.Enqueue(ui); } - private void OnOpenAttempt(EntityUid uid, ActivatableUIRequiresVisionComponent component, ActivatableUIOpenAttemptEvent args) + foreach (var ui in closeList) { - if (args.Cancelled) - return; - - if (TryComp(args.User, out var blindable) && blindable.Sources > 0) - { - _popupSystem.PopupCursor(Loc.GetString("blindness-fail-attempt"), args.User, Shared.Popups.PopupType.MediumCaution); - args.Cancel(); - } - } - - private void OnBlindnessChanged(EntityUid uid, BlindableComponent component, BlindnessChangedEvent args) - { - if (!args.Blind) - return; - - if (!TryComp(uid, out var actor)) - return; - - var uiList = _userInterfaceSystem.GetAllUIsForSession(actor.PlayerSession); - if (uiList == null) - return; - - Queue closeList = new(); // foreach collection modified moment - - foreach (var ui in uiList) - { - if (HasComp(ui.Owner)) - closeList.Enqueue(ui); - } - - foreach (var ui in closeList) - { - _userInterfaceSystem.CloseUi(ui, actor.PlayerSession); - } + _userInterfaceSystem.CloseUi(ui, actor.PlayerSession); } } } diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs index 197656ab70..85ace0f1d6 100644 --- a/Content.Server/Medical/HealthAnalyzerSystem.cs +++ b/Content.Server/Medical/HealthAnalyzerSystem.cs @@ -1,6 +1,8 @@ using Content.Server.Disease; using Content.Server.Medical.Components; using Content.Server.Popups; +using Content.Server.PowerCell; +using Content.Server.UserInterface; using Content.Shared.Damage; using Content.Shared.DoAfter; using Content.Shared.IdentityManagement; @@ -13,28 +15,23 @@ namespace Content.Server.Medical { public sealed class HealthAnalyzerSystem : EntitySystem { - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly DiseaseSystem _disease = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly PowerCellSystem _cell = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(HandleActivateInWorld); SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(OnDoAfter); } - private void HandleActivateInWorld(EntityUid uid, HealthAnalyzerComponent healthAnalyzer, ActivateInWorldEvent args) - { - OpenUserInterface(args.User, healthAnalyzer); - } - private void OnAfterInteract(EntityUid uid, HealthAnalyzerComponent healthAnalyzer, AfterInteractEvent args) { - if (args.Target == null || !args.CanReach || !HasComp(args.Target)) + if (args.Target == null || !args.CanReach || !HasComp(args.Target) || !_cell.HasActivatableCharge(uid, user: args.User)) return; _audio.PlayPvs(healthAnalyzer.ScanningBeginSound, uid); @@ -49,7 +46,7 @@ namespace Content.Server.Medical private void OnDoAfter(EntityUid uid, HealthAnalyzerComponent component, DoAfterEvent args) { - if (args.Handled || args.Cancelled || args.Args.Target == null) + if (args.Handled || args.Cancelled || args.Args.Target == null || !_cell.TryUseActivatableCharge(uid, user: args.User)) return; _audio.PlayPvs(component.ScanningEndSound, args.Args.User); @@ -58,6 +55,9 @@ namespace Content.Server.Medical // Below is for the traitor item // Piggybacking off another component's doafter is complete CBT so I gave up // and put it on the same component + /* + * this code is cursed wuuuuuuut + */ if (string.IsNullOrEmpty(component.Disease)) { args.Handled = true; @@ -71,8 +71,6 @@ namespace Content.Server.Medical _popupSystem.PopupEntity(Loc.GetString("disease-scanner-gave-self", ("disease", component.Disease)), args.Args.User, args.Args.User); } - - else { _popupSystem.PopupEntity(Loc.GetString("disease-scanner-gave-other", ("target", Identity.Entity(args.Args.Target.Value, EntityManager)), diff --git a/Content.Server/Pinpointer/StationMapSystem.cs b/Content.Server/Pinpointer/StationMapSystem.cs index 3b5eea07cf..c0dbbc641f 100644 --- a/Content.Server/Pinpointer/StationMapSystem.cs +++ b/Content.Server/Pinpointer/StationMapSystem.cs @@ -1,4 +1,3 @@ -using Content.Shared.Interaction.Events; using Content.Shared.Pinpointer; using Robust.Server.GameObjects; diff --git a/Content.Server/Power/Components/BatteryComponent.cs b/Content.Server/Power/Components/BatteryComponent.cs index 8071321745..f9cd0201ca 100644 --- a/Content.Server/Power/Components/BatteryComponent.cs +++ b/Content.Server/Power/Components/BatteryComponent.cs @@ -62,5 +62,5 @@ namespace Content.Server.Power.Components /// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage). /// [ByRefEvent] - public record struct ChargeChangedEvent(float Charge, float MaxCharge); + public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge); } diff --git a/Content.Server/PowerCell/PowerCellDrawComponent.cs b/Content.Server/PowerCell/PowerCellDrawComponent.cs new file mode 100644 index 0000000000..7697fa2654 --- /dev/null +++ b/Content.Server/PowerCell/PowerCellDrawComponent.cs @@ -0,0 +1,25 @@ +namespace Content.Server.PowerCell; + +/// +/// Indicates that the entity's ActivatableUI requires power or else it closes. +/// +[RegisterComponent] +public sealed class PowerCellDrawComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite), DataField("enabled")] + public bool Enabled = false; + + /// + /// How much the entity draws while the UI is open. + /// Set to 0 if you just wish to check for power upon opening the UI. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("drawRate")] + public float DrawRate = 1f; + + /// + /// How much power is used whenever the entity is "used". + /// This is used to ensure the UI won't open again without a minimum use power. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("useRate")] + public float UseRate = 0f; +} diff --git a/Content.Server/PowerCell/PowerCellSlotEmptyEvent.cs b/Content.Server/PowerCell/PowerCellSlotEmptyEvent.cs new file mode 100644 index 0000000000..b2930ee50f --- /dev/null +++ b/Content.Server/PowerCell/PowerCellSlotEmptyEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.PowerCell; + +/// +/// Raised directed on an entity when its active power cell has no more charge to supply. +/// +[ByRefEvent] +public readonly record struct PowerCellSlotEmptyEvent; diff --git a/Content.Server/PowerCell/PowerCellSystem.cs b/Content.Server/PowerCell/PowerCellSystem.cs index f7882dd644..b65a949da5 100644 --- a/Content.Server/PowerCell/PowerCellSystem.cs +++ b/Content.Server/PowerCell/PowerCellSystem.cs @@ -10,19 +10,25 @@ using Content.Shared.Rounding; using Robust.Shared.Containers; using System.Diagnostics.CodeAnalysis; using Content.Server.Kitchen.Components; +using Content.Server.UserInterface; using Content.Shared.Containers.ItemSlots; +using Content.Shared.Popups; using Content.Shared.Rejuvenate; +using Content.Shared.UserInterface; +using Robust.Server.GameObjects; namespace Content.Server.PowerCell; public sealed class PowerCellSystem : SharedPowerCellSystem { + [Dependency] private readonly ActivatableUISystem _activatable = default!; [Dependency] private readonly SolutionContainerSystem _solutionsSystem = default!; [Dependency] private readonly ExplosionSystem _explosionSystem = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; [Dependency] private readonly SharedAppearanceSystem _sharedAppearanceSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; public override void Initialize() { @@ -39,6 +45,28 @@ public sealed class PowerCellSystem : SharedPowerCellSystem SubscribeLocalEvent(OnMicrowaved); } + public override void Update(float frameTime) + { + base.Update(frameTime); + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp, out var slot)) + { + if (!comp.Enabled) + continue; + + if (!TryGetBatteryFromSlot(uid, out var battery, slot)) + continue; + + if (battery.TryUseCharge(comp.DrawRate * frameTime)) + continue; + + comp.Enabled = false; + var ev = new PowerCellSlotEmptyEvent(); + RaiseLocalEvent(uid, ref ev); + } + } + private void OnRejuvenate(EntityUid uid, PowerCellComponent component, RejuvenateEvent args) { component.IsRigged = false; @@ -102,6 +130,89 @@ public sealed class PowerCellSystem : SharedPowerCellSystem QueueDel(uid); } + #region Activatable + + /// + /// Returns whether the entity has a slotted battery and charge. + /// + /// Popup to this user with the relevant detail if specified. + public bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, EntityUid? user = null) + { + if (!Resolve(uid, ref battery, ref cell, false)) + return false; + + return HasCharge(uid, battery.UseRate, cell, user); + } + + /// + /// Tries to use the for this entity. + /// + /// Popup to this user with the relevant detail if specified. + public bool TryUseActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, EntityUid? user = null) + { + if (!Resolve(uid, ref battery, ref cell, false)) + return false; + + if (TryUseCharge(uid, battery.UseRate, cell, user)) + { + _activatable.CheckUsage(uid); + return true; + } + + return false; + } + + #endregion + + /// + /// Returns whether the entity has a slotted battery and charge for the requested action. + /// + /// Popup to this user with the relevant detail if specified. + public bool HasCharge(EntityUid uid, float charge, PowerCellSlotComponent? component = null, EntityUid? user = null) + { + if (!TryGetBatteryFromSlot(uid, out var battery, component)) + { + if (user != null) + _popup.PopupEntity(Loc.GetString("power-cell-no-battery"), uid, user.Value); + + return false; + } + + if (battery.CurrentCharge < charge) + { + if (user != null) + _popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value); + + return false; + } + + return true; + } + + /// + /// Tries to use charge from a slotted battery. + /// + public bool TryUseCharge(EntityUid uid, float charge, PowerCellSlotComponent? component = null, EntityUid? user = null) + { + if (!TryGetBatteryFromSlot(uid, out var battery, component)) + { + if (user != null) + _popup.PopupEntity(Loc.GetString("power-cell-no-battery"), uid, user.Value); + + return false; + } + + if (!battery.TryUseCharge(charge)) + { + if (user != null) + _popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value); + + return false; + } + + return true; + } + public bool TryGetBatteryFromSlot(EntityUid uid, [NotNullWhen(true)] out BatteryComponent? battery, PowerCellSlotComponent? component = null) { if (!Resolve(uid, ref component, false)) diff --git a/Content.Server/UserInterface/ActivatableUIRequiresPowerCellComponent.cs b/Content.Server/UserInterface/ActivatableUIRequiresPowerCellComponent.cs new file mode 100644 index 0000000000..14429dd455 --- /dev/null +++ b/Content.Server/UserInterface/ActivatableUIRequiresPowerCellComponent.cs @@ -0,0 +1,13 @@ +using Content.Server.PowerCell; +using Content.Shared.UserInterface; + +namespace Content.Server.UserInterface; + +/// +/// Specifies that the attached entity requires power. +/// +[RegisterComponent] +public sealed class ActivatableUIRequiresPowerCellComponent : Component +{ + +} diff --git a/Content.Server/UserInterface/ActivatableUISystem.Power.cs b/Content.Server/UserInterface/ActivatableUISystem.Power.cs new file mode 100644 index 0000000000..7b36116744 --- /dev/null +++ b/Content.Server/UserInterface/ActivatableUISystem.Power.cs @@ -0,0 +1,77 @@ +using Content.Server.PowerCell; +using Content.Shared.PowerCell; +using Robust.Shared.Containers; + +namespace Content.Server.UserInterface; + +public sealed partial class ActivatableUISystem +{ + [Dependency] private readonly PowerCellSystem _cell = default!; + + private void InitializePower() + { + SubscribeLocalEvent(OnBatteryOpenAttempt); + SubscribeLocalEvent(OnBatteryOpened); + SubscribeLocalEvent(OnBatteryClosed); + + SubscribeLocalEvent(OnPowerCellRemoved); + } + + private void OnPowerCellRemoved(EntityUid uid, PowerCellDrawComponent component, EntRemovedFromContainerMessage args) + { + if (TryComp(uid, out var draw)) + { + draw.Enabled = false; + } + + if (HasComp(uid) && + TryComp(uid, out var activatable) && + activatable.Key != null) + { + _uiSystem.TryCloseAll(uid, activatable.Key); + } + } + + private void OnBatteryOpened(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, BoundUIOpenedEvent args) + { + if (!TryComp(uid, out var draw)) + return; + + draw.Enabled = true; + } + + private void OnBatteryClosed(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, BoundUIClosedEvent args) + { + if (!TryComp(uid, out var draw)) + return; + + draw.Enabled = false; + } + + /// + /// Call if you want to check if the UI should close due to a recent battery usage. + /// + public void CheckUsage(EntityUid uid, ActivatableUIComponent? active = null, ActivatableUIRequiresPowerCellComponent? component = null, PowerCellDrawComponent? draw = null) + { + if (!Resolve(uid, ref component, ref draw, ref active, false) || active.Key == null) + return; + + if (_cell.HasCharge(uid, draw.UseRate)) + return; + + _uiSystem.TryCloseAll(uid, active.Key); + } + + private void OnBatteryOpenAttempt(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, ActivatableUIOpenAttemptEvent args) + { + if (!TryComp(uid, out var draw)) + return; + + // Check if we have the appropriate drawrate / userate to even open it. + if (args.Cancelled || !_cell.HasCharge(uid, MathF.Max(draw.DrawRate, draw.UseRate), user: args.User)) + { + args.Cancel(); + return; + } + } +} diff --git a/Content.Server/UserInterface/ActivatableUISystem.cs b/Content.Server/UserInterface/ActivatableUISystem.cs index e9bdce6c2c..b947aac89f 100644 --- a/Content.Server/UserInterface/ActivatableUISystem.cs +++ b/Content.Server/UserInterface/ActivatableUISystem.cs @@ -6,226 +6,225 @@ using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Verbs; -using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Server.Player; -namespace Content.Server.UserInterface +namespace Content.Server.UserInterface; + +public sealed partial class ActivatableUISystem : EntitySystem { - [UsedImplicitly] - internal sealed class ActivatableUISystem : EntitySystem + [Dependency] private readonly IAdminManager _adminManager = default!; + [Dependency] private readonly ActionBlockerSystem _blockerSystem = default!; + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + + public override void Initialize() { - [Dependency] private readonly IAdminManager _adminManager = default!; - [Dependency] private readonly ActionBlockerSystem _blockerSystem = default!; - [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + base.Initialize(); - public override void Initialize() - { - base.Initialize(); + SubscribeLocalEvent(OnActivate); + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnHandDeselected); + SubscribeLocalEvent((uid, aui, _) => CloseAll(uid, aui)); + // *THIS IS A BLATANT WORKAROUND!* RATIONALE: Microwaves need it + SubscribeLocalEvent(OnParentChanged); + SubscribeLocalEvent(OnUIClose); + SubscribeLocalEvent(OnBoundInterfaceInteractAttempt); - SubscribeLocalEvent(OnActivate); - SubscribeLocalEvent(OnUseInHand); - SubscribeLocalEvent(OnHandDeselected); - SubscribeLocalEvent((uid, aui, _) => CloseAll(uid, aui)); - // *THIS IS A BLATANT WORKAROUND!* RATIONALE: Microwaves need it - SubscribeLocalEvent(OnParentChanged); - SubscribeLocalEvent(OnUIClose); - SubscribeLocalEvent(OnBoundInterfaceInteractAttempt); + SubscribeLocalEvent>(AddOpenUiVerb); - SubscribeLocalEvent>(AddOpenUiVerb); + SubscribeLocalEvent(OnActionPerform); - SubscribeLocalEvent(OnActionPerform); - } - - private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev) - { - if (!TryComp(ev.Target, out ActivatableUIComponent? comp)) - return; - - if (!comp.RequireHands) - return; - - if (!TryComp(ev.Sender.AttachedEntity, out HandsComponent? hands) || hands.Hands.Count == 0) - ev.Cancel(); - } - - private void OnActionPerform(EntityUid uid, ServerUserInterfaceComponent component, OpenUiActionEvent args) - { - if (args.Handled || args.Key == null) - return; - - if (!TryComp(args.Performer, out ActorComponent? actor)) - return; - - args.Handled = _uiSystem.TryToggleUi(uid, args.Key, actor.PlayerSession); - } - - private void AddOpenUiVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent args) - { - if (!args.CanAccess) - return; - - if (component.RequireHands && args.Hands == null) - return; - - if (component.InHandsOnly && args.Using != uid) - return; - - if (!args.CanInteract && (!component.AllowSpectator || !HasComp(args.User))) - return; - - ActivationVerb verb = new(); - verb.Act = () => InteractUI(args.User, component); - verb.Text = Loc.GetString(component.VerbText); - // TODO VERBS add "open UI" icon? - args.Verbs.Add(verb); - } - - private void OnActivate(EntityUid uid, ActivatableUIComponent component, ActivateInWorldEvent args) - { - if (args.Handled) return; - if (component.InHandsOnly) return; - args.Handled = InteractUI(args.User, component); - } - - private void OnUseInHand(EntityUid uid, ActivatableUIComponent component, UseInHandEvent args) - { - if (args.Handled) return; - args.Handled = InteractUI(args.User, component); - } - - private void OnParentChanged(EntityUid uid, ActivatableUIComponent aui, ref EntParentChangedMessage args) - { - CloseAll(uid, aui); - } - - private void OnUIClose(EntityUid uid, ActivatableUIComponent component, BoundUIClosedEvent args) - { - if (args.Session != component.CurrentSingleUser) return; - if (args.UiKey != component.Key) return; - SetCurrentSingleUser(uid, null, component); - } - - private bool InteractUI(EntityUid user, ActivatableUIComponent aui) - { - if (!_blockerSystem.CanInteract(user, aui.Owner) && (!aui.AllowSpectator || !HasComp(user))) - return false; - - if (aui.RequireHands && !HasComp(user)) - return false; - - if (!EntityManager.TryGetComponent(user, out ActorComponent? actor)) return false; - - if (aui.AdminOnly && !_adminManager.IsAdmin(actor.PlayerSession)) return false; - - var ui = aui.UserInterface; - if (ui == null) return false; - - if (aui.SingleUser && (aui.CurrentSingleUser != null) && (actor.PlayerSession != aui.CurrentSingleUser)) - { - // If we get here, supposedly, the object is in use. - // Check with BUI that it's ACTUALLY in use just in case. - // Since this could brick the object if it goes wrong. - if (ui.SubscribedSessions.Count != 0) return false; - } - - // If we've gotten this far, fire a cancellable event that indicates someone is about to activate this. - // This is so that stuff can require further conditions (like power). - var oae = new ActivatableUIOpenAttemptEvent(user); - var uae = new UserOpenActivatableUIAttemptEvent(user, aui.Owner); - RaiseLocalEvent(user, uae, false); - RaiseLocalEvent((aui).Owner, oae, false); - if (oae.Cancelled || uae.Cancelled) return false; - - // Give the UI an opportunity to prepare itself if it needs to do anything - // before opening - var bae = new BeforeActivatableUIOpenEvent(user); - RaiseLocalEvent((aui).Owner, bae, false); - - SetCurrentSingleUser((aui).Owner, actor.PlayerSession, aui); - ui.Toggle(actor.PlayerSession); - - //Let the component know a user opened it so it can do whatever it needs to do - var aae = new AfterActivatableUIOpenEvent(user, actor.PlayerSession); - RaiseLocalEvent((aui).Owner, aae, false); - - return true; - } - - public void SetCurrentSingleUser(EntityUid uid, IPlayerSession? v, ActivatableUIComponent? aui = null) - { - if (!Resolve(uid, ref aui)) - return; - if (!aui.SingleUser) - return; - - aui.CurrentSingleUser = v; - - RaiseLocalEvent(uid, new ActivatableUIPlayerChangedEvent(), false); - } - - public void CloseAll(EntityUid uid, ActivatableUIComponent? aui = null) - { - if (!Resolve(uid, ref aui, false)) return; - aui.UserInterface?.CloseAll(); - } - - private void OnHandDeselected(EntityUid uid, ActivatableUIComponent? aui, HandDeselectedEvent args) - { - if (!Resolve(uid, ref aui, false)) return; - if (!aui.CloseOnHandDeselect) - return; - CloseAll(uid, aui); - } + InitializePower(); } - public sealed class ActivatableUIOpenAttemptEvent : CancellableEntityEventArgs + private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev) { - public EntityUid User { get; } - public ActivatableUIOpenAttemptEvent(EntityUid who) - { - User = who; - } + if (!TryComp(ev.Target, out ActivatableUIComponent? comp)) + return; + + if (!comp.RequireHands) + return; + + if (!TryComp(ev.Sender.AttachedEntity, out HandsComponent? hands) || hands.Hands.Count == 0) + ev.Cancel(); } - public sealed class UserOpenActivatableUIAttemptEvent : CancellableEntityEventArgs //have to one-up the already stroke-inducing name + private void OnActionPerform(EntityUid uid, ServerUserInterfaceComponent component, OpenUiActionEvent args) { - public EntityUid User { get; } - public EntityUid Target { get; } - public UserOpenActivatableUIAttemptEvent(EntityUid who, EntityUid target) - { - User = who; - Target = target; - } + if (args.Handled || args.Key == null) + return; + + if (!TryComp(args.Performer, out ActorComponent? actor)) + return; + + args.Handled = _uiSystem.TryToggleUi(uid, args.Key, actor.PlayerSession); } - public sealed class AfterActivatableUIOpenEvent : EntityEventArgs + private void AddOpenUiVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent args) { - public EntityUid User { get; } - public readonly IPlayerSession Session; + if (!args.CanAccess) + return; - public AfterActivatableUIOpenEvent(EntityUid who, IPlayerSession session) - { - User = who; - Session = session; - } + if (component.RequireHands && args.Hands == null) + return; + + if (component.InHandsOnly && args.Using != uid) + return; + + if (!args.CanInteract && (!component.AllowSpectator || !HasComp(args.User))) + return; + + ActivationVerb verb = new(); + verb.Act = () => InteractUI(args.User, component); + verb.Text = Loc.GetString(component.VerbText); + // TODO VERBS add "open UI" icon? + args.Verbs.Add(verb); } - /// - /// This is after it's decided the user can open the UI, - /// but before the UI actually opens. - /// Use this if you need to prepare the UI itself - /// - public sealed class BeforeActivatableUIOpenEvent : EntityEventArgs + private void OnActivate(EntityUid uid, ActivatableUIComponent component, ActivateInWorldEvent args) { - public EntityUid User { get; } - public BeforeActivatableUIOpenEvent(EntityUid who) - { - User = who; - } + if (args.Handled) return; + if (component.InHandsOnly) return; + args.Handled = InteractUI(args.User, component); } - public sealed class ActivatableUIPlayerChangedEvent : EntityEventArgs + private void OnUseInHand(EntityUid uid, ActivatableUIComponent component, UseInHandEvent args) { + if (args.Handled) return; + args.Handled = InteractUI(args.User, component); + } + + private void OnParentChanged(EntityUid uid, ActivatableUIComponent aui, ref EntParentChangedMessage args) + { + CloseAll(uid, aui); + } + + private void OnUIClose(EntityUid uid, ActivatableUIComponent component, BoundUIClosedEvent args) + { + if (args.Session != component.CurrentSingleUser) return; + if (args.UiKey != component.Key) return; + SetCurrentSingleUser(uid, null, component); + } + + private bool InteractUI(EntityUid user, ActivatableUIComponent aui) + { + if (!_blockerSystem.CanInteract(user, aui.Owner) && (!aui.AllowSpectator || !HasComp(user))) + return false; + + if (aui.RequireHands && !HasComp(user)) + return false; + + if (!EntityManager.TryGetComponent(user, out ActorComponent? actor)) return false; + + if (aui.AdminOnly && !_adminManager.IsAdmin(actor.PlayerSession)) return false; + + var ui = aui.UserInterface; + if (ui == null) return false; + + if (aui.SingleUser && (aui.CurrentSingleUser != null) && (actor.PlayerSession != aui.CurrentSingleUser)) + { + // If we get here, supposedly, the object is in use. + // Check with BUI that it's ACTUALLY in use just in case. + // Since this could brick the object if it goes wrong. + if (ui.SubscribedSessions.Count != 0) return false; + } + + // If we've gotten this far, fire a cancellable event that indicates someone is about to activate this. + // This is so that stuff can require further conditions (like power). + var oae = new ActivatableUIOpenAttemptEvent(user); + var uae = new UserOpenActivatableUIAttemptEvent(user, aui.Owner); + RaiseLocalEvent(user, uae, false); + RaiseLocalEvent((aui).Owner, oae, false); + if (oae.Cancelled || uae.Cancelled) return false; + + // Give the UI an opportunity to prepare itself if it needs to do anything + // before opening + var bae = new BeforeActivatableUIOpenEvent(user); + RaiseLocalEvent((aui).Owner, bae, false); + + SetCurrentSingleUser((aui).Owner, actor.PlayerSession, aui); + ui.Toggle(actor.PlayerSession); + + //Let the component know a user opened it so it can do whatever it needs to do + var aae = new AfterActivatableUIOpenEvent(user, actor.PlayerSession); + RaiseLocalEvent((aui).Owner, aae, false); + + return true; + } + + public void SetCurrentSingleUser(EntityUid uid, IPlayerSession? v, ActivatableUIComponent? aui = null) + { + if (!Resolve(uid, ref aui)) + return; + if (!aui.SingleUser) + return; + + aui.CurrentSingleUser = v; + + RaiseLocalEvent(uid, new ActivatableUIPlayerChangedEvent(), false); + } + + public void CloseAll(EntityUid uid, ActivatableUIComponent? aui = null) + { + if (!Resolve(uid, ref aui, false)) return; + aui.UserInterface?.CloseAll(); + } + + private void OnHandDeselected(EntityUid uid, ActivatableUIComponent? aui, HandDeselectedEvent args) + { + if (!Resolve(uid, ref aui, false)) return; + if (!aui.CloseOnHandDeselect) + return; + CloseAll(uid, aui); } } + +public sealed class ActivatableUIOpenAttemptEvent : CancellableEntityEventArgs +{ + public EntityUid User { get; } + public ActivatableUIOpenAttemptEvent(EntityUid who) + { + User = who; + } +} + +public sealed class UserOpenActivatableUIAttemptEvent : CancellableEntityEventArgs //have to one-up the already stroke-inducing name +{ + public EntityUid User { get; } + public EntityUid Target { get; } + public UserOpenActivatableUIAttemptEvent(EntityUid who, EntityUid target) + { + User = who; + Target = target; + } +} + +public sealed class AfterActivatableUIOpenEvent : EntityEventArgs +{ + public EntityUid User { get; } + public readonly IPlayerSession Session; + + public AfterActivatableUIOpenEvent(EntityUid who, IPlayerSession session) + { + User = who; + Session = session; + } +} + +/// +/// This is after it's decided the user can open the UI, +/// but before the UI actually opens. +/// Use this if you need to prepare the UI itself +/// +public sealed class BeforeActivatableUIOpenEvent : EntityEventArgs +{ + public EntityUid User { get; } + public BeforeActivatableUIOpenEvent(EntityUid who) + { + User = who; + } +} + +public sealed class ActivatableUIPlayerChangedEvent : EntityEventArgs +{ +} diff --git a/Resources/Locale/en-US/power-cell/components/power-cell-component.ftl b/Resources/Locale/en-US/power-cell/components/power-cell-component.ftl index f1406c477b..6190250a36 100644 --- a/Resources/Locale/en-US/power-cell/components/power-cell-component.ftl +++ b/Resources/Locale/en-US/power-cell/components/power-cell-component.ftl @@ -1 +1,3 @@ -power-cell-component-examine-details = The charge indicator reads {$currentCharge} %. \ No newline at end of file +power-cell-component-examine-details = The charge indicator reads [color=#5E7C16]{$currentCharge}[/color] %. +power-cell-no-battery = No power cell found +power-cell-insufficient = Insufficient power diff --git a/Resources/Locale/en-US/stunnable/components/stunbaton-component.ftl b/Resources/Locale/en-US/stunnable/components/stunbaton-component.ftl index 61e8adf4a9..cfe1ea9360 100644 --- a/Resources/Locale/en-US/stunnable/components/stunbaton-component.ftl +++ b/Resources/Locale/en-US/stunnable/components/stunbaton-component.ftl @@ -13,4 +13,4 @@ comp-stunbaton-activated-low-charge = Insufficient charge... stunbaton-component-low-charge = Insufficient charge... stunbaton-component-on-examine = The light is currently [color=darkgreen]on[/color]. -stunbaton-component-on-examine-charge = The charge indicator reads {$charge} % +stunbaton-component-on-examine-charge = The charge indicator reads [color=#5E7C16]{$charge}[/color] % diff --git a/Resources/Prototypes/Entities/Objects/Devices/station_map.yml b/Resources/Prototypes/Entities/Objects/Devices/station_map.yml index f88cd896ef..d333417ff1 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/station_map.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/station_map.yml @@ -1,8 +1,10 @@ - type: entity - id: StationMap + id: HandheldStationMap name: station map description: Displays a readout of the current station. - parent: BaseItem + parent: + - BaseItem + - PowerCellSlotSmallItem suffix: Handheld components: - type: StationMap @@ -13,6 +15,10 @@ - state: tablet - state: generic shader: unshaded + - type: PowerCellDraw + drawRate: 0 + useRate: 20 + - type: ActivatableUIRequiresPowerCell - type: ActivatableUI inHandsOnly: true singleUser: true diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml index 6e692044f6..b64b7b087f 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml @@ -1,12 +1,18 @@ - type: entity name: handheld crew monitor - parent: BaseItem + parent: + - BaseItem + - PowerCellSlotSmallItem id: HandheldCrewMonitor description: A hand-held crew monitor displaying the status of suit sensors. components: - type: Sprite sprite: Objects/Specific/Medical/handheldcrewmonitor.rsi state: scanner + - type: PowerCellDraw + drawRate: 0 + useRate: 20 + - type: ActivatableUIRequiresPowerCell - type: ActivatableUI key: enum.CrewMonitoringUIKey.Key closeOnHandDeselect: false diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml index 0ae2385e03..a36bf732f9 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml @@ -1,6 +1,8 @@ - type: entity name: health analyzer - parent: BaseItem + parent: + - BaseItem + - PowerCellSlotSmallItem id: HandheldHealthAnalyzer description: A hand-held body scanner capable of distinguishing vital signs of the subject. components: @@ -8,6 +10,10 @@ sprite: Objects/Specific/Medical/healthanalyzer.rsi netsync: false state: analyzer + - type: PowerCellDraw + drawRate: 0 + useRate: 20 + - type: ActivatableUIRequiresPowerCell - type: ActivatableUI key: enum.HealthAnalyzerUiKey.Key closeOnHandDeselect: false diff --git a/Resources/Prototypes/Entities/Objects/base_item.yml b/Resources/Prototypes/Entities/Objects/base_item.yml index e72823220c..a9b70dd662 100644 --- a/Resources/Prototypes/Entities/Objects/base_item.yml +++ b/Resources/Prototypes/Entities/Objects/base_item.yml @@ -56,3 +56,36 @@ storagebase: !type:Container ents: [] + +# PowerCellSlot parents +- type: entity + name: a + id: PowerCellSlotSmallItem + abstract: true + components: + - type: ContainerContainer + containers: + cell_slot: !type:ContainerSlot { } + - type: PowerCellSlot + cellSlotId: cell_slot + - type: ItemSlots + slots: + cell_slot: + name: power-cell-slot-component-slot-name-default + startingItem: PowerCellSmall + +- type: entity + name: a + id: PowerCellSlotMediumItem + abstract: true + components: + - type: ContainerContainer + containers: + cell_slot: !type:ContainerSlot { } + - type: PowerCellSlot + cellSlotId: cell_slot + - type: ItemSlots + slots: + cell_slot: + name: power-cell-slot-component-slot-name-default + startingItem: PowerCellMedium diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/station_map.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/station_map.yml index 5fa2f4467a..071c8cca71 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/station_map.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/station_map.yml @@ -1,5 +1,5 @@ - type: entity - id: WallStationMapBroken + id: StationMapBroken name: station map description: A virtual map of the surrounding station. suffix: Wall broken @@ -29,9 +29,9 @@ acts: [ "Destruction" ] - type: entity - id: WallStationMap + id: StationMap name: station map - parent: WallStationMapBroken + parent: StationMapBroken suffix: Wall placement: mode: SnapgridCenter @@ -65,7 +65,7 @@ collection: GlassBreak - !type:SpawnEntitiesBehavior spawn: - WallStationMapBroken: + StationMapBroken: min: 1 max: 1 - !type:DoActsBehavior