Action charges refactor (#33993)
* Action charges refactor - Fixes the slight godmoding of baseactioncomponent. - Gets back 1ms of server time. * chorg * Remove FrameUpdate * Fixes * More fixes * Combine * Fixes * Updates * weh * Last fixes * weh * Fix naughty * YAML fixes * This one too * Merge conflicts * This thing * Review * Fix this as well * Icon fix * weh * Review * Review * seamless * Review
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -22,6 +23,7 @@ namespace Content.Client.Actions
|
||||
{
|
||||
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!;
|
||||
@@ -51,29 +53,6 @@ namespace Content.Client.Actions
|
||||
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)
|
||||
@@ -127,9 +106,6 @@ namespace Content.Client.Actions
|
||||
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;
|
||||
@@ -152,7 +128,8 @@ namespace Content.Client.Actions
|
||||
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)
|
||||
|
||||
52
Content.Client/Charges/ChargesSystem.cs
Normal file
52
Content.Client/Charges/ChargesSystem.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
using Content.Shared.Charges.Systems;
|
||||
|
||||
namespace Content.Client.Charges.Systems;
|
||||
|
||||
public sealed class ChargesSystem : SharedChargesSystem { }
|
||||
@@ -12,6 +12,7 @@ using Content.Client.UserInterface.Systems.Actions.Widgets;
|
||||
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;
|
||||
@@ -42,9 +43,9 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
|
||||
[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;
|
||||
@@ -173,7 +174,6 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
|
||||
|
||||
// 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
|
||||
@@ -483,7 +483,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
|
||||
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;
|
||||
|
||||
@@ -4,6 +4,8 @@ using Content.Client.Actions.UI;
|
||||
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;
|
||||
@@ -22,10 +24,13 @@ public sealed class ActionButton : Control, IEntityControl
|
||||
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
|
||||
@@ -65,6 +70,7 @@ public sealed class ActionButton : Control, IEntityControl
|
||||
|
||||
_entities = entities;
|
||||
_spriteSys = spriteSys;
|
||||
_sharedChargesSys = _entities.System<SharedChargesSystem>();
|
||||
_controller = controller;
|
||||
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
@@ -194,14 +200,22 @@ public sealed class ActionButton : Control, IEntityControl
|
||||
|
||||
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()
|
||||
|
||||
8
Content.Server/Charges/ChargesSystem.cs
Normal file
8
Content.Server/Charges/ChargesSystem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Charges.Systems;
|
||||
|
||||
namespace Content.Server.Charges;
|
||||
|
||||
public sealed class ChargesSystem : SharedChargesSystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ namespace Content.Server.Flash
|
||||
{
|
||||
[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!;
|
||||
@@ -86,15 +86,15 @@ namespace Content.Server.Flash
|
||||
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);
|
||||
|
||||
@@ -86,24 +86,6 @@ public abstract partial class BaseActionComponent : Component
|
||||
/// </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.
|
||||
@@ -209,9 +191,6 @@ public abstract class BaseActionComponentState : ComponentState
|
||||
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;
|
||||
@@ -243,9 +222,6 @@ public abstract class BaseActionComponentState : ComponentState
|
||||
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;
|
||||
|
||||
@@ -21,14 +21,14 @@ namespace Content.Shared.Actions;
|
||||
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()
|
||||
{
|
||||
@@ -69,47 +69,9 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
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)
|
||||
@@ -312,68 +274,6 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
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));
|
||||
@@ -416,6 +316,10 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
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);
|
||||
@@ -423,14 +327,6 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
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))
|
||||
@@ -705,16 +601,8 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
|
||||
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);
|
||||
@@ -1014,8 +902,6 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
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)
|
||||
@@ -1125,15 +1011,9 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
/// <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 };
|
||||
}
|
||||
}
|
||||
|
||||
19
Content.Shared/Charges/Components/AutoRechargeComponent.cs
Normal file
19
Content.Shared/Charges/Components/AutoRechargeComponent.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum number of charges
|
||||
/// </summary>
|
||||
[DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[AutoNetworkedField]
|
||||
public int MaxCharges = 3;
|
||||
[DataField, AutoNetworkedField]
|
||||
public int LastCharges;
|
||||
|
||||
/// <summary>
|
||||
/// The current number of charges
|
||||
/// The max charges this action has.
|
||||
/// </summary>
|
||||
[DataField("charges"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[AutoNetworkedField]
|
||||
public int Charges = 3;
|
||||
[DataField, AutoNetworkedField, Access(Other = AccessPermissions.Read)]
|
||||
public int MaxCharges = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Last time charges was changed. Used to derive current charges.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer:typeof(TimeOffsetSerializer)), AutoNetworkedField]
|
||||
public TimeSpan LastUpdate;
|
||||
}
|
||||
|
||||
@@ -1,103 +1,232 @@
|
||||
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"))));
|
||||
}
|
||||
|
||||
private void OnChargesAttempt(Entity<LimitedChargesComponent> ent, ref ActionAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
var charges = GetCurrentCharges((ent.Owner, ent.Comp, null));
|
||||
|
||||
if (charges <= 0)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 OnChargesPerformed(Entity<LimitedChargesComponent> ent, ref ActionPerformedEvent args)
|
||||
{
|
||||
if (!Query.Resolve(uid, ref comp, false))
|
||||
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>
|
||||
/// Adds the specified charges. Does not reset the accumulator.
|
||||
/// </summary>
|
||||
public void AddCharges(Entity<LimitedChargesComponent?> action, int addCharges)
|
||||
{
|
||||
if (addCharges == 0)
|
||||
return;
|
||||
|
||||
var old = comp.Charges;
|
||||
comp.Charges = Math.Clamp(comp.Charges + change, 0, comp.MaxCharges);
|
||||
if (comp.Charges != old)
|
||||
Dirty(uid, comp);
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the limited charges component and returns true if there are no charges. Will return false if there is no limited charges component.
|
||||
/// </summary>
|
||||
public bool IsEmpty(EntityUid uid, LimitedChargesComponent? comp = null)
|
||||
public bool TryUseCharge(Entity<LimitedChargesComponent?> entity)
|
||||
{
|
||||
// can't be empty if there are no limited charges
|
||||
if (!Query.Resolve(uid, ref comp, false))
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses a single charge. Must check IsEmpty beforehand to prevent using with 0 charge.
|
||||
/// </summary>
|
||||
public void UseCharge(EntityUid uid, LimitedChargesComponent? comp = null)
|
||||
{
|
||||
AddCharges(uid, -1, comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks IsEmpty and uses a charge if it isn't empty.
|
||||
/// </summary>
|
||||
public bool TryUseCharge(Entity<LimitedChargesComponent?> ent)
|
||||
{
|
||||
if (!Query.Resolve(ent, ref ent.Comp, false))
|
||||
return true;
|
||||
|
||||
if (IsEmpty(ent, ent.Comp))
|
||||
return false;
|
||||
|
||||
UseCharge(ent, ent.Comp);
|
||||
AddCharges(entity, -amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public bool HasInsufficientCharges(EntityUid uid, int requiredCharges, LimitedChargesComponent? comp = null)
|
||||
[Pure]
|
||||
public bool IsEmpty(Entity<LimitedChargesComponent?> entity)
|
||||
{
|
||||
// can't be empty if there are no limited charges
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return false;
|
||||
|
||||
return comp.Charges < requiredCharges;
|
||||
return GetCurrentCharges(entity) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses up a specified number of charges. Must check HasInsufficentCharges beforehand to prevent using with insufficient remaining charges.
|
||||
/// Resets action charges to MaxCharges.
|
||||
/// </summary>
|
||||
public virtual void UseCharges(EntityUid uid, int chargesUsed, LimitedChargesComponent? comp = null)
|
||||
public void ResetCharges(Entity<LimitedChargesComponent?> action)
|
||||
{
|
||||
AddCharges(uid, -chargesUsed, comp);
|
||||
if (!Resolve(action.Owner, ref action.Comp, false))
|
||||
return;
|
||||
|
||||
var charges = GetCurrentCharges((action.Owner, action.Comp, null));
|
||||
|
||||
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>
|
||||
/// The next time a charge will be considered to be filled.
|
||||
/// </summary>
|
||||
/// <returns>0 timespan if invalid or no charges to generate.</returns>
|
||||
[Pure]
|
||||
public TimeSpan GetNextRechargeTime(Entity<LimitedChargesComponent?, AutoRechargeComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity.Owner, ref entity.Comp1, ref entity.Comp2, false))
|
||||
{
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
// 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>
|
||||
/// Derives the current charges of an entity.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public int GetCurrentCharges(Entity<LimitedChargesComponent?, AutoRechargeComponent?> entity)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Content.Shared.Emag.Systems;
|
||||
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!;
|
||||
@@ -61,8 +61,8 @@ public sealed class EmagSystem : EntitySystem
|
||||
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;
|
||||
@@ -80,8 +80,8 @@ public sealed class EmagSystem : EntitySystem
|
||||
|
||||
_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)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Charges.Systems;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Magic.Components;
|
||||
@@ -9,6 +10,7 @@ namespace Content.Shared.Magic;
|
||||
|
||||
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!;
|
||||
@@ -30,11 +32,7 @@ public sealed class SpellbookSystem : EntitySystem
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +73,7 @@ public sealed class SpellbookSystem : EntitySystem
|
||||
{
|
||||
EntityUid? actionId = null;
|
||||
if (_actions.AddAction(args.Args.User, ref actionId, id))
|
||||
_actions.SetCharges(actionId, charges < 0 ? null : charges);
|
||||
_sharedCharges.SetCharges(actionId.Value, charges);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class DashAbilitySystem : EntitySystem
|
||||
{
|
||||
[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!;
|
||||
@@ -79,7 +79,7 @@ public sealed class DashAbilitySystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_charges.TryUseCharge(uid))
|
||||
if (!_sharedCharges.TryUseCharge(uid))
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user);
|
||||
return;
|
||||
|
||||
@@ -28,6 +28,7 @@ public sealed class EnergyKatanaSystem : EntitySystem
|
||||
|
||||
private void OnCheckDash(Entity<EnergyKatanaComponent> ent, ref CheckDashEvent args)
|
||||
{
|
||||
// Just use a whitelist fam
|
||||
if (!_ninja.IsNinja(args.User))
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@ public sealed partial class RCDAmmoComponent : Component
|
||||
/// 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;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Content.Shared.RCD.Systems;
|
||||
|
||||
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!;
|
||||
|
||||
@@ -41,9 +41,10 @@ public sealed class RCDAmmoSystem : EntitySystem
|
||||
!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);
|
||||
@@ -51,7 +52,7 @@ public sealed class RCDAmmoSystem : EntitySystem
|
||||
}
|
||||
|
||||
_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);
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed class RCDSystem : EntitySystem
|
||||
[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!;
|
||||
@@ -43,7 +43,6 @@ public sealed class RCDSystem : EntitySystem
|
||||
[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";
|
||||
@@ -133,7 +132,7 @@ public sealed class RCDSystem : EntitySystem
|
||||
if (!location.IsValid(EntityManager))
|
||||
return;
|
||||
|
||||
var gridUid = _transformSystem.GetGrid(location);
|
||||
var gridUid = _transform.GetGrid(location);
|
||||
|
||||
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
|
||||
{
|
||||
@@ -239,7 +238,7 @@ public sealed class RCDSystem : EntitySystem
|
||||
// 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))
|
||||
{
|
||||
@@ -272,7 +271,7 @@ public sealed class RCDSystem : EntitySystem
|
||||
|
||||
var location = GetCoordinates(args.Location);
|
||||
|
||||
var gridUid = _transformSystem.GetGrid(location);
|
||||
var gridUid = _transform.GetGrid(location);
|
||||
|
||||
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
|
||||
return;
|
||||
@@ -289,7 +288,7 @@ public sealed class RCDSystem : EntitySystem
|
||||
|
||||
// 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)
|
||||
@@ -321,10 +320,10 @@ public sealed class RCDSystem : EntitySystem
|
||||
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);
|
||||
@@ -332,7 +331,7 @@ public sealed class RCDSystem : EntitySystem
|
||||
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);
|
||||
|
||||
@@ -93,8 +93,9 @@
|
||||
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
|
||||
@@ -121,9 +122,10 @@
|
||||
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
|
||||
@@ -137,9 +139,10 @@
|
||||
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
|
||||
@@ -155,8 +158,9 @@
|
||||
components:
|
||||
- type: ConfirmableAction
|
||||
popup: dna-scrambler-action-popup
|
||||
- type: LimitedCharges
|
||||
maxCharges: 1
|
||||
- type: InstantAction
|
||||
charges: 1
|
||||
itemIconStyle: BigAction
|
||||
priority: -20
|
||||
icon:
|
||||
|
||||
@@ -159,7 +159,6 @@
|
||||
- type: Flash
|
||||
- type: LimitedCharges
|
||||
maxCharges: 3
|
||||
charges: 3
|
||||
- type: AutoRecharge
|
||||
rechargeDuration: 30
|
||||
- type: MeleeWeapon
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
- type: Flash
|
||||
- type: LimitedCharges
|
||||
maxCharges: 15
|
||||
charges: 15
|
||||
- type: MeleeWeapon
|
||||
damage:
|
||||
types:
|
||||
|
||||
@@ -320,7 +320,6 @@
|
||||
- Deconstruct
|
||||
- type: LimitedCharges
|
||||
maxCharges: 30
|
||||
charges: 30
|
||||
- type: Sprite
|
||||
sprite: Objects/Tools/rcd.rsi
|
||||
state: icon
|
||||
@@ -351,7 +350,7 @@
|
||||
suffix: Empty
|
||||
components:
|
||||
- type: LimitedCharges
|
||||
charges: 0
|
||||
lastCharges: -1
|
||||
|
||||
- type: entity
|
||||
id: RCDRecharging
|
||||
@@ -362,7 +361,6 @@
|
||||
components:
|
||||
- type: LimitedCharges
|
||||
maxCharges: 20
|
||||
charges: 20
|
||||
- type: AutoRecharge
|
||||
rechargeDuration: 10
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
- type: DashAbility
|
||||
- type: LimitedCharges
|
||||
maxCharges: 3
|
||||
charges: 3
|
||||
- type: AutoRecharge
|
||||
rechargeDuration: 20
|
||||
- type: Clothing
|
||||
|
||||
@@ -147,7 +147,6 @@
|
||||
- type: Flash
|
||||
- type: LimitedCharges
|
||||
maxCharges: 5
|
||||
charges: 5
|
||||
- type: MeleeWeapon
|
||||
wideAnimationRotation: 180
|
||||
damage:
|
||||
@@ -183,7 +182,6 @@
|
||||
components:
|
||||
- type: LimitedCharges
|
||||
maxCharges: 2
|
||||
charges: 2
|
||||
|
||||
- type: entity
|
||||
name: portable flasher
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
name: Animate
|
||||
description: Bring an inanimate object to life!
|
||||
components:
|
||||
- type: LimitedCharges
|
||||
maxCharges: 5
|
||||
- type: EntityTargetAction
|
||||
useDelay: 0
|
||||
charges: 5
|
||||
itemIconStyle: BigAction
|
||||
whitelist:
|
||||
components:
|
||||
|
||||
@@ -30,8 +30,6 @@
|
||||
description: Fires a fireball, but faster!
|
||||
components:
|
||||
- type: WorldTargetAction
|
||||
useDelay: 10
|
||||
renewCharges: true
|
||||
itemIconStyle: BigAction
|
||||
checkCanAccess: false
|
||||
raiseOnUser: true
|
||||
@@ -52,8 +50,6 @@
|
||||
description: The fastest fireball in the west!
|
||||
components:
|
||||
- type: WorldTargetAction
|
||||
useDelay: 8
|
||||
renewCharges: true
|
||||
itemIconStyle: BigAction
|
||||
checkCanAccess: false
|
||||
raiseOnUser: true
|
||||
|
||||
@@ -59,9 +59,10 @@
|
||||
- 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:
|
||||
|
||||
Reference in New Issue
Block a user