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;
/// </summary>
public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; }
- public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null)
+ public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null, FormattedMessage? charges = null)
{
_gameTiming = IoCManager.Resolve<IGameTiming>();
vbox.AddChild(description);
}
+ if (charges != null && !string.IsNullOrWhiteSpace(charges.ToString()))
+ {
+ var chargesLabel = new RichTextLabel
+ {
+ MaxWidth = TooltipTextMaxWidth,
+ StyleClasses = { StyleNano.StyleClassTooltipActionCharges }
+ };
+ chargesLabel.SetMessage(charges);
+ vbox.AddChild(chargesLabel);
+ }
+
vbox.AddChild(_cooldownLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
public const string StyleClassTooltipActionDescription = "tooltipActionDesc";
public const string StyleClassTooltipActionCooldown = "tooltipActionCooldown";
public const string StyleClassTooltipActionRequirements = "tooltipActionCooldown";
+ public const string StyleClassTooltipActionCharges = "tooltipActionCharges";
public const string StyleClassHotbarSlotNumber = "hotbarSlotNumber";
public const string StyleClassActionSearchBox = "actionSearchBox";
public const string StyleClassActionMenuItemRevoked = "actionMenuItemRevoked";
{
new StyleProperty("font", notoSans15)
}),
+ new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionCharges}, null, null), new[]
+ {
+ new StyleProperty("font", notoSans15)
+ }),
// small number for the entity counter in the entity menu
new StyleRule(new SelectorElement(typeof(Label), new[] {ContextMenuElement.StyleClassEntityMenuIconLabel}, null, null), new[]
// Is the action currently valid?
if (!action.Enabled
- || action.Charges is 0
+ || 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
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
+ if (_action is { Charges: not null })
+ {
+ var charges = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {_action.Charges.Value.ToString()}/{_action.MaxCharges.ToString()}"));
+ return new ActionAlertTooltip(name, decr, charges: charges);
+ }
+
return new ActionAlertTooltip(name, decr);
}
<BoxContainer Orientation="Vertical" RectClipContent="True">
<RichTextLabel MaxWidth="350" StyleClasses="StyleClassTooltipActionTitle"/>
<RichTextLabel MaxWidth="350" StyleClasses="StyleClassTooltipActionDescription"/>
+ <RichTextLabel MaxWidth="350" StyleClasses="StyleClassTooltipActionCharges"/>
</BoxContainer>
</controls:ActionTooltip>
--- /dev/null
+using Content.Server.Administration;
+using Content.Shared.Actions;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+
+namespace Content.Server.Commands;
+
+[AdminCommand(AdminFlags.Fun)]
+internal sealed class UpgradeActionCommand : IConsoleCommand
+{
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ public string Command => "upgradeaction";
+ public string Description => Loc.GetString("upgradeaction-command-description");
+ public string Help => Loc.GetString("upgradeaction-command-help");
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length < 1)
+ {
+ shell.WriteLine(Loc.GetString("upgradeaction-command-need-one-argument"));
+ return;
+ }
+
+ if (args.Length > 2)
+ {
+ shell.WriteLine(Loc.GetString("upgradeaction-command-max-two-arguments"));
+ return;
+ }
+
+ var actionUpgrade = _entMan.EntitySysManager.GetEntitySystem<ActionUpgradeSystem>();
+ var id = args[0];
+
+ if (!EntityUid.TryParse(id, out var uid))
+ {
+ shell.WriteLine(Loc.GetString("upgradeaction-command-incorrect-entityuid-format"));
+ return;
+ }
+
+ if (!_entMan.EntityExists(uid))
+ {
+ shell.WriteLine(Loc.GetString("upgradeaction-command-entity-does-not-exist"));
+ return;
+ }
+
+ if (!_entMan.TryGetComponent<ActionUpgradeComponent>(uid, out var actionUpgradeComponent))
+ {
+ shell.WriteLine(Loc.GetString("upgradeaction-command-entity-is-not-action"));
+ return;
+ }
+
+ if (args.Length == 1)
+ {
+ if (!actionUpgrade.TryUpgradeAction(uid, actionUpgradeComponent))
+ {
+ shell.WriteLine(Loc.GetString("upgradeaction-command-cannot-level-up"));
+ return;
+ }
+ }
+
+ if (args.Length == 2)
+ {
+ var levelArg = args[1];
+
+ if (!int.TryParse(levelArg, out var level))
+ {
+ shell.WriteLine(Loc.GetString("upgradeaction-command-second-argument-not-number"));
+ return;
+ }
+
+ if (level <= 0)
+ {
+ shell.WriteLine(Loc.GetString("upgradeaction-command-less-than-required-level"));
+ return;
+ }
+
+ if (!actionUpgrade.TryUpgradeAction(uid, actionUpgradeComponent, level))
+ shell.WriteLine(Loc.GetString("upgradeaction-command-cannot-level-up"));
+ }
+ }
+}
--- /dev/null
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Actions;
+
+// For actions that can use basic upgrades
+// Not all actions should be upgradable
+[RegisterComponent, NetworkedComponent, Access(typeof(ActionUpgradeSystem))]
+public sealed partial class ActionUpgradeComponent : Component
+{
+ /// <summary>
+ /// Current Level of the action.
+ /// </summary>
+ public int Level = 1;
+
+ /// <summary>
+ /// What level(s) effect this action?
+ /// You can skip levels, so you can have this entity change at level 2 but then won't change again until level 5.
+ /// </summary>
+ [DataField("effectedLevels"), ViewVariables]
+ public Dictionary<int, EntProtoId> EffectedLevels = new();
+
+ // TODO: Branching level upgrades
+}
--- /dev/null
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Shared.Actions.Events;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Actions;
+
+public sealed class ActionUpgradeSystem : EntitySystem
+{
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
+ [Dependency] private readonly EntityManager _entityManager = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<ActionUpgradeComponent, ActionUpgradeEvent>(OnActionUpgradeEvent);
+ }
+
+ private void OnActionUpgradeEvent(EntityUid uid, ActionUpgradeComponent component, ActionUpgradeEvent args)
+ {
+ if (!CanLevelUp(args.NewLevel, component.EffectedLevels, out var newActionProto)
+ || !_actions.TryGetActionData(uid, out var actionComp))
+ return;
+
+ var originalContainer = actionComp.Container;
+ var originalAttachedEntity = actionComp.AttachedEntity;
+
+ _actionContainer.RemoveAction(uid, actionComp);
+
+ EntityUid? upgradedActionId = null;
+ if (originalContainer != null
+ && TryComp<ActionsContainerComponent>(originalContainer.Value, out var actionContainerComp))
+ {
+ upgradedActionId = _actionContainer.AddAction(originalContainer.Value, newActionProto, actionContainerComp);
+
+ if (originalAttachedEntity != null)
+ _actions.GrantContainedActions(originalAttachedEntity.Value, originalContainer.Value);
+ else
+ _actions.GrantContainedActions(originalContainer.Value, originalContainer.Value);
+ }
+ else if (originalAttachedEntity != null)
+ {
+ upgradedActionId = _actionContainer.AddAction(originalAttachedEntity.Value, newActionProto);
+ }
+
+ if (!TryComp<ActionUpgradeComponent>(upgradedActionId, out var upgradeComp))
+ return;
+
+ upgradeComp.Level = args.NewLevel;
+
+ // TODO: Preserve ordering of actions
+
+ _entityManager.DeleteEntity(uid);
+ }
+
+ public bool TryUpgradeAction(EntityUid? actionId, ActionUpgradeComponent? actionUpgradeComponent = null, int newLevel = 0)
+ {
+ if (!TryGetActionUpgrade(actionId, out var actionUpgradeComp))
+ return false;
+
+ actionUpgradeComponent ??= actionUpgradeComp;
+ DebugTools.AssertNotNull(actionUpgradeComponent);
+ DebugTools.AssertNotNull(actionId);
+
+ if (newLevel < 1)
+ newLevel = actionUpgradeComponent.Level + 1;
+
+ if (!CanLevelUp(newLevel, actionUpgradeComponent.EffectedLevels, out _))
+ return false;
+
+ UpgradeAction(actionId, actionUpgradeComp);
+ return true;
+ }
+
+ // TODO: Add checks for branching upgrades
+ private bool CanLevelUp(
+ int newLevel,
+ Dictionary<int, EntProtoId> levelDict,
+ [NotNullWhen(true)]out EntProtoId? newLevelProto)
+ {
+ newLevelProto = null;
+
+ if (levelDict.Count < 1)
+ return false;
+
+ var canLevel = false;
+ var finalLevel = levelDict.Keys.ToList()[levelDict.Keys.Count - 1];
+
+ foreach (var (level, proto) in levelDict)
+ {
+ if (newLevel != level || newLevel > finalLevel)
+ continue;
+
+ canLevel = true;
+ newLevelProto = proto;
+ DebugTools.AssertNotNull(newLevelProto);
+ break;
+ }
+
+ return canLevel;
+ }
+
+ /// <summary>
+ /// Raises a level by one
+ /// </summary>
+ public void UpgradeAction(EntityUid? actionId, ActionUpgradeComponent? actionUpgradeComponent = null, int newLevel = 0)
+ {
+ if (!TryGetActionUpgrade(actionId, out var actionUpgradeComp))
+ return;
+
+ actionUpgradeComponent ??= actionUpgradeComp;
+ DebugTools.AssertNotNull(actionUpgradeComponent);
+ DebugTools.AssertNotNull(actionId);
+
+ if (newLevel < 1)
+ newLevel = actionUpgradeComponent.Level + 1;
+
+ RaiseActionUpgradeEvent(newLevel, actionId.Value);
+ }
+
+ private void RaiseActionUpgradeEvent(int level, EntityUid actionId)
+ {
+ var ev = new ActionUpgradeEvent(level, actionId);
+ RaiseLocalEvent(actionId, ev);
+ }
+
+ public bool TryGetActionUpgrade(
+ [NotNullWhen(true)] EntityUid? uid,
+ [NotNullWhen(true)] out ActionUpgradeComponent? result,
+ bool logError = true)
+ {
+ result = null;
+ if (!Exists(uid))
+ return false;
+
+ if (!TryComp<ActionUpgradeComponent>(uid, out var actionUpgradeComponent))
+ {
+ Log.Error($"Failed to get action upgrade from action entity: {ToPrettyString(uid.Value)}");
+ return false;
+ }
+
+ result = actionUpgradeComponent;
+ DebugTools.AssertOwner(uid, result);
+ return true;
+ }
+}
/// <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, set automatically from <see cref="Charges"/>
+ /// </summary>
+ 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 (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;
Cooldown = component.Cooldown;
UseDelay = component.UseDelay;
Charges = component.Charges;
+ MaxCharges = component.MaxCharges;
+ RenewCharges = component.RenewCharges;
CheckCanInteract = component.CheckCanInteract;
ClientExclusive = component.ClientExclusive;
Priority = component.Priority;
--- /dev/null
+namespace Content.Shared.Actions.Events;
+
+public sealed class ActionUpgradeEvent : EntityEventArgs
+{
+ public int NewLevel;
+ public EntityUid? ActionId;
+
+ public ActionUpgradeEvent(int newLevel, EntityUid? actionId)
+ {
+ NewLevel = newLevel;
+ ActionId = actionId;
+ }
+}
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
+ [Dependency] private readonly MetaDataSystem _metaData = default!;
public override void Initialize()
{
base.Initialize();
+ SubscribeLocalEvent<InstantActionComponent, MapInitEvent>(OnInit);
+ SubscribeLocalEvent<EntityTargetActionComponent, MapInitEvent>(OnInit);
+ SubscribeLocalEvent<WorldTargetActionComponent, MapInitEvent>(OnInit);
+
SubscribeLocalEvent<ActionsComponent, DidEquipEvent>(OnDidEquip);
SubscribeLocalEvent<ActionsComponent, DidEquipHandEvent>(OnHandEquipped);
SubscribeLocalEvent<ActionsComponent, DidUnequipEvent>(OnDidUnequip);
SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
}
+ private void OnInit(EntityUid uid, BaseActionComponent component, MapInitEvent args)
+ {
+ if (component.Charges != null)
+ component.MaxCharges = component.Charges.Value;
+ }
+
private void OnShutdown(EntityUid uid, ActionsComponent component, ComponentShutdown args)
{
foreach (var act in component.Actions)
Dirty(actionId.Value, action);
}
+ public void SetUseDelay(EntityUid? actionId, TimeSpan? delay)
+ {
+ if (!TryGetActionData(actionId, out var action) || action.UseDelay == delay)
+ return;
+
+ action.UseDelay = delay;
+ UpdateAction(actionId, action);
+ Dirty(actionId.Value, action);
+ }
+
+ public void ReduceUseDelay(EntityUid? actionId, TimeSpan? lowerDelay)
+ {
+ if (!TryGetActionData(actionId, out var action))
+ return;
+
+ if (action.UseDelay != null && lowerDelay != null)
+ action.UseDelay = action.UseDelay - lowerDelay;
+
+ if (action.UseDelay < TimeSpan.Zero)
+ action.UseDelay = null;
+
+ UpdateAction(actionId, action);
+ Dirty(actionId.Value, action);
+ }
+
private void OnRejuventate(EntityUid uid, ActionsComponent component, RejuvenateEvent args)
{
foreach (var act in component.Actions)
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)
+ {
+ if (!TryGetActionData(actionId, out var action))
+ return;
+
+ action.Charges = action.MaxCharges;
+ UpdateAction(actionId, action);
+ Dirty(actionId.Value, action);
+ }
+
private void OnActionsGetState(EntityUid uid, ActionsComponent component, ref ComponentGetState args)
{
args.State = new ActionsComponentState(GetNetEntitySet(component.Actions));
return;
var curTime = GameTiming.CurTime;
+ // TODO: Check for charge recovery timer
if (action.Cooldown.HasValue && action.Cooldown.Value.End > 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);
+
BaseActionEvent? performEvent = null;
// Validate request by checking action blockers and the like:
{
dirty = true;
action.Charges--;
- if (action.Charges == 0)
+ if (action is { Charges: 0, RenewCharges: false })
action.Enabled = false;
}
action.Cooldown = null;
- if (action.UseDelay != null)
+ if (action is { UseDelay: not null, Charges: null or < 1 })
{
dirty = true;
action.Cooldown = (curTime, curTime + action.UseDelay.Value);
--- /dev/null
+## Actions Commands loc
+
+## Upgradeaction command loc
+upgradeaction-command-need-one-argument = upgradeaction needs at least one argument, the action entity uid. The second optional argument is a specified level.
+upgradeaction-command-max-two-arguments = upgradeaction has a maximum of two arguments, the action entity uid and the (optional) level to set.
+upgradeaction-command-second-argument-not-number = upgradeaction's second argument can only be a number.
+upgradeaction-command-less-than-required-level = upgradeaction cannot accept a level of 0 or lower.
+upgradeaction-command-incorrect-entityuid-format = You must use a valid entityuid format for upgradeaction.
+upgradeaction-command-entity-does-not-exist = This entity does not exist, a valid entity is required for upgradeaction.
+upgradeaction-command-entity-is-not-action = This entity doesn't have the action upgrade component, so this action cannot be leveled.
+upgradeaction-command-cannot-level-up = The action cannot be leveled up.
+upgradeaction-command-description = Upgrades an action by one level, or to the specified level, if applicable.
noSpawn: true
components:
- type: WorldTargetAction
- useDelay: 30
+ useDelay: 15
+ itemIconStyle: BigAction
+ checkCanAccess: false
+ range: 60
+ sound: !type:SoundPathSpecifier
+ path: /Audio/Magic/fireball.ogg
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: fireball
+ event: !type:ProjectileSpellEvent
+ prototype: ProjectileFireball
+ posData: !type:TargetCasterPos
+ speech: action-speech-spell-fireball
+ - type: ActionUpgrade
+ effectedLevels:
+ 2: ActionFireballII
+
+- type: entity
+ id: ActionFireballII
+ parent: ActionFireball
+ name: Fireball II
+ description: Fire three explosive fireball towards the clicked location.
+ noSpawn: true
+ components:
+ - type: WorldTargetAction
+ useDelay: 5
+ charges: 3
+ renewCharges: true
itemIconStyle: BigAction
checkCanAccess: false
range: 60