diff --git a/Content.Server/Objectives/Systems/ChangelingObjectiveSystem.cs b/Content.Server/Objectives/Systems/ChangelingObjectiveSystem.cs index 451e56426c..3b5b3c9006 100644 --- a/Content.Server/Objectives/Systems/ChangelingObjectiveSystem.cs +++ b/Content.Server/Objectives/Systems/ChangelingObjectiveSystem.cs @@ -20,7 +20,10 @@ public sealed class ChangelingObjectiveSystem : EntitySystem private void OnChangelingDevoured(ref ChangelingDevouredEvent args) { - if (!args.Unique) + // We check if the devour granted us Dna. + // We do this because we could already have gotten the identity by other means before, such as the Dna sting. + // Dna grant basically ensures this is the first time valid devour has happened on a target. + if (!args.GrantedDna) return; if (!_mind.TryGetObjectiveComp(args.Changeling, out var obj)) diff --git a/Content.Shared/Changeling/Components/ChangelingBiodegradeAbilityComponent.cs b/Content.Shared/Changeling/Components/ChangelingBiodegradeAbilityComponent.cs index c0c97d5852..7c43f0d970 100644 --- a/Content.Shared/Changeling/Components/ChangelingBiodegradeAbilityComponent.cs +++ b/Content.Shared/Changeling/Components/ChangelingBiodegradeAbilityComponent.cs @@ -6,8 +6,7 @@ using Robust.Shared.GameStates; namespace Content.Shared.Changeling.Components; /// -/// Allows the changeling to spawn dummy chameleon clothing items that will transform with them, -/// mimicing the equipment of the stored disguise. +/// Allows the changeling to vomit acid over restraints, setting themselves free and stunning whoever is pulling them. /// [RegisterComponent, NetworkedComponent] public sealed partial class ChangelingBiodegradeAbilityComponent : Component diff --git a/Content.Shared/Changeling/Systems/ChangelingAbilitySystem.cs b/Content.Shared/Changeling/Systems/ChangelingAbilitySystem.cs index d7fb5111d2..825903e34e 100644 --- a/Content.Shared/Changeling/Systems/ChangelingAbilitySystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingAbilitySystem.cs @@ -1,4 +1,6 @@ using System.Linq; +using Content.Shared.Actions; +using Content.Shared.Atmos.Rotting; using Content.Shared.Changeling.Components; using Content.Shared.Cuffs; using Content.Shared.Ensnaring; @@ -20,12 +22,15 @@ public sealed partial class ChangelingAbilitySystem : EntitySystem [Dependency] private readonly PullingSystem _pulling = default!; [Dependency] private readonly SharedStunSystem _stun = default!; [Dependency] private readonly SharedPuddleSystem _puddle = default!; + [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!; + [Dependency] private readonly ChangelingDevourSystem _changelingDevour = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnBiodegradeAction); + SubscribeLocalEvent(OnStingDna); } private void OnBiodegradeAction(Entity ent, ref ChangelingBiodegradeActionEvent args) @@ -66,4 +71,23 @@ public sealed partial class ChangelingAbilitySystem : EntitySystem if (ent.Comp.SpillSolution != null) _puddle.TrySpillAt(args.Performer, ent.Comp.SpillSolution, out _, false); } + + private void OnStingDna(Entity ent, ref ChangelingStingDnaEvent args) + { + if (args.Target == ent.Owner) + return; // Can't sting yourself. + + if (!_changelingDevour.CanDevour(ent.Owner, args.Target, checkDead: false, checkProtected: false)) + return; + + _popup.PopupClient(Loc.GetString("changeling-sting-success", ("target", Identity.Entity(args.Target, EntityManager))), args.Target, ent.Owner, PopupType.Medium); + _changelingIdentity.GrantIdentity(ent, args.Target); + + args.Handled = true; + } } + +/// +/// Action event for the Dna sting ability. Used to grand the changeling an identity without devouring somebody. +/// +public sealed partial class ChangelingStingDnaEvent : EntityTargetActionEvent; diff --git a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs index 63aad8b4d6..adfcbc396a 100644 --- a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs @@ -1,4 +1,3 @@ -using System.Linq; using Content.Shared.Actions; using Content.Shared.Administration.Logs; using Content.Shared.Armor; @@ -29,7 +28,7 @@ public sealed class ChangelingDevourSystem : EntitySystem [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentitySystem = default!; + [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedStoreSystem _store = default!; @@ -175,7 +174,7 @@ public sealed class ChangelingDevourSystem : EntitySystem var willGrantDna = WillDevourGrantDna(ent.Owner, target); // Even if not unique, target is supposed to give us an identity if it is not currently in our identity list. - var becomesIdentity = !HasIdentity(ent.Owner, target); + var becomesIdentity = !_changelingIdentity.HasIdentity(ent.Owner, target); var ev = new ChangelingDevouredEvent(ent.Owner, target, becomesIdentity, uniqueIdentity, willGrantDna); RaiseLocalEvent(ent, ref ev, true); // We broadcast the event to allow relevant objectives to update. @@ -190,21 +189,10 @@ public sealed class ChangelingDevourSystem : EntitySystem _store.TryAddCurrency(ent.Comp.DevourDnaReward, ent.Owner, store); } - /// - /// Whether the given changeling has a valid identity of the given entity. - /// - public bool HasIdentity(Entity changeling, EntityUid devoured) - { - if (!Resolve(changeling, ref changeling.Comp, false)) - return false; - - return changeling.Comp.ConsumedIdentities.FirstOrDefault(data => data.Original == devoured && data.Identity != null) != null; - } - /// /// Can the given changeling devour the given victim? /// - public bool CanDevour(Entity changeling, EntityUid victim, bool showPopup = true) + public bool CanDevour(Entity changeling, EntityUid victim, bool checkDead = true, bool checkProtected = true, bool showPopup = true) { if (!Resolve(changeling, ref changeling.Comp)) return false; @@ -226,7 +214,7 @@ public sealed class ChangelingDevourSystem : EntitySystem return false; } - if (!_mobState.IsDead(victim)) + if (checkDead && !_mobState.IsDead(victim)) { if (showPopup) _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-not-dead"), changeling.Owner, changeling.Owner, PopupType.Medium); @@ -240,14 +228,14 @@ public sealed class ChangelingDevourSystem : EntitySystem return false; } - if (IsTargetProtected(victim, changeling!)) + if (checkProtected && IsTargetProtected(victim, changeling)) { if (showPopup) _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-protected"), changeling.Owner, changeling.Owner, PopupType.Medium); return false; } - if (!HasIdentity(changeling.Owner, victim) && !_changelingIdentitySystem.HasFreeDisguiseSlot(changeling.Owner)) + if (!_changelingIdentity.HasIdentity(changeling.Owner, victim) && !_changelingIdentity.HasFreeDisguiseSlot(changeling.Owner)) { if (showPopup) _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-no-space"), changeling.Owner, changeling.Owner, PopupType.Medium); @@ -263,8 +251,11 @@ public sealed class ChangelingDevourSystem : EntitySystem /// The Targeted entity /// Changelings Devour Component /// Is the target Protected from the attack - private bool IsTargetProtected(EntityUid target, Entity ent) + public bool IsTargetProtected(EntityUid target, Entity ent) { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + var ev = new CoefficientQueryEvent(SlotFlags.OUTERCLOTHING); RaiseLocalEvent(target, ev); @@ -291,7 +282,7 @@ public sealed class ChangelingDevourSystem : EntitySystem if (!Resolve(ent, ref ent.Comp, false)) return false; - return !_changelingIdentitySystem.TryGetDataFromOriginal(ent, devoured, out _); + return !_changelingIdentity.TryGetDataFromOriginal(ent, devoured, out _); } /// @@ -306,7 +297,7 @@ public sealed class ChangelingDevourSystem : EntitySystem return false; // This target was never devoured, so obviously it can grant us DNA. - if (!_changelingIdentitySystem.TryGetDataFromOriginal(ent, devoured, out var data)) + if (!_changelingIdentity.TryGetDataFromOriginal(ent, devoured, out var data)) return true; // If the entity was Devoured, it means it already granted DNA, so we return False. diff --git a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs index a6c1379a7a..7523533eb7 100644 --- a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs +++ b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs @@ -44,8 +44,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem { if (args.ObtainedIdentity) { - CloneToPausedMap(ent, args.Devoured); - AddDevouredReference(ent, args.Devoured); + GrantIdentity(ent, args.Devoured); } if (args.GrantedDna && TryGetDataFromOriginal(ent.AsNullable(), args.Devoured, out var data)) @@ -67,7 +66,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem private void OnMapInit(Entity ent, ref MapInitEvent args) { // Make a backup of our current identity so we can transform back. - CloneToPausedMap(ent, ent.Owner); + GrantIdentity(ent, ent.Owner); if (!TryGetDataFromOriginal(ent.AsNullable(), ent, out var data)) return; @@ -197,7 +196,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem /// /// The Changeling. /// The target to clone. - public EntityUid? CloneToPausedMap(Entity ent, EntityUid target) + public EntityUid? GrantIdentity(Entity ent, EntityUid target) { var clone = CloneToPausedMap(ent.Comp.IdentityCloningSettings, target); @@ -213,6 +212,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem } UpdateIdentityData(newIdentity, clone.Value, target); + AddDevouredReference(ent, target); HandlePvsOverride(ent, clone.Value); Dirty(ent); @@ -319,6 +319,17 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem return ent.Comp.ConsumedIdentities.Count(data => data.Identity != null) < ent.Comp.MaxStoredDisguises; } + /// + /// Whether the given changeling has a valid identity of the given entity. + /// + public bool HasIdentity(Entity changeling, EntityUid devoured) + { + if (!Resolve(changeling, ref changeling.Comp, false)) + return false; + + return changeling.Comp.ConsumedIdentities.FirstOrDefault(data => data.Original == devoured && data.Identity != null) != null; + } + /// /// Create a paused map for storing devoured identities as a clone of the player. /// diff --git a/Resources/Locale/en-US/changeling/changeling.ftl b/Resources/Locale/en-US/changeling/changeling.ftl index 4a851d6fa7..52daca4783 100644 --- a/Resources/Locale/en-US/changeling/changeling.ftl +++ b/Resources/Locale/en-US/changeling/changeling.ftl @@ -5,6 +5,7 @@ roles-antag-changeling-objective = A intelligent predator that assumes the ident # 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-devoured-recently = This body is too mangled to devour! changeling-devour-attempt-failed-not-dead = This body yet lives! We cannot consume it alive! changeling-devour-attempt-failed-rotting = This corpse has only rotted biomass. changeling-devour-attempt-failed-protected = This victim's biomass is protected by armor! @@ -25,8 +26,8 @@ changeling-transform-attempt-others = { CAPITALIZE(POSS-ADJ($user)) } bones snap 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. +changeling-transform-bui-drop-identity-entity-popup = We dropped {$entity} from our memory. +changeling-transform-bui-drop-identity-cannot-drop = We cannot drop our current identity. # voice mimicry changeling-voice-mimic-window-title = Voice Mimicry @@ -41,5 +42,8 @@ changeling-flesh-clothing-alert-desc = Whether clothing transformation is enable changeling-biodegrade-used-popup = {CAPITALIZE(THE($user))} vomits acid over {POSS-ADJ($user)} {$restraint}! changeling-biodegrade-used-popup-self = We vomit acid over our {$restraint}! +# stings +changeling-sting-success = We silently sting {THE($target)}! + # other changeling-paused-map-name = Changeling identity storage map diff --git a/Resources/Locale/en-US/store/changeling-catalog.ftl b/Resources/Locale/en-US/store/changeling-catalog.ftl index 893f2a84af..134ad32b08 100644 --- a/Resources/Locale/en-US/store/changeling-catalog.ftl +++ b/Resources/Locale/en-US/store/changeling-catalog.ftl @@ -9,3 +9,6 @@ changeling-catalog-voice-mimic-desc = Change your vocal coords at will to imitat changeling-catalog-biodegrade-name = Biodegrade changeling-catalog-biodegrade-desc = You learn to utilize acid glands within your body to vomit acid over constraints, setting yourself free. + +changeling-catalog-sting-dna-name = Extract DNA +changeling-catalog-sting-dna-desc = Learn to utilize your stinger to absorb the identity of unsuspecting victims. Does not grant DNA to your store or objective progress. diff --git a/Resources/Prototypes/Actions/changeling.yml b/Resources/Prototypes/Actions/changeling.yml index c3cd19e5ec..d7fb2b5610 100644 --- a/Resources/Prototypes/Actions/changeling.yml +++ b/Resources/Prototypes/Actions/changeling.yml @@ -29,7 +29,6 @@ - type: Action useDelay: 60 # TODO: Short cooldown but chemical cost raiseOnAction: true - itemIconStyle: BigAction checkCanInteract: false icon: sprite: Interface/Actions/changeling2.rsi @@ -46,6 +45,7 @@ - type: Action icon: { sprite : Interface/Actions/changeling.rsi, state: "devour" } iconOn: { sprite : Interface/Actions/changeling.rsi, state: "devour_on" } + itemIconStyle: BigAction priority: 1 - type: TargetAction - type: EntityTargetAction @@ -80,3 +80,25 @@ state: mimicry - type: InstantAction event: !type:VoiceMaskSetNameEvent + +- type: entity + parent: BaseAction + id: ActionChangelingStingDna + name: Extract DNA + description: Sneakily absorb the identity of unsuspecting victims. Does not grant DNA to your store or objective progress. + components: + - type: Action + icon: { sprite : Interface/Actions/changeling2.rsi, state: "extract_dna_off" } + iconOn: { sprite : Interface/Actions/changeling2.rsi, state: "extract_dna_on" } + itemIconStyle: BigAction + priority: 1 + useDelay: 90 # TODO: Chemical cost, maybe this could be a DoAfter action? + raiseOnUser: true + - type: TargetAction + - type: EntityTargetAction + canTargetSelf: false + whitelist: + components: + - MobState + - HumanoidProfile + event: !type:ChangelingStingDnaEvent diff --git a/Resources/Prototypes/Catalog/changeling_catalog.yml b/Resources/Prototypes/Catalog/changeling_catalog.yml index c15fbde62c..f03ddeb7b1 100644 --- a/Resources/Prototypes/Catalog/changeling_catalog.yml +++ b/Resources/Prototypes/Catalog/changeling_catalog.yml @@ -56,3 +56,17 @@ - !type:ListingLimitedStockCondition stock: 1 productAction: ActionChangelingBiodegrade + +- type: listing + id: ChangelingStingDna + name: changeling-catalog-sting-dna-name + description: changeling-catalog-sting-dna-desc + applyToMob: true + cost: + ChangelingDNA: 10 + categories: + - ChangelingStoreStealth + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + productAction: ActionChangelingStingDna diff --git a/Resources/Textures/Interface/Actions/changeling2.rsi/extract_dna_off.png b/Resources/Textures/Interface/Actions/changeling2.rsi/extract_dna_off.png new file mode 100644 index 0000000000..593e0ddcc4 Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling2.rsi/extract_dna_off.png differ diff --git a/Resources/Textures/Interface/Actions/changeling2.rsi/extract_dna_on.png b/Resources/Textures/Interface/Actions/changeling2.rsi/extract_dna_on.png new file mode 100644 index 0000000000..93b1726bba Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling2.rsi/extract_dna_on.png differ diff --git a/Resources/Textures/Interface/Actions/changeling2.rsi/meta.json b/Resources/Textures/Interface/Actions/changeling2.rsi/meta.json index 3a9cf0c5af..6a4ade2d24 100644 --- a/Resources/Textures/Interface/Actions/changeling2.rsi/meta.json +++ b/Resources/Textures/Interface/Actions/changeling2.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Made by ketufaispikinut from a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (scientist suit), a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (hydro suit) and the changeling ability border/background sprites created by TiniestShark. Biodegrade and Mimicry by ketufaispikinut, based on TiniestShark's textures.", + "copyright": "Made by ketufaispikinut from a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (scientist suit), a sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c838ba21dae97db345e0113f99596decd1d66039 (hydro suit) and the changeling ability border/background sprites created by TiniestShark. Biodegrade, Mimicry and Extract Dna On/Off by ketufaispikinut, based on TiniestShark's textures.", "size": { "x": 32, "y": 32 @@ -19,5 +19,13 @@ { "name": "biodegrade" } + , + { + "name": "extract_dna_on" + } + , + { + "name": "extract_dna_off" + } ] }