From 41c51e29052c16f092e6941f7e9ccfd14dfb3dae Mon Sep 17 00:00:00 2001 From: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Date: Thu, 27 Feb 2025 18:57:28 +0100 Subject: [PATCH] Implanter draw rework (#32136) * Initial commit * Clean-up * Fix ftl, new damage * ftl fix for real * Updates based on feedback * Child implant fix * Make the UI only open when implanter is in draw mode * Review fixes * shunting --- Content.Client/Implants/ImplanterSystem.cs | 16 ++ .../UI/DeimplantBoundUserInterface.cs | 35 +++ .../Implants/UI/DeimplantChoiceWindow.xaml | 12 + .../Implants/UI/DeimplantChoiceWindow.xaml.cs | 53 +++++ .../Implants/UI/ImplanterStatusControl.cs | 28 ++- Content.Server/Implants/ImplanterSystem.cs | 2 - .../Implants/Components/ImplanterComponent.cs | 29 ++- .../Components/SubdermalImplantComponent.cs | 7 + .../Implants/SharedImplanterSystem.cs | 212 ++++++++++++++++-- Resources/Locale/en-US/implant/implant.ftl | 11 +- .../Entities/Objects/Misc/implanters.yml | 27 ++- 11 files changed, 396 insertions(+), 36 deletions(-) create mode 100644 Content.Client/Implants/UI/DeimplantBoundUserInterface.cs create mode 100644 Content.Client/Implants/UI/DeimplantChoiceWindow.xaml create mode 100644 Content.Client/Implants/UI/DeimplantChoiceWindow.xaml.cs diff --git a/Content.Client/Implants/ImplanterSystem.cs b/Content.Client/Implants/ImplanterSystem.cs index 13a90f3e39..cca09f5dad 100644 --- a/Content.Client/Implants/ImplanterSystem.cs +++ b/Content.Client/Implants/ImplanterSystem.cs @@ -2,11 +2,15 @@ using Content.Client.Items; using Content.Shared.Implants; using Content.Shared.Implants.Components; +using Robust.Shared.Prototypes; namespace Content.Client.Implants; public sealed class ImplanterSystem : SharedImplanterSystem { + [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + public override void Initialize() { base.Initialize(); @@ -17,6 +21,18 @@ public sealed class ImplanterSystem : SharedImplanterSystem private void OnHandleImplanterState(EntityUid uid, ImplanterComponent component, ref AfterAutoHandleStateEvent args) { + if (_uiSystem.TryGetOpenUi(uid, DeimplantUiKey.Key, out var bui)) + { + Dictionary implants = new(); + foreach (var implant in component.DeimplantWhitelist) + { + if (_proto.TryIndex(implant, out var proto)) + implants.Add(proto.ID, proto.Name); + } + + bui.UpdateState(implants, component.DeimplantChosen); + } + component.UiUpdateNeeded = true; } } diff --git a/Content.Client/Implants/UI/DeimplantBoundUserInterface.cs b/Content.Client/Implants/UI/DeimplantBoundUserInterface.cs new file mode 100644 index 0000000000..0857cdf86f --- /dev/null +++ b/Content.Client/Implants/UI/DeimplantBoundUserInterface.cs @@ -0,0 +1,35 @@ +using Content.Shared.Implants; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; + +namespace Content.Client.Implants.UI; + +public sealed class DeimplantBoundUserInterface : BoundUserInterface +{ + [Dependency] private readonly IPrototypeManager _protomanager = default!; + + [ViewVariables] + private DeimplantChoiceWindow? _window; + + public DeimplantBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow(); + + _window.OnImplantChange += implant => SendMessage(new DeimplantChangeVerbMessage(implant)); + } + + public void UpdateState(Dictionary implantList, string? implant) + { + if (_window != null) + { + _window.UpdateImplantList(implantList); + _window.UpdateState(implant); + } + } +} diff --git a/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml b/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml new file mode 100644 index 0000000000..f0de9f32e0 --- /dev/null +++ b/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml @@ -0,0 +1,12 @@ + + + + diff --git a/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml.cs b/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml.cs new file mode 100644 index 0000000000..a7ce50d855 --- /dev/null +++ b/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml.cs @@ -0,0 +1,53 @@ +using Content.Client.UserInterface.Controls; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.XAML; +using System.Linq; + +namespace Content.Client.Implants.UI; + +[GenerateTypedNameReferences] +public sealed partial class DeimplantChoiceWindow : FancyWindow +{ + public Action? OnImplantChange; + + private Dictionary _implants = new(); + + private string? _chosenImplant; + + public DeimplantChoiceWindow() + { + RobustXamlLoader.Load(this); + + ImplantSelector.OnItemSelected += args => + { + OnImplantChange?.Invoke(_implants.ElementAt(args.Id).Key); + ImplantSelector.SelectId(args.Id); + }; + } + + public void UpdateImplantList(Dictionary implants) + { + _implants = implants; + int i = 0; + ImplantSelector.Clear(); + foreach (var implantDict in _implants) + { + ImplantSelector.AddItem(implantDict.Value, i); + i++; + } + } + + public void UpdateState(string? implant) + { + _chosenImplant = implant; + + for (int id = 0; id < ImplantSelector.ItemCount; id++) + { + if (_implants.ElementAt(id).Key.Equals(_chosenImplant)) + { + ImplantSelector.SelectId(id); + break; + } + } + } +} diff --git a/Content.Client/Implants/UI/ImplanterStatusControl.cs b/Content.Client/Implants/UI/ImplanterStatusControl.cs index e2ffabd17d..569dd785d7 100644 --- a/Content.Client/Implants/UI/ImplanterStatusControl.cs +++ b/Content.Client/Implants/UI/ImplanterStatusControl.cs @@ -4,17 +4,20 @@ using Content.Client.UserInterface.Controls; using Content.Shared.Implants.Components; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Client.Implants.UI; public sealed class ImplanterStatusControl : Control { + [Dependency] private readonly IPrototypeManager _prototype = default!; private readonly ImplanterComponent _parent; private readonly RichTextLabel _label; public ImplanterStatusControl(ImplanterComponent parent) { + IoCManager.InjectDependencies(this); _parent = parent; _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; _label.MaxWidth = 350; @@ -43,12 +46,25 @@ public sealed class ImplanterStatusControl : Control _ => Loc.GetString("injector-invalid-injector-toggle-mode") }; - var implantName = _parent.ImplanterSlot.HasItem - ? _parent.ImplantData.Item1 - : Loc.GetString("implanter-empty-text"); + if (_parent.CurrentMode == ImplanterToggleMode.Draw) + { + string implantName = _parent.DeimplantChosen != null + ? (_prototype.TryIndex(_parent.DeimplantChosen.Value, out EntityPrototype? implantProto) ? implantProto.Name : Loc.GetString("implanter-empty-text")) + : Loc.GetString("implanter-empty-text"); - _label.SetMarkup(Loc.GetString("implanter-label", - ("implantName", implantName), - ("modeString", modeStringLocalized))); + _label.SetMarkup(Loc.GetString("implanter-label-draw", + ("implantName", implantName), + ("modeString", modeStringLocalized))); + } + else + { + var implantName = _parent.ImplanterSlot.HasItem + ? _parent.ImplantData.Item1 + : Loc.GetString("implanter-empty-text"); + + _label.SetMarkup(Loc.GetString("implanter-label-inject", + ("implantName", implantName), + ("modeString", modeStringLocalized))); + } } } diff --git a/Content.Server/Implants/ImplanterSystem.cs b/Content.Server/Implants/ImplanterSystem.cs index 5efd5dc6fb..5023b1b3e4 100644 --- a/Content.Server/Implants/ImplanterSystem.cs +++ b/Content.Server/Implants/ImplanterSystem.cs @@ -68,8 +68,6 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem args.Handled = true; } - - /// /// Attempt to implant someone else. /// diff --git a/Content.Shared/Implants/Components/ImplanterComponent.cs b/Content.Shared/Implants/Components/ImplanterComponent.cs index 80330aa7e6..f956bd6502 100644 --- a/Content.Shared/Implants/Components/ImplanterComponent.cs +++ b/Content.Shared/Implants/Components/ImplanterComponent.cs @@ -1,4 +1,5 @@ -using Content.Shared.Containers.ItemSlots; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Damage; using Content.Shared.Whitelist; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -49,7 +50,7 @@ public sealed partial class ImplanterComponent : Component /// [ViewVariables(VVAccess.ReadWrite)] [DataField] - public float DrawTime = 60f; + public float DrawTime = 25f; /// /// Good for single-use injectors @@ -82,6 +83,30 @@ public sealed partial class ImplanterComponent : Component [DataField(required: true)] public ItemSlot ImplanterSlot = new(); + /// + /// If true, the implanter may be used to remove all kinds of (deimplantable) implants without selecting any. + /// + [DataField] + public bool AllowDeimplantAll = false; + + /// + /// The subdermal implants that may be removed via this implanter + /// + [DataField] + public List DeimplantWhitelist = new(); + + /// + /// The subdermal implants that may be removed via this implanter + /// + [DataField] + public DamageSpecifier DeimplantFailureDamage = new(); + + /// + /// Chosen implant to remove, if necessary. + /// + [AutoNetworkedField] + public EntProtoId? DeimplantChosen = null; + public bool UiUpdateNeeded; } diff --git a/Content.Shared/Implants/Components/SubdermalImplantComponent.cs b/Content.Shared/Implants/Components/SubdermalImplantComponent.cs index 09ef05e48a..bd0ff09678 100644 --- a/Content.Shared/Implants/Components/SubdermalImplantComponent.cs +++ b/Content.Shared/Implants/Components/SubdermalImplantComponent.cs @@ -49,6 +49,13 @@ public sealed partial class SubdermalImplantComponent : Component /// [DataField] public EntityWhitelist? Blacklist; + + /// + /// If set, this ProtoId is used when attempting to draw the implant instead. + /// Useful if the implant is a child to another implant and you don't want to differentiate between them when drawing. + /// + [DataField] + public EntProtoId? DrawableProtoIdOverride; } /// diff --git a/Content.Shared/Implants/SharedImplanterSystem.cs b/Content.Shared/Implants/SharedImplanterSystem.cs index 6f394fb932..1b0ca7fecc 100644 --- a/Content.Shared/Implants/SharedImplanterSystem.cs +++ b/Content.Shared/Implants/SharedImplanterSystem.cs @@ -1,14 +1,18 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Containers.ItemSlots; +using Content.Shared.Damage; using Content.Shared.DoAfter; using Content.Shared.Examine; using Content.Shared.Forensics; using Content.Shared.IdentityManagement; using Content.Shared.Implants.Components; +using Content.Shared.Interaction.Events; using Content.Shared.Popups; +using Content.Shared.Verbs; using Content.Shared.Whitelist; using Robust.Shared.Containers; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; @@ -21,6 +25,9 @@ public abstract class SharedImplanterSystem : EntitySystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; public override void Initialize() { @@ -29,6 +36,10 @@ public abstract class SharedImplanterSystem : EntitySystem SubscribeLocalEvent(OnImplanterInit); SubscribeLocalEvent(OnEntInserted); SubscribeLocalEvent(OnExamine); + + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent>(OnVerb); + SubscribeLocalEvent(OnSelected); } private void OnImplanterInit(EntityUid uid, ImplanterComponent component, ComponentInit args) @@ -37,6 +48,10 @@ public abstract class SharedImplanterSystem : EntitySystem component.ImplanterSlot.StartingItem = component.Implant; _itemSlots.AddItemSlot(uid, ImplanterComponent.ImplanterSlotId, component.ImplanterSlot); + + component.DeimplantChosen ??= component.DeimplantWhitelist.FirstOrNull(); + + Dirty(uid, component); } private void OnEntInserted(EntityUid uid, ImplanterComponent component, EntInsertedIntoContainerMessage args) @@ -56,10 +71,49 @@ public abstract class SharedImplanterSystem : EntitySystem { if (!TryComp(target, out var implanted)) return false; - var implantPrototype = Prototype(implant); return implanted.ImplantContainer.ContainedEntities.Any(entity => Prototype(entity) == implantPrototype); } + + private void OnVerb(EntityUid uid, ImplanterComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + if (component.CurrentMode == ImplanterToggleMode.Draw) + { + args.Verbs.Add(new InteractionVerb() + { + Text = Loc.GetString("implanter-set-draw-verb"), + Act = () => TryOpenUi(uid, args.User, component) + }); + } + } + + private void OnUseInHand(EntityUid uid, ImplanterComponent? component, UseInHandEvent args) + { + if (!Resolve(uid, ref component)) + return; + + if (component.CurrentMode == ImplanterToggleMode.Draw) + TryOpenUi(uid, args.User, component); + } + + private void OnSelected(EntityUid uid, ImplanterComponent component, DeimplantChangeVerbMessage args) + { + component.DeimplantChosen = args.Implant; + SetSelectedDeimplant(uid, args.Implant, component: component); + } + + private void TryOpenUi(EntityUid uid, EntityUid user, ImplanterComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + _uiSystem.TryToggleUi(uid, DeimplantUiKey.Key, user); + component.DeimplantChosen ??= component.DeimplantWhitelist.FirstOrNull(); + Dirty(uid, component); + } + //Instantly implant something and add all necessary components and containers. //Set to draw mode if not implant only public void Implant(EntityUid user, EntityUid target, EntityUid implanter, ImplanterComponent component) @@ -142,40 +196,103 @@ public abstract class SharedImplanterSystem : EntitySystem { var implantCompQuery = GetEntityQuery(); - foreach (var implant in implantContainer.ContainedEntities) + if (component.AllowDeimplantAll) { - if (!implantCompQuery.TryGetComponent(implant, out var implantComp)) - continue; - - //Don't remove a permanent implant and look for the next that can be drawn - if (!_container.CanRemove(implant, implantContainer)) + foreach (var implant in implantContainer.ContainedEntities) { - var implantName = Identity.Entity(implant, EntityManager); - var targetName = Identity.Entity(target, EntityManager); - var failedPermanentMessage = Loc.GetString("implanter-draw-failed-permanent", - ("implant", implantName), ("target", targetName)); - _popup.PopupEntity(failedPermanentMessage, target, user); + if (!implantCompQuery.TryGetComponent(implant, out var implantComp)) + continue; + + //Don't remove a permanent implant and look for the next that can be drawn + if (!_container.CanRemove(implant, implantContainer)) + { + DrawPermanentFailurePopup(implant, target, user); + permanentFound = implantComp.Permanent; + continue; + } + + DrawImplantIntoImplanter(implanter, target, implant, implantContainer, implanterContainer, implantComp); permanentFound = implantComp.Permanent; - continue; + + //Break so only one implant is drawn + break; } - _container.Remove(implant, implantContainer); - implantComp.ImplantedEntity = null; - _container.Insert(implant, implanterContainer); - permanentFound = implantComp.Permanent; + if (component.CurrentMode == ImplanterToggleMode.Draw && !component.ImplantOnly && !permanentFound) + ImplantMode(implanter, component); + } + else + { + EntityUid? implant = null; + var implants = implantContainer.ContainedEntities; + foreach (var implantEntity in implants) + { + if (TryComp(implantEntity, out var subdermalComp)) + { + if (component.DeimplantChosen == subdermalComp.DrawableProtoIdOverride || + (Prototype(implantEntity) != null && component.DeimplantChosen == Prototype(implantEntity)!)) + implant = implantEntity; + } + } - var ev = new TransferDnaEvent { Donor = target, Recipient = implanter }; - RaiseLocalEvent(target, ref ev); + if (implant != null && implantCompQuery.TryGetComponent(implant, out var implantComp)) + { + //Don't remove a permanent implant + if (!_container.CanRemove(implant.Value, implantContainer)) + { + DrawPermanentFailurePopup(implant.Value, target, user); + permanentFound = implantComp.Permanent; - //Break so only one implant is drawn - break; + } + else + { + DrawImplantIntoImplanter(implanter, target, implant.Value, implantContainer, implanterContainer, implantComp); + permanentFound = implantComp.Permanent; + } + + if (component.CurrentMode == ImplanterToggleMode.Draw && !component.ImplantOnly && !permanentFound) + ImplantMode(implanter, component); + } + else + { + DrawCatastrophicFailure(implanter, component, user); + } } - if (component.CurrentMode == ImplanterToggleMode.Draw && !component.ImplantOnly && !permanentFound) - ImplantMode(implanter, component); - Dirty(implanter, component); + } + else + { + DrawCatastrophicFailure(implanter, component, user); + } + } + + private void DrawPermanentFailurePopup(EntityUid implant, EntityUid target, EntityUid user) + { + var implantName = Identity.Entity(implant, EntityManager); + var targetName = Identity.Entity(target, EntityManager); + var failedPermanentMessage = Loc.GetString("implanter-draw-failed-permanent", + ("implant", implantName), ("target", targetName)); + _popup.PopupEntity(failedPermanentMessage, target, user); + } + + private void DrawImplantIntoImplanter(EntityUid implanter, EntityUid target, EntityUid implant, BaseContainer implantContainer, ContainerSlot implanterContainer, SubdermalImplantComponent implantComp) + { + _container.Remove(implant, implantContainer); + implantComp.ImplantedEntity = null; + _container.Insert(implant, implanterContainer); + + var ev = new TransferDnaEvent { Donor = target, Recipient = implanter }; + RaiseLocalEvent(target, ref ev); + } + + private void DrawCatastrophicFailure(EntityUid implanter, ImplanterComponent component, EntityUid user) + { + _damageableSystem.TryChangeDamage(user, component.DeimplantFailureDamage, ignoreResistances: true, origin: implanter); + var userName = Identity.Entity(user, EntityManager); + var failedCatastrophicallyMessage = Loc.GetString("implanter-draw-failed-catastrophically", ("user", userName)); + _popup.PopupEntity(failedCatastrophicallyMessage, user, PopupType.MediumCaution); } private void ImplantMode(EntityUid uid, ImplanterComponent component) @@ -216,6 +333,17 @@ public abstract class SharedImplanterSystem : EntitySystem else _appearance.SetData(uid, ImplanterVisuals.Full, implantFound, appearance); } + + public void SetSelectedDeimplant(EntityUid uid, string? implant, ImplanterComponent? component = null) + { + if (!Resolve(uid, ref component, false)) + return; + + if (implant != null && _proto.TryIndex(implant, out EntityPrototype? proto)) + component.DeimplantChosen = proto; + + Dirty(uid, component); + } } [Serializable, NetSerializable] @@ -243,3 +371,39 @@ public sealed class AddImplantAttemptEvent : CancellableEntityEventArgs Implanter = implanter; } } + + +[Serializable, NetSerializable] +public sealed class DeimplantBuiState : BoundUserInterfaceState +{ + public readonly string? Implant; + + public Dictionary ImplantList; + + public DeimplantBuiState(string? implant, Dictionary implantList) + { + Implant = implant; + ImplantList = implantList; + } +} + + +/// +/// Change the chosen implanter in the UI. +/// +[Serializable, NetSerializable] +public sealed class DeimplantChangeVerbMessage : BoundUserInterfaceMessage +{ + public readonly string? Implant; + + public DeimplantChangeVerbMessage(string? implant) + { + Implant = implant; + } +} + +[Serializable, NetSerializable] +public enum DeimplantUiKey : byte +{ + Key +} diff --git a/Resources/Locale/en-US/implant/implant.ftl b/Resources/Locale/en-US/implant/implant.ftl index 073757f53c..8cddef4c81 100644 --- a/Resources/Locale/en-US/implant/implant.ftl +++ b/Resources/Locale/en-US/implant/implant.ftl @@ -4,15 +4,24 @@ implanter-component-implanting-target = {$user} is trying to implant you with so implanter-component-implant-failed = The {$implant} cannot be given to {$target}! implanter-draw-failed-permanent = The {$implant} in {$target} is fused with { OBJECT($target) } and cannot be removed! implanter-draw-failed = You tried to remove an implant but found nothing. +implanter-draw-failed-catastrophically = The implanter finds nothing and catastrophically fails, shunting genetic material into {$user}'s hand! implanter-component-implant-already = {$target} already has the {$implant}! ## UI +implanter-set-draw-verb = Set Implant Draw +implanter-set-draw-window = Set Implant Draw +implanter-set-draw-info = Select the implant type this implanter should remove: +implanter-set-draw-type = Implant type: + implanter-draw-text = Draw implanter-inject-text = Inject implanter-empty-text = Empty -implanter-label = [color=green]{$implantName}[/color] +implanter-label-inject = [color=green]{$implantName}[/color] + Mode: [color=white]{$modeString}[/color] + +implanter-label-draw = [color=red]{$implantName}[/color] Mode: [color=white]{$modeString}[/color] implanter-contained-implant-text = [color=green]{$desc}[/color] diff --git a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml index d40f48dc7d..58aefc93fd 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml @@ -2,7 +2,7 @@ - type: entity name: implanter - description: A syringe exclusively designed for the injection and extraction of subdermal implants. + description: A syringe exclusively designed for the injection and extraction of subdermal implants. Use care when extracting implants, as incorrect draw settings may injure the user. id: BaseImplanter parent: BaseItem abstract: true @@ -28,6 +28,27 @@ whitelist: tags: - SubdermalImplant + allowDeimplantAll: false + deimplantWhitelist: + - SadTromboneImplant + - LightImplant + - BikeHornImplant + - TrackingImplant + - StorageImplant + - FreedomImplant + - UplinkImplant + - EmpImplant + - ScramImplant + - DnaScramblerImplant + - MicroBombImplant + - MacroBombImplant + - DeathAcidifierImplant + - DeathRattleImplant + - MindShieldImplant + deimplantFailureDamage: + types: + Cellular: 50 + Heat: 10 - type: Sprite sprite: Objects/Specific/Medical/implanter.rsi state: implanter0 @@ -53,6 +74,10 @@ implantOnly: True: {state: broken} False: {state: implanter0} + - type: UserInterface + interfaces: + enum.DeimplantUiKey.Key: + type: DeimplantBoundUserInterface - type: entity id: Implanter