Changeling Dna Sting (#43785)

* init

* fix objective

* Somehow this locale got lost i guess

* some more flavor idk

* wording

* sprites

* be gone copy paste

* we

* review

* rename

* explosion

* oops

* Apply suggestion from @slarticodefast

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
ScarKy0 2026-04-29 23:16:39 +02:00 • committed by GitHub
parent 510e9e6139
commit b3619d28c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 111 additions and 32 deletions

View file

@ -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<ChangelingUniqueIdentityConditionComponent>(args.Changeling, out var obj))

View file

@ -6,8 +6,7 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Changeling.Components;
/// <summary>
/// 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.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ChangelingBiodegradeAbilityComponent : Component

View file

@ -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<ChangelingBiodegradeAbilityComponent, ChangelingBiodegradeActionEvent>(OnBiodegradeAction);
SubscribeLocalEvent<ChangelingIdentityComponent, ChangelingStingDnaEvent>(OnStingDna);
}
private void OnBiodegradeAction(Entity<ChangelingBiodegradeAbilityComponent> 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<ChangelingIdentityComponent> 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;
}
}
/// <summary>
/// Action event for the Dna sting ability. Used to grand the changeling an identity without devouring somebody.
/// </summary>
public sealed partial class ChangelingStingDnaEvent : EntityTargetActionEvent;

View file

@ -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);
}
/// <summary>
/// Whether the given changeling has a valid identity of the given entity.
/// </summary>
public bool HasIdentity(Entity<ChangelingIdentityComponent?> 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;
}
/// <summary>
/// Can the given changeling devour the given victim?
/// </summary>
public bool CanDevour(Entity<ChangelingDevourComponent?> changeling, EntityUid victim, bool showPopup = true)
public bool CanDevour(Entity<ChangelingDevourComponent?> 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
/// <param name="target">The Targeted entity</param>
/// <param name="ent">Changelings Devour Component</param>
/// <returns>Is the target Protected from the attack</returns>
private bool IsTargetProtected(EntityUid target, Entity<ChangelingDevourComponent> ent)
public bool IsTargetProtected(EntityUid target, Entity<ChangelingDevourComponent?> 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 _);
}
/// <summary>
@ -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.

View file

@ -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<ChangelingIdentityComponent> 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
/// </summary>
/// <param name="ent">The Changeling.</param>
/// <param name="target">The target to clone.</param>
public EntityUid? CloneToPausedMap(Entity<ChangelingIdentityComponent> ent, EntityUid target)
public EntityUid? GrantIdentity(Entity<ChangelingIdentityComponent> 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;
}
/// <summary>
/// Whether the given changeling has a valid identity of the given entity.
/// </summary>
public bool HasIdentity(Entity<ChangelingIdentityComponent?> 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;
}
/// <summary>
/// Create a paused map for storing devoured identities as a clone of the player.
/// </summary>

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -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"
}
]
}