using System.IO;
using System.Linq;
using Content.Shared.Actions;
+using Content.Shared.Charges.Systems;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.ContentPack;
{
public delegate void OnActionReplaced(EntityUid actionId);
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
}
- public override void FrameUpdate(float frameTime)
- {
- base.FrameUpdate(frameTime);
-
- var worldActionQuery = EntityQueryEnumerator<WorldTargetActionComponent>();
- while (worldActionQuery.MoveNext(out var uid, out var action))
- {
- UpdateAction(uid, action);
- }
-
- var instantActionQuery = EntityQueryEnumerator<InstantActionComponent>();
- while (instantActionQuery.MoveNext(out var uid, out var action))
- {
- UpdateAction(uid, action);
- }
-
- var entityActionQuery = EntityQueryEnumerator<EntityTargetActionComponent>();
- while (entityActionQuery.MoveNext(out var uid, out var action))
- {
- UpdateAction(uid, action);
- }
- }
-
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
{
if (args.Current is not InstantActionComponentState state)
component.Toggled = state.Toggled;
component.Cooldown = state.Cooldown;
component.UseDelay = state.UseDelay;
- component.Charges = state.Charges;
- component.MaxCharges = state.MaxCharges;
- component.RenewCharges = state.RenewCharges;
component.Container = EnsureEntity<T>(state.Container, uid);
component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
component.CheckCanInteract = state.CheckCanInteract;
if (!ResolveActionData(actionId, ref action))
return;
- action.IconColor = action.Charges < 1 ? action.DisabledIconColor : action.OriginalIconColor;
+ // TODO: Decouple this.
+ action.IconColor = _sharedCharges.GetCurrentCharges(actionId.Value) == 0 ? action.DisabledIconColor : action.OriginalIconColor;
base.UpdateAction(actionId, action);
if (_playerManager.LocalEntity != action.AttachedEntity)
--- /dev/null
+using Content.Client.Actions;
+using Content.Shared.Actions;
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
+
+namespace Content.Client.Charges;
+
+public sealed class ChargesSystem : SharedChargesSystem
+{
+ [Dependency] private readonly ActionsSystem _actions = default!;
+
+ private Dictionary<EntityUid, int> _lastCharges = new();
+ private Dictionary<EntityUid, int> _tempLastCharges = new();
+
+ public override void Update(float frameTime)
+ {
+ // Technically this should probably be in frameupdate but no one will ever notice a tick of delay on this.
+ base.Update(frameTime);
+
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ // Update recharging actions. Server doesn't actually care about this and it's a waste of performance, actions are immediate.
+ var query = AllEntityQuery<AutoRechargeComponent, LimitedChargesComponent>();
+
+ while (query.MoveNext(out var uid, out var recharge, out var charges))
+ {
+ BaseActionComponent? actionComp = null;
+
+ if (!_actions.ResolveActionData(uid, ref actionComp, logError: false))
+ continue;
+
+ var current = GetCurrentCharges((uid, charges, recharge));
+
+ if (!_lastCharges.TryGetValue(uid, out var last) || current != last)
+ {
+ _actions.UpdateAction(uid, actionComp);
+ }
+
+ _tempLastCharges[uid] = current;
+ }
+
+ _lastCharges.Clear();
+
+ foreach (var (uid, value) in _tempLastCharges)
+ {
+ _lastCharges[uid] = value;
+ }
+
+ _tempLastCharges.Clear();
+ }
+}
+++ /dev/null
-using Content.Shared.Charges.Systems;
-
-namespace Content.Client.Charges.Systems;
-
-public sealed class ChargesSystem : SharedChargesSystem { }
using Content.Client.UserInterface.Systems.Actions.Windows;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Shared.Actions;
+using Content.Shared.Charges.Systems;
using Content.Shared.Input;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
[Dependency] private readonly IOverlayManager _overlays = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IInputManager _input = default!;
+ [UISystemDependency] private readonly SharedChargesSystem _sharedCharges = default!;
[UISystemDependency] private readonly ActionsSystem? _actionsSystem = default;
[UISystemDependency] private readonly InteractionOutlineSystem? _interactionOutline = default;
[UISystemDependency] private readonly TargetOutlineSystem? _targetOutline = default;
// Is the action currently valid?
if (!action.Enabled
- || action is { Charges: 0, RenewCharges: false }
|| action.Cooldown.HasValue && action.Cooldown.Value.End > _timing.CurTime)
{
// The user is targeting with this action, but it is not valid. Maybe mark this click as
continue;
}
- var button = new ActionButton(_entMan, _spriteSystem, this) {Locked = true};
+ var button = new ActionButton(EntityManager, _spriteSystem, this) {Locked = true};
button.ActionPressed += OnWindowActionPressed;
button.ActionUnpressed += OnWindowActionUnPressed;
button.ActionFocusExited += OnWindowActionFocusExisted;
using Content.Client.Cooldown;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
+using Content.Shared.Charges.Components;
+using Content.Shared.Charges.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
private IEntityManager _entities;
private SpriteSystem? _spriteSys;
private ActionUIController? _controller;
+ private SharedChargesSystem _sharedChargesSys;
private bool _beingHovered;
private bool _depressed;
private bool _toggled;
+ private int _lastCharges;
+
public BoundKeyFunction? KeyBind
{
set
_entities = entities;
_spriteSys = spriteSys;
+ _sharedChargesSys = _entities.System<SharedChargesSystem>();
_controller = controller;
MouseFilter = MouseFilterMode.Pass;
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
+ FormattedMessage? chargesText = null;
- if (_action is { Charges: not null })
+ // TODO: Don't touch this use an event make callers able to add their own shit for actions or I kill you.
+ if (_entities.TryGetComponent(ActionId, out LimitedChargesComponent? actionCharges))
{
- var charges = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {_action.Charges.Value.ToString()}/{_action.MaxCharges.ToString()}"));
- return new ActionAlertTooltip(name, decr, charges: charges);
+ var charges = _sharedChargesSys.GetCurrentCharges((ActionId.Value, actionCharges, null));
+ chargesText = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {charges.ToString()}/{actionCharges.MaxCharges}"));
+
+ if (_entities.TryGetComponent(ActionId, out AutoRechargeComponent? autoRecharge))
+ {
+ var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((ActionId.Value, actionCharges, autoRecharge));
+ chargesText.AddText(Loc.GetString($"{Environment.NewLine}Time Til Recharge: {chargeTimeRemaining}"));
+ }
}
- return new ActionAlertTooltip(name, decr);
+ return new ActionAlertTooltip(name, decr, charges: chargesText);
}
protected override void ControlFocusExited()
--- /dev/null
+using Content.Shared.Charges.Systems;
+
+namespace Content.Server.Charges;
+
+public sealed class ChargesSystem : SharedChargesSystem
+{
+
+}
+++ /dev/null
-using Content.Server.Charges.Systems;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-
-namespace Content.Server.Charges.Components;
-
-/// <summary>
-/// Something with limited charges that can be recharged automatically.
-/// Requires LimitedChargesComponent to function.
-/// </summary>
-// TODO: no reason this cant be predicted and server system deleted
-[RegisterComponent, AutoGenerateComponentPause]
-[Access(typeof(ChargesSystem))]
-public sealed partial class AutoRechargeComponent : Component
-{
- /// <summary>
- /// The time it takes to regain a single charge
- /// </summary>
- [DataField("rechargeDuration"), ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan RechargeDuration = TimeSpan.FromSeconds(90);
-
- /// <summary>
- /// The time when the next charge will be added
- /// </summary>
- [DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
- [AutoPausedField]
- public TimeSpan NextChargeTime;
-}
+++ /dev/null
-using Content.Server.Charges.Components;
-using Content.Shared.Charges.Components;
-using Content.Shared.Charges.Systems;
-using Content.Shared.Examine;
-using Robust.Shared.Timing;
-
-namespace Content.Server.Charges.Systems;
-
-public sealed class ChargesSystem : SharedChargesSystem
-{
- [Dependency] private readonly IGameTiming _timing = default!;
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var query = EntityQueryEnumerator<LimitedChargesComponent, AutoRechargeComponent>();
- while (query.MoveNext(out var uid, out var charges, out var recharge))
- {
- if (charges.Charges == charges.MaxCharges || _timing.CurTime < recharge.NextChargeTime)
- continue;
-
- AddCharges(uid, 1, charges);
- recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration;
- }
- }
-
- protected override void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
- {
- base.OnExamine(uid, comp, args);
-
- // only show the recharging info if it's not full
- if (!args.IsInDetailsRange || comp.Charges == comp.MaxCharges || !TryComp<AutoRechargeComponent>(uid, out var recharge))
- return;
-
- var timeRemaining = Math.Round((recharge.NextChargeTime - _timing.CurTime).TotalSeconds);
- args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining)));
- }
-
- public override void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null)
- {
- if (!Query.Resolve(uid, ref comp, false))
- return;
-
- var startRecharge = comp.Charges == comp.MaxCharges;
- base.AddCharges(uid, change, comp);
-
- // if a charge was just used from full, start the recharge timer
- // TODO: probably make this an event instead of having le server system that just does this
- if (change < 0 && startRecharge && TryComp<AutoRechargeComponent>(uid, out var recharge))
- recharge.NextChargeTime = _timing.CurTime + recharge.RechargeDuration;
- }
-}
{
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly AudioSystem _audio = default!;
- [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
return false;
TryComp<LimitedChargesComponent>(uid, out var charges);
- if (_charges.IsEmpty(uid, charges))
+ if (_sharedCharges.IsEmpty((uid, charges)))
return false;
- _charges.UseCharge(uid, charges);
+ _sharedCharges.TryUseCharge((uid, charges));
_audio.PlayPvs(comp.Sound, uid);
comp.Flashing = true;
_appearance.SetData(uid, FlashVisuals.Flashing, true);
- if (_charges.IsEmpty(uid, charges))
+ if (_sharedCharges.IsEmpty((uid, charges)))
{
_appearance.SetData(uid, FlashVisuals.Burnt, true);
_tag.AddTag(uid, TrashTag);
/// </summary>
[DataField("useDelay")] public TimeSpan? UseDelay;
- /// <summary>
- /// Convenience tool for actions with limited number of charges. Automatically decremented on use, and the
- /// action is disabled when it reaches zero. Does NOT automatically remove the action from the action bar.
- /// However, charges will regenerate if <see cref="RenewCharges"/> is enabled and the action will not disable
- /// when charges reach zero.
- /// </summary>
- [DataField("charges")] public int? Charges;
-
- /// <summary>
- /// The max charges this action has. If null, this is set automatically from <see cref="Charges"/> on mapinit.
- /// </summary>
- [DataField] public int? MaxCharges;
-
- /// <summary>
- /// If enabled, charges will regenerate after a <see cref="Cooldown"/> is complete
- /// </summary>
- [DataField("renewCharges")]public bool RenewCharges;
-
/// <summary>
/// The entity that contains this action. If the action is innate, this may be the user themselves.
/// This should almost always be non-null.
public bool Toggled;
public (TimeSpan Start, TimeSpan End)? Cooldown;
public TimeSpan? UseDelay;
- public int? Charges;
- public int? MaxCharges;
- public bool RenewCharges;
public NetEntity? Container;
public NetEntity? EntityIcon;
public bool CheckCanInteract;
Toggled = component.Toggled;
Cooldown = component.Cooldown;
UseDelay = component.UseDelay;
- Charges = component.Charges;
- MaxCharges = component.MaxCharges;
- RenewCharges = component.RenewCharges;
CheckCanInteract = component.CheckCanInteract;
CheckConsciousness = component.CheckConsciousness;
ClientExclusive = component.ClientExclusive;
public abstract class SharedActionsSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming GameTiming = default!;
- [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
- [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
- [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
- [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
- [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
- [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
+ [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
+ [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
+ [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
public override void Initialize()
{
SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
}
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var worldActionQuery = EntityQueryEnumerator<WorldTargetActionComponent>();
- while (worldActionQuery.MoveNext(out var uid, out var action))
- {
- if (IsCooldownActive(action) || !ShouldResetCharges(action))
- continue;
-
- ResetCharges(uid, dirty: true);
- }
-
- var instantActionQuery = EntityQueryEnumerator<InstantActionComponent>();
- while (instantActionQuery.MoveNext(out var uid, out var action))
- {
- if (IsCooldownActive(action) || !ShouldResetCharges(action))
- continue;
-
- ResetCharges(uid, dirty: true);
- }
-
- var entityActionQuery = EntityQueryEnumerator<EntityTargetActionComponent>();
- while (entityActionQuery.MoveNext(out var uid, out var action))
- {
- if (IsCooldownActive(action) || !ShouldResetCharges(action))
- continue;
-
- ResetCharges(uid, dirty: true);
- }
- }
-
private void OnActionMapInit(EntityUid uid, BaseActionComponent component, MapInitEvent args)
{
component.OriginalIconColor = component.IconColor;
-
- if (component.Charges == null)
- return;
-
- component.MaxCharges ??= component.Charges.Value;
- Dirty(uid, component);
}
private void OnActionShutdown(EntityUid uid, BaseActionComponent component, ComponentShutdown args)
Dirty(actionId.Value, action);
}
- public void SetCharges(EntityUid? actionId, int? charges)
- {
- if (!TryGetActionData(actionId, out var action) ||
- action.Charges == charges)
- {
- return;
- }
-
- action.Charges = charges;
- UpdateAction(actionId, action);
- Dirty(actionId.Value, action);
- }
-
- public int? GetCharges(EntityUid? actionId)
- {
- if (!TryGetActionData(actionId, out var action))
- return null;
-
- return action.Charges;
- }
-
- public void AddCharges(EntityUid? actionId, int addCharges)
- {
- if (!TryGetActionData(actionId, out var action) || action.Charges == null || addCharges < 1)
- return;
-
- action.Charges += addCharges;
- UpdateAction(actionId, action);
- Dirty(actionId.Value, action);
- }
-
- public void RemoveCharges(EntityUid? actionId, int? removeCharges)
- {
- if (!TryGetActionData(actionId, out var action) || action.Charges == null)
- return;
-
- if (removeCharges == null)
- action.Charges = removeCharges;
- else
- action.Charges -= removeCharges;
-
- if (action.Charges is < 0)
- action.Charges = null;
-
- UpdateAction(actionId, action);
- Dirty(actionId.Value, action);
- }
-
- public void ResetCharges(EntityUid? actionId, bool update = false, bool dirty = false)
- {
- if (!TryGetActionData(actionId, out var action))
- return;
-
- action.Charges = action.MaxCharges;
-
- if (update)
- UpdateAction(actionId, action);
-
- if (dirty)
- Dirty(actionId.Value, action);
- }
-
private void OnActionsGetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
{
args.State = new ActionsComponentState(GetNetEntitySet(component.Actions));
if (!action.Enabled)
return;
+ var curTime = GameTiming.CurTime;
+ if (IsCooldownActive(action, curTime))
+ return;
+
// check for action use prevention
// TODO: make code below use this event with a dedicated component
var attemptEv = new ActionAttemptEvent(user);
if (attemptEv.Cancelled)
return;
- var curTime = GameTiming.CurTime;
- if (IsCooldownActive(action, curTime))
- return;
-
- // TODO: Replace with individual charge recovery when we have the visuals to aid it
- if (action is { Charges: < 1, RenewCharges: true })
- ResetCharges(actionEnt, true, true);
-
BaseActionEvent? performEvent = null;
if (action.CheckConsciousness && !_actionBlockerSystem.CanConsciouslyPerformAction(user))
var dirty = toggledBefore != action.Toggled;
- if (action.Charges != null)
- {
- dirty = true;
- action.Charges--;
- if (action is { Charges: 0, RenewCharges: false })
- action.Enabled = false;
- }
-
action.Cooldown = null;
- if (action is { UseDelay: not null, Charges: null or < 1 })
+ if (action is { UseDelay: not null})
{
dirty = true;
action.Cooldown = (curTime, curTime + action.UseDelay.Value);
if (!action.Enabled)
return false;
- if (action.Charges.HasValue && action.Charges <= 0)
- return false;
var curTime = GameTiming.CurTime;
if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime)
/// <summary>
/// Checks if the action has a cooldown and if it's still active
/// </summary>
- protected bool IsCooldownActive(BaseActionComponent action, TimeSpan? curTime = null)
+ public bool IsCooldownActive(BaseActionComponent action, TimeSpan? curTime = null)
{
- curTime ??= GameTiming.CurTime;
// TODO: Check for charge recovery timer
return action.Cooldown.HasValue && action.Cooldown.Value.End > curTime;
}
-
- protected bool ShouldResetCharges(BaseActionComponent action)
- {
- return action is { Charges: < 1, RenewCharges: true };
- }
}
--- /dev/null
+using Content.Shared.Charges.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Charges.Components;
+
+/// <summary>
+/// Something with limited charges that can be recharged automatically.
+/// Requires LimitedChargesComponent to function.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedChargesSystem))]
+public sealed partial class AutoRechargeComponent : Component
+{
+ /// <summary>
+ /// The time it takes to regain a single charge
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public TimeSpan RechargeDuration = TimeSpan.FromSeconds(90);
+}
using Content.Shared.Charges.Systems;
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Charges.Components;
-[RegisterComponent, NetworkedComponent]
-[Access(typeof(SharedChargesSystem))]
-[AutoGenerateComponentState]
+/// <summary>
+/// Specifies the attached action has discrete charges, separate to a cooldown.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedChargesSystem))]
public sealed partial class LimitedChargesComponent : Component
{
+ [DataField, AutoNetworkedField]
+ public int LastCharges;
+
/// <summary>
- /// The maximum number of charges
+ /// The max charges this action has.
/// </summary>
- [DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite)]
- [AutoNetworkedField]
- public int MaxCharges = 3;
+ [DataField, AutoNetworkedField, Access(Other = AccessPermissions.Read)]
+ public int MaxCharges = 1;
/// <summary>
- /// The current number of charges
+ /// Last time charges was changed. Used to derive current charges.
/// </summary>
- [DataField("charges"), ViewVariables(VVAccess.ReadWrite)]
- [AutoNetworkedField]
- public int Charges = 3;
+ [DataField(customTypeSerializer:typeof(TimeOffsetSerializer)), AutoNetworkedField]
+ public TimeSpan LastUpdate;
}
+using Content.Shared.Actions.Events;
using Content.Shared.Charges.Components;
using Content.Shared.Examine;
+using JetBrains.Annotations;
+using Robust.Shared.Timing;
namespace Content.Shared.Charges.Systems;
public abstract class SharedChargesSystem : EntitySystem
{
- protected EntityQuery<LimitedChargesComponent> Query;
+ [Dependency] protected readonly IGameTiming _timing = default!;
+
+ /*
+ * Despite what a bunch of systems do you don't need to continuously tick linear number updates and can just derive it easily.
+ */
public override void Initialize()
{
base.Initialize();
- Query = GetEntityQuery<LimitedChargesComponent>();
-
SubscribeLocalEvent<LimitedChargesComponent, ExaminedEvent>(OnExamine);
+
+ SubscribeLocalEvent<LimitedChargesComponent, ActionAttemptEvent>(OnChargesAttempt);
+ SubscribeLocalEvent<LimitedChargesComponent, MapInitEvent>(OnChargesMapInit);
+ SubscribeLocalEvent<LimitedChargesComponent, ActionPerformedEvent>(OnChargesPerformed);
}
- protected virtual void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
+ private void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
- using (args.PushGroup(nameof(LimitedChargesComponent)))
+ var rechargeEnt = new Entity<LimitedChargesComponent?, AutoRechargeComponent?>(uid, comp, null);
+ var charges = GetCurrentCharges(rechargeEnt);
+ using var _ = args.PushGroup(nameof(LimitedChargesComponent));
+
+ args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", charges)));
+ if (charges == comp.MaxCharges)
{
- args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", comp.Charges)));
- if (comp.Charges == comp.MaxCharges)
- {
- args.PushMarkup(Loc.GetString("limited-charges-max-charges"));
- }
+ args.PushMarkup(Loc.GetString("limited-charges-max-charges"));
}
+
+ // only show the recharging info if it's not full
+ if (charges == comp.MaxCharges || !TryComp<AutoRechargeComponent>(uid, out var recharge))
+ return;
+
+ rechargeEnt.Comp2 = recharge;
+ var timeRemaining = GetNextRechargeTime(rechargeEnt);
+ args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining.TotalSeconds.ToString("F1"))));
}
- /// <summary>
- /// Tries to add a number of charges. If it over or underflows it will be clamped, wasting the extra charges.
- /// </summary>
- public virtual void AddCharges(EntityUid uid, int change, LimitedChargesComponent? comp = null)
+ private void OnChargesAttempt(Entity<LimitedChargesComponent> ent, ref ActionAttemptEvent args)
{
- if (!Query.Resolve(uid, ref comp, false))
+ if (args.Cancelled)
return;
- var old = comp.Charges;
- comp.Charges = Math.Clamp(comp.Charges + change, 0, comp.MaxCharges);
- if (comp.Charges != old)
- Dirty(uid, comp);
+ var charges = GetCurrentCharges((ent.Owner, ent.Comp, null));
+
+ if (charges <= 0)
+ {
+ args.Cancelled = true;
+ }
+ }
+
+ private void OnChargesPerformed(Entity<LimitedChargesComponent> ent, ref ActionPerformedEvent args)
+ {
+ AddCharges((ent.Owner, ent.Comp), -1);
+ }
+
+ private void OnChargesMapInit(Entity<LimitedChargesComponent> ent, ref MapInitEvent args)
+ {
+ // If nothing specified use max.
+ if (ent.Comp.LastCharges == 0)
+ {
+ ent.Comp.LastCharges = ent.Comp.MaxCharges;
+ }
+ // If -1 used then we don't want any.
+ else if (ent.Comp.LastCharges < 0)
+ {
+ ent.Comp.LastCharges = 0;
+ }
+
+ ent.Comp.LastUpdate = _timing.CurTime;
+ Dirty(ent);
+ }
+
+ [Pure]
+ public bool HasCharges(Entity<LimitedChargesComponent?> action, int charges)
+ {
+ var current = GetCurrentCharges(action);
+
+ return current >= charges;
}
/// <summary>
- /// Gets the limited charges component and returns true if there are no charges. Will return false if there is no limited charges component.
+ /// Adds the specified charges. Does not reset the accumulator.
/// </summary>
- public bool IsEmpty(EntityUid uid, LimitedChargesComponent? comp = null)
+ public void AddCharges(Entity<LimitedChargesComponent?> action, int addCharges)
{
- // can't be empty if there are no limited charges
- if (!Query.Resolve(uid, ref comp, false))
+ if (addCharges == 0)
+ return;
+
+ action.Comp ??= EnsureComp<LimitedChargesComponent>(action.Owner);
+
+ // 1. If we're going FROM max then set lastupdate to now (so it doesn't instantly recharge).
+ // 2. If we're going TO max then also set lastupdate to now.
+ // 3. Otherwise don't modify it.
+ // No idea if we go to 0 but future problem.
+
+ var lastCharges = GetCurrentCharges(action);
+ var charges = lastCharges + addCharges;
+
+ if (lastCharges == charges)
+ return;
+
+ if (charges == action.Comp.MaxCharges || lastCharges == action.Comp.MaxCharges)
+ {
+ action.Comp.LastUpdate = _timing.CurTime;
+ }
+
+ action.Comp.LastCharges = Math.Clamp(action.Comp.LastCharges + addCharges, 0, action.Comp.MaxCharges);
+ Dirty(action);
+ }
+
+ public bool TryUseCharge(Entity<LimitedChargesComponent?> entity)
+ {
+ return TryUseCharges(entity, 1);
+ }
+
+ public bool TryUseCharges(Entity<LimitedChargesComponent?> entity, int amount)
+ {
+ var current = GetCurrentCharges(entity);
+
+ if (current < amount)
+ {
return false;
+ }
- return comp.Charges <= 0;
+ AddCharges(entity, -amount);
+ return true;
}
- /// <summary>
- /// Uses a single charge. Must check IsEmpty beforehand to prevent using with 0 charge.
- /// </summary>
- public void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null)
+ [Pure]
+ public bool IsEmpty(Entity<LimitedChargesComponent?> entity)
{
- AddCharges(uid, -1, comp);
+ return GetCurrentCharges(entity) == 0;
}
/// <summary>
- /// Checks IsEmpty and uses a charge if it isn't empty.
+ /// Resets action charges to MaxCharges.
/// </summary>
- public bool TryUseCharge(Entity<LimitedChargesComponent?> ent)
+ public void ResetCharges(Entity<LimitedChargesComponent?> action)
{
- if (!Query.Resolve(ent, ref ent.Comp, false))
- return true;
+ if (!Resolve(action.Owner, ref action.Comp, false))
+ return;
- if (IsEmpty(ent, ent.Comp))
- return false;
+ var charges = GetCurrentCharges((action.Owner, action.Comp, null));
- UseCharge(ent, ent.Comp);
- return true;
+ if (charges == action.Comp.MaxCharges)
+ return;
+
+ action.Comp.LastCharges = action.Comp.MaxCharges;
+ action.Comp.LastUpdate = _timing.CurTime;
+ Dirty(action);
+ }
+
+ public void SetCharges(Entity<LimitedChargesComponent?> action, int value)
+ {
+ action.Comp ??= EnsureComp<LimitedChargesComponent>(action.Owner);
+
+ var adjusted = Math.Clamp(value, 0, action.Comp.MaxCharges);
+
+ if (action.Comp.LastCharges == adjusted)
+ {
+ return;
+ }
+
+ action.Comp.LastCharges = adjusted;
+ action.Comp.LastUpdate = _timing.CurTime;
+ Dirty(action);
}
/// <summary>
- /// Gets the limited charges component and returns true if the number of charges remaining is less than the specified value.
- /// Will return false if there is no limited charges component.
+ /// The next time a charge will be considered to be filled.
/// </summary>
- public bool HasInsufficientCharges(EntityUid uid, int requiredCharges, LimitedChargesComponent? comp = null)
+ /// <returns>0 timespan if invalid or no charges to generate.</returns>
+ [Pure]
+ public TimeSpan GetNextRechargeTime(Entity<LimitedChargesComponent?, AutoRechargeComponent?> entity)
{
- // can't be empty if there are no limited charges
- if (!Resolve(uid, ref comp, false))
- return false;
+ if (!Resolve(entity.Owner, ref entity.Comp1, ref entity.Comp2, false))
+ {
+ return TimeSpan.Zero;
+ }
- return comp.Charges < requiredCharges;
+ // Okay so essentially we need to get recharge time to full, then modulus that by the recharge timer which should be the next tick.
+ var fullTime = ((entity.Comp1.MaxCharges - entity.Comp1.LastCharges) * entity.Comp2.RechargeDuration) + entity.Comp1.LastUpdate;
+ var timeRemaining = fullTime - _timing.CurTime;
+
+ if (timeRemaining < TimeSpan.Zero)
+ {
+ return TimeSpan.Zero;
+ }
+
+ var nextChargeTime = timeRemaining.TotalSeconds % entity.Comp2.RechargeDuration.TotalSeconds;
+ return TimeSpan.FromSeconds(nextChargeTime);
}
/// <summary>
- /// Uses up a specified number of charges. Must check HasInsufficentCharges beforehand to prevent using with insufficient remaining charges.
+ /// Derives the current charges of an entity.
/// </summary>
- public virtual void UseCharges(EntityUid uid, int chargesUsed, LimitedChargesComponent? comp = null)
+ [Pure]
+ public int GetCurrentCharges(Entity<LimitedChargesComponent?, AutoRechargeComponent?> entity)
{
- AddCharges(uid, -chargesUsed, comp);
+ if (!Resolve(entity.Owner, ref entity.Comp1, false))
+ {
+ // I'm all in favor of nullable ints however null-checking return args against comp nullability is dodgy
+ // so we get this.
+ return -1;
+ }
+
+ var calculated = 0;
+
+ if (Resolve(entity.Owner, ref entity.Comp2, false) && entity.Comp2.RechargeDuration.TotalSeconds != 0.0)
+ {
+ calculated = (int)((_timing.CurTime - entity.Comp1.LastUpdate).TotalSeconds / entity.Comp2.RechargeDuration.TotalSeconds);
+ }
+
+ return Math.Clamp(entity.Comp1.LastCharges + calculated,
+ 0,
+ entity.Comp1.MaxCharges);
}
}
public sealed class EmagSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
- [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
if (_tag.HasTag(target, ent.Comp.EmagImmuneTag))
return false;
- TryComp<LimitedChargesComponent>(ent, out var charges);
- if (_charges.IsEmpty(ent, charges))
+ Entity<LimitedChargesComponent?> chargesEnt = ent.Owner;
+ if (_sharedCharges.IsEmpty(chargesEnt))
{
_popup.PopupClient(Loc.GetString("emag-no-charges"), user, user);
return false;
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target} with flag(s): {ent.Comp.EmagType}");
- if (charges != null && emaggedEvent.Handled)
- _charges.UseCharge(ent, charges);
+ if (emaggedEvent.Handled)
+ _sharedCharges.TryUseCharge(chargesEnt);
if (!emaggedEvent.Repeatable)
{
using Content.Shared.Actions;
+using Content.Shared.Charges.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Interaction.Events;
using Content.Shared.Magic.Components;
public sealed class SpellbookSystem : EntitySystem
{
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
if (spell == null)
continue;
- int? charge = charges;
- if (_actions.GetCharges(spell) != null)
- charge = _actions.GetCharges(spell);
-
- _actions.SetCharges(spell, charge < 0 ? null : charge);
+ _sharedCharges.SetCharges(spell.Value, charges);
ent.Comp.Spells.Add(spell.Value);
}
}
{
EntityUid? actionId = null;
if (_actions.AddAction(args.Args.User, ref actionId, id))
- _actions.SetCharges(actionId, charges < 0 ? null : charges);
+ _sharedCharges.SetCharges(actionId.Value, charges);
}
}
{
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
return;
}
- if (!_charges.TryUseCharge(uid))
+ if (!_sharedCharges.TryUseCharge(uid))
{
_popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user);
return;
private void OnCheckDash(Entity<EnergyKatanaComponent> ent, ref CheckDashEvent args)
{
+ // Just use a whitelist fam
if (!_ninja.IsNinja(args.User))
args.Cancelled = true;
}
/// How many charges are contained in this ammo cartridge.
/// Can be partially transferred into an RCD, until it is empty then it gets deleted.
/// </summary>
- [DataField("charges"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public int Charges = 30;
}
public sealed class RCDAmmoSystem : EntitySystem
{
- [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IGameTiming _timing = default!;
!TryComp<LimitedChargesComponent>(target, out var charges))
return;
+ var current = _sharedCharges.GetCurrentCharges((target, charges));
var user = args.User;
args.Handled = true;
- var count = Math.Min(charges.MaxCharges - charges.Charges, comp.Charges);
+ var count = Math.Min(charges.MaxCharges - current, comp.Charges);
if (count <= 0)
{
_popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-full"), target, user);
}
_popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-refilled"), target, user);
- _charges.AddCharges(target, count, charges);
+ _sharedCharges.AddCharges(target, count);
comp.Charges -= count;
Dirty(uid, comp);
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
[Dependency] private readonly FloorTileSystem _floors = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedChargesSystem _charges = default!;
+ [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TagSystem _tags = default!;
- [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
private readonly int _instantConstructionDelay = 0;
private readonly EntProtoId _instantConstructionFx = "EffectRCDConstruct0";
if (!location.IsValid(EntityManager))
return;
- var gridUid = _transformSystem.GetGrid(location);
+ var gridUid = _transform.GetGrid(location);
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
{
// Ensure the RCD operation is still valid
var location = GetCoordinates(args.Event.Location);
- var gridUid = _transformSystem.GetGrid(location);
+ var gridUid = _transform.GetGrid(location);
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
{
var location = GetCoordinates(args.Location);
- var gridUid = _transformSystem.GetGrid(location);
+ var gridUid = _transform.GetGrid(location);
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
return;
// Play audio and consume charges
_audio.PlayPredicted(component.SuccessSound, uid, args.User);
- _charges.UseCharges(uid, args.Cost);
+ _sharedCharges.AddCharges(uid, -args.Cost);
}
private void OnRCDconstructionGhostRotationEvent(RCDConstructionGhostRotationEvent ev, EntitySessionEventArgs session)
var prototype = _protoManager.Index(component.ProtoId);
// Check that the RCD has enough ammo to get the job done
- TryComp<LimitedChargesComponent>(uid, out var charges);
+ var charges = _sharedCharges.GetCurrentCharges(uid);
// Both of these were messages were suppose to be predicted, but HasInsufficientCharges wasn't being checked on the client for some reason?
- if (_charges.IsEmpty(uid, charges))
+ if (charges == 0)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
return false;
}
- if (_charges.HasInsufficientCharges(uid, prototype.Cost, charges))
+ if (prototype.Cost > charges)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-insufficient-ammo-message"), uid, user);
name: Break Free
description: Activating your freedom implant will free you from any hand restraints
components:
+ - type: LimitedCharges
+ maxCharges: 3
- type: InstantAction
- charges: 3
checkCanInteract: false
itemIconStyle: BigAction
priority: -20
name: Activate EMP
description: Triggers a small EMP pulse around you
components:
+ - type: LimitedCharges
+ maxCharges: 3
- type: InstantAction
checkCanInteract: false
- charges: 3
useDelay: 5
itemIconStyle: BigAction
priority: -20
name: SCRAM!
description: Randomly teleports you within a large distance.
components:
+ - type: LimitedCharges
+ maxCharges: 2
- type: InstantAction
checkCanInteract: false
- charges: 2
useDelay: 5
itemIconStyle: BigAction
priority: -20
components:
- type: ConfirmableAction
popup: dna-scrambler-action-popup
+ - type: LimitedCharges
+ maxCharges: 1
- type: InstantAction
- charges: 1
itemIconStyle: BigAction
priority: -20
icon:
- type: Flash
- type: LimitedCharges
maxCharges: 3
- charges: 3
- type: AutoRecharge
rechargeDuration: 30
- type: MeleeWeapon
- type: Flash
- type: LimitedCharges
maxCharges: 15
- charges: 15
- type: MeleeWeapon
damage:
types:
- Deconstruct
- type: LimitedCharges
maxCharges: 30
- charges: 30
- type: Sprite
sprite: Objects/Tools/rcd.rsi
state: icon
suffix: Empty
components:
- type: LimitedCharges
- charges: 0
+ lastCharges: -1
- type: entity
id: RCDRecharging
components:
- type: LimitedCharges
maxCharges: 20
- charges: 20
- type: AutoRecharge
rechargeDuration: 10
- type: DashAbility
- type: LimitedCharges
maxCharges: 3
- charges: 3
- type: AutoRecharge
rechargeDuration: 20
- type: Clothing
- type: Flash
- type: LimitedCharges
maxCharges: 5
- charges: 5
- type: MeleeWeapon
wideAnimationRotation: 180
damage:
components:
- type: LimitedCharges
maxCharges: 2
- charges: 2
- type: entity
name: portable flasher
name: Animate
description: Bring an inanimate object to life!
components:
+ - type: LimitedCharges
+ maxCharges: 5
- type: EntityTargetAction
useDelay: 0
- charges: 5
itemIconStyle: BigAction
whitelist:
components:
description: Fires a fireball, but faster!
components:
- type: WorldTargetAction
- useDelay: 10
- renewCharges: true
itemIconStyle: BigAction
checkCanAccess: false
raiseOnUser: true
description: The fastest fireball in the west!
components:
- type: WorldTargetAction
- useDelay: 8
- renewCharges: true
itemIconStyle: BigAction
checkCanAccess: false
raiseOnUser: true
- type: entity
id: ActionRgbLight
components:
+ - type: LimitedCharges
+ maxCharges: 25
- type: EntityTargetAction
whitelist: { components: [ PointLight ] }
- charges: 25
sound: /Audio/Magic/blink.ogg
event: !type:ChangeComponentsSpellEvent
toAdd: