allow changelings to drop stored disguises (#43487)

* drop option

* popup and better icon
This commit is contained in:
slarticodefast 2026-04-08 15:21:53 +02:00 • committed by GitHub
parent 4f35ac5686
commit a6ec5e7d3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 115 additions and 29 deletions

View file

@ -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<RadialMenuOptionBase>();
var dropButtons = new List<RadialMenuOptionBase>();
foreach (var identity in identities)
{
if (!EntMan.TryGetComponent<MetaDataComponent>(identity, out var metadata))
continue;
// Options for selecting identities.
var option = new RadialMenuActionOption<NetEntity>(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<NetEntity>(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));
}
}

View file

@ -3,13 +3,25 @@
namespace Content.Shared.Changeling.Systems;
/// <summary>
/// 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.
/// </summary>
[Serializable, NetSerializable]
public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage
{
/// <summary>
/// The uid of the cloned identity.
/// The uid of the stored identity.
/// </summary>
public readonly NetEntity TargetIdentity = targetIdentity;
}
/// <summary>
/// Send when a player selects an identity to drop from their storage.
/// </summary>
[Serializable, NetSerializable]
public sealed class ChangelingTransformIdentityDropMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage
{
/// <summary>
/// The uid of the stored identity.
/// </summary>
public readonly NetEntity TargetIdentity = targetIdentity;
}

View file

@ -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<ChangelingTransformComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformActionEvent>(OnTransformAction);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformDoAfterEvent>(OnSuccessfulTransform);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformIdentitySelectMessage>(OnTransformSelected);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformIdentityDropMessage>(OnTransformDrop);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformDoAfterEvent>(OnSuccessfulTransform);
SubscribeLocalEvent<ChangelingTransformComponent, ComponentShutdown>(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<ChangelingTransformComponent> ent,
ref ChangelingTransformIdentitySelectMessage args)
{
if (!TryGetEntity(args.TargetIdentity, out var targetIdentity))
return;
if (!TryComp<ChangelingIdentityComponent>(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<ChangelingTransformComponent> ent,
ref ChangelingTransformIdentityDropMessage args)
{
if (!TryGetEntity(args.TargetIdentity, out var targetIdentity))
return;
if (!TryComp<ChangelingIdentityComponent>(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);
}
/// <summary>
/// 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<ChangelingTransformComponent> ent,
ref ChangelingTransformIdentitySelectMessage args)
{
_ui.CloseUi(ent.Owner, ChangelingTransformUiKey.Key, ent);
if (!TryGetEntity(args.TargetIdentity, out var targetIdentity))
return;
if (!TryComp<ChangelingIdentityComponent>(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<ChangelingTransformComponent> ent,
ref ChangelingTransformDoAfterEvent args)
{

View file

@ -184,6 +184,22 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
return clone;
}
/// <summary>
/// Drop a stored identity from the changeling's storage.
/// </summary>
public void DropStoredIdentity(Entity<ChangelingIdentityComponent?> ent, EntityUid identity)
{
if (!Resolve(ent, ref ent.Comp))
return;
if (!HasComp<ChangelingStoredIdentityComponent>(identity))
return; // Not a stored identity.
PredictedQueueDel(identity);
if (ent.Comp.ConsumedIdentities.Remove(identity))
Dirty(ent);
}
/// <summary>
/// Simple helper to add a PVS override to a nullspace identity.
/// </summary>

View file

@ -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