Action Upgrade System (#22277)

* Adds uses before delay so actions can be used multiple times before cooldown

* adds methods to get remaining charges, to set uses before delay, and to set use delay

* adds method to change action name

* moves set usedelay

* action upgrade ECS

* adds method to reset remaining uses

* adds upgrade events

* refactors action upgrade event and adds logic to parse it

* fix serialization issue

* adds level up draft method

* adds action commands and a command to upgrade an action

* more warning lines to help

* Gets action to upgrade properly

* Removes unneeded fields from the action upgrade component and now properly raises the level of the new action

* Cleans up dead code and comments

* Fixes punctuation in actions-commands and adds a TryUpgradeAction method.

* removes TODO comment

* robust fix

* removes RT

* readds RT

* update RT to 190

* removes change name method

* removes remaining uses & related fields and adds that functionality to charges

* Adds Charges to action tooltips that require it
This commit is contained in:
keronshb
2023-12-15 04:41:44 -05:00
committed by GitHub
parent 39cda517a5
commit 2d692f47da
14 changed files with 438 additions and 5 deletions

View File

@@ -87,6 +87,8 @@ namespace Content.Client.Actions
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;

View File

@@ -21,7 +21,7 @@ namespace Content.Client.Actions.UI
/// </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>();
@@ -52,6 +52,17 @@ namespace Content.Client.Actions.UI
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,

View File

@@ -56,6 +56,7 @@ namespace Content.Client.Stylesheets
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";
@@ -940,6 +941,10 @@ namespace Content.Client.Stylesheets
{
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[]

View File

@@ -171,7 +171,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
// 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

View File

@@ -191,6 +191,12 @@ public sealed class ActionButton : Control, IEntityControl
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);
}

View File

@@ -5,5 +5,6 @@
<BoxContainer Orientation="Vertical" RectClipContent="True">
<RichTextLabel MaxWidth="350" StyleClasses="StyleClassTooltipActionTitle"/>
<RichTextLabel MaxWidth="350" StyleClasses="StyleClassTooltipActionDescription"/>
<RichTextLabel MaxWidth="350" StyleClasses="StyleClassTooltipActionCharges"/>
</BoxContainer>
</controls:ActionTooltip>

View File

@@ -0,0 +1,81 @@
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"));
}
}
}

View File

@@ -0,0 +1,24 @@
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
}

View File

@@ -0,0 +1,149 @@
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;
}
}

View File

@@ -66,9 +66,21 @@ public abstract partial class BaseActionComponent : Component
/// <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.
@@ -159,6 +171,8 @@ public abstract class BaseActionComponentState : ComponentState
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;
@@ -186,6 +200,8 @@ public abstract class BaseActionComponentState : ComponentState
Cooldown = component.Cooldown;
UseDelay = component.UseDelay;
Charges = component.Charges;
MaxCharges = component.MaxCharges;
RenewCharges = component.RenewCharges;
CheckCanInteract = component.CheckCanInteract;
ClientExclusive = component.ClientExclusive;
Priority = component.Priority;

View File

@@ -0,0 +1,13 @@
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;
}
}

View File

@@ -30,11 +30,16 @@ public abstract class SharedActionsSystem : EntitySystem
[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);
@@ -56,6 +61,12 @@ public abstract class SharedActionsSystem : EntitySystem
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)
@@ -165,6 +176,31 @@ public abstract class SharedActionsSystem : EntitySystem
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)
@@ -218,6 +254,51 @@ public abstract class SharedActionsSystem : EntitySystem
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));
@@ -261,9 +342,14 @@ public abstract class SharedActionsSystem : EntitySystem
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:
@@ -438,12 +524,12 @@ public abstract class SharedActionsSystem : EntitySystem
{
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);

View File

@@ -0,0 +1,12 @@
## 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.

View File

@@ -5,7 +5,34 @@
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