diff --git a/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs b/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs index dc40147220..25cfb1ae82 100644 --- a/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs +++ b/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs @@ -4,6 +4,7 @@ using Content.Shared.Changeling.Components; using Content.Shared.Changeling.Systems; using JetBrains.Annotations; using Robust.Client.UserInterface; +using Robust.Shared.Utility; namespace Content.Client.Changeling.UI; @@ -12,7 +13,9 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne { private SimpleRadialMenu? _menu; private static readonly Color SelectedOptionBackground = Palettes.Green.Element.WithAlpha(128); + private static readonly Color DisabledOptionBackground = Palettes.Slate.Element.WithAlpha(128); private static readonly Color SelectedOptionHoverBackground = Palettes.Green.HoveredElement.WithAlpha(128); + private static readonly Color DisabledOptionHoverBackground = Palettes.Slate.HoveredElement.WithAlpha(128); protected override void Open() { @@ -42,21 +45,41 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne ) { var buttons = new List(); + var dropButtons = new List(); + foreach (var identity in identities) { - if (!EntMan.TryGetComponent(identity, out var metadata)) - continue; - + // Options for selecting identities. var option = new RadialMenuActionOption(SendIdentitySelect, EntMan.GetNetEntity(identity)) { IconSpecifier = RadialMenuIconSpecifier.With(identity), - ToolTip = metadata.EntityName, - BackgroundColor = (currentIdentity == identity) ? SelectedOptionBackground : null, + ToolTip = Loc.GetString("changeling-transform-bui-select-entity", ("entity", identity)), + BackgroundColor = (currentIdentity == identity) ? SelectedOptionBackground : null, // mark as selected HoverBackgroundColor = (currentIdentity == identity) ? SelectedOptionHoverBackground : null }; buttons.Add(option); + + // Options for dropping identities. + var dropOption = new RadialMenuActionOption(SendIdentityDrop, EntMan.GetNetEntity(identity)) + { + IconSpecifier = RadialMenuIconSpecifier.With(identity), + ToolTip = (currentIdentity == identity) + ? Loc.GetString("changeling-transform-bui-drop-identity-cannot-drop") + : Loc.GetString("changeling-transform-bui-drop-identity-entity", ("entity", identity)), + BackgroundColor = (currentIdentity == identity) ? DisabledOptionBackground : null, // cannot drop your current identity + HoverBackgroundColor = (currentIdentity == identity) ? DisabledOptionHoverBackground : null + }; + dropButtons.Add(dropOption); } + // Menu category for dropping identities. + var dropMenuButton = new RadialMenuNestedLayerOption(dropButtons) + { + IconSpecifier = RadialMenuIconSpecifier.With(new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/delete.svg.192dpi.png"))), + ToolTip = Loc.GetString("changeling-transform-bui-drop-identity-menu") + }; + buttons.Add(dropMenuButton); + return buttons; } @@ -64,4 +87,9 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne { SendPredictedMessage(new ChangelingTransformIdentitySelectMessage(identityId)); } + + private void SendIdentityDrop(NetEntity identityId) + { + SendPredictedMessage(new ChangelingTransformIdentityDropMessage(identityId)); + } } diff --git a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs index e555147352..50f5ceb547 100644 --- a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs +++ b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs @@ -3,13 +3,25 @@ namespace Content.Shared.Changeling.Systems; /// -/// Send when a player selects an intentity to transform into in the radial menu. +/// Send when a player selects an identity to transform into in the radial menu. /// [Serializable, NetSerializable] public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage { /// - /// The uid of the cloned identity. + /// The uid of the stored identity. + /// + public readonly NetEntity TargetIdentity = targetIdentity; +} + +/// +/// Send when a player selects an identity to drop from their storage. +/// +[Serializable, NetSerializable] +public sealed class ChangelingTransformIdentityDropMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage +{ + /// + /// The uid of the stored identity. /// public readonly NetEntity TargetIdentity = targetIdentity; } diff --git a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs index 8bea4dedc6..8ec2d36cd8 100644 --- a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs @@ -30,6 +30,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly IdentitySystem _identity = default!; + [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!; private const string ChangelingBuiXmlGeneratedName = "ChangelingTransformBoundUserInterface"; public override void Initialize() @@ -38,8 +39,9 @@ public sealed partial class ChangelingTransformSystem : EntitySystem SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnTransformAction); - SubscribeLocalEvent(OnSuccessfulTransform); SubscribeLocalEvent(OnTransformSelected); + SubscribeLocalEvent(OnTransformDrop); + SubscribeLocalEvent(OnSuccessfulTransform); SubscribeLocalEvent(OnShutdown); // Components that need special handling outside of cloning. @@ -79,6 +81,43 @@ public sealed partial class ChangelingTransformSystem : EntitySystem // but pressing the number does. } + private void OnTransformSelected(Entity ent, + ref ChangelingTransformIdentitySelectMessage args) + { + if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) + return; + + if (!TryComp(ent, out var identity)) + return; + + if (identity.CurrentIdentity == targetIdentity) + return; // don't transform into ourselves + + if (!identity.ConsumedIdentities.ContainsKey(targetIdentity.Value)) + return; // this identity does not belong to this player + + TransformInto(ent.AsNullable(), targetIdentity.Value); + } + + private void OnTransformDrop(Entity ent, + ref ChangelingTransformIdentityDropMessage args) + { + if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) + return; + + if (!TryComp(ent, out var identity)) + return; + + if (identity.CurrentIdentity == targetIdentity) + return; // don't drop our current identity + + if (!identity.ConsumedIdentities.ContainsKey(targetIdentity.Value)) + return; // this identity does not belong to this player + + _popup.PopupClient(Loc.GetString("changeling-transform-bui-drop-identity-entity-popup", ("entity", targetIdentity.Value)), ent.Owner, PopupType.Large); + _changelingIdentity.DropStoredIdentity(ent.Owner, targetIdentity.Value); + } + /// /// Transform the changeling into another identity. /// This can be any cloneable humanoid and doesn't have to be stored in the ChangelingIdentityComponent, @@ -125,26 +164,6 @@ public sealed partial class ChangelingTransformSystem : EntitySystem }); } - private void OnTransformSelected(Entity ent, - ref ChangelingTransformIdentitySelectMessage args) - { - _ui.CloseUi(ent.Owner, ChangelingTransformUiKey.Key, ent); - - if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) - return; - - if (!TryComp(ent, out var identity)) - return; - - if (identity.CurrentIdentity == targetIdentity) - return; // don't transform into ourselves - - if (!identity.ConsumedIdentities.ContainsKey(targetIdentity.Value)) - return; // this identity does not belong to this player - - TransformInto(ent.AsNullable(), targetIdentity.Value); - } - private void OnSuccessfulTransform(Entity ent, ref ChangelingTransformDoAfterEvent args) { diff --git a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs index 12d8f162a9..b9e2fbef3e 100644 --- a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs +++ b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs @@ -184,6 +184,22 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem return clone; } + /// + /// Drop a stored identity from the changeling's storage. + /// + public void DropStoredIdentity(Entity ent, EntityUid identity) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + if (!HasComp(identity)) + return; // Not a stored identity. + + PredictedQueueDel(identity); + if (ent.Comp.ConsumedIdentities.Remove(identity)) + Dirty(ent); + } + /// /// Simple helper to add a PVS override to a nullspace identity. /// diff --git a/Resources/Locale/en-US/changeling/changeling.ftl b/Resources/Locale/en-US/changeling/changeling.ftl index 873744fa06..d5c88d06f2 100644 --- a/Resources/Locale/en-US/changeling/changeling.ftl +++ b/Resources/Locale/en-US/changeling/changeling.ftl @@ -1,6 +1,8 @@ -roles-antag-changeling-name = Changeling +# antag selection +roles-antag-changeling-name = Changeling roles-antag-changeling-objective = A intelligent predator that assumes the identities of its victims. +# devour changeling-devour-attempt-failed-cannot-devour = We cannot devour this! changeling-devour-attempt-failed-already-devoured = We already consumed this body! changeling-devour-attempt-failed-not-dead = This body yet lives! We cannot consume it alive! @@ -14,7 +16,16 @@ changeling-devour-begin-consume-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny changeling-devour-consume-complete-self = Our uncanny mouth retreats, biomass consumed. changeling-devour-consume-complete-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth retreats. +# transformation changeling-transform-attempt-self = Our bones snap, muscles tear, one flesh becomes another. changeling-transform-attempt-others = { CAPITALIZE(POSS-ADJ($user)) } bones snap, muscles tear, body shifts into another. +# transformation BUI +changeling-transform-bui-select-entity = {$entity} +changeling-transform-bui-drop-identity-menu = Drop a devoured identity from your memory. +changeling-transform-bui-drop-identity-entity = Drop {$entity} +changeling-transform-bui-drop-identity-entity-popup = You dropped {$entity} from your memory. +changeling-transform-bui-drop-identity-cannot-drop = You cannot drop your current identity. + +# other changeling-paused-map-name = Changeling identity storage map