Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Content.Shared.Actions;
using Robust.Shared.Audio;
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.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ChangelingBiodegradeAbilityComponent : Component
{
/// <summary>
/// How long to stun the puller of the changeling when this action is activated.
/// </summary>
[DataField]
public TimeSpan PullerStunDuration = TimeSpan.FromSeconds(3f);

/// <summary>
/// The popup to display over the user when the action is used.
/// Only visible to other players.
/// </summary>
[DataField]
public LocId ActivatedPopup = "changeling-biodegrade-used-popup";

/// <summary>
/// The popup to display over the user when the action is used.
/// Only visible to the user
/// </summary>
[DataField]
public LocId ActivatedPopupSelf = "changeling-biodegrade-used-popup-self";

/// <summary>
/// The sound to play when the ability is successfully used.
/// </summary>
[DataField]
public SoundSpecifier ActivatedSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
Comment thread
ScarKy0 marked this conversation as resolved.
}

/// <summary>
/// Action event for Biodegrade, raised on the ability when used.
/// </summary>
public sealed partial class ChangelingBiodegradeActionEvent : InstantActionEvent;
71 changes: 71 additions & 0 deletions Content.Shared/Changeling/Systems/ChangelingAbilitySystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Linq;
using Content.Shared.Changeling.Components;
using Content.Shared.Cuffs;
using Content.Shared.Ensnaring;
using Content.Shared.IdentityManagement;
using Content.Shared.Movement.Pulling.Systems;
using Content.Shared.Popups;
using Content.Shared.Stunnable;
using Robust.Shared.Audio.Systems;

namespace Content.Shared.Changeling.Systems;

public sealed partial class ChangelingAbilitySystem : EntitySystem
{

[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedCuffableSystem _cuffable = default!;
[Dependency] private readonly SharedEnsnareableSystem _snare = default!;
[Dependency] private readonly PullingSystem _pulling = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<ChangelingBiodegradeAbilityComponent, ChangelingBiodegradeActionEvent>(OnBiodegradeAction);
}

private void OnBiodegradeAction(Entity<ChangelingBiodegradeAbilityComponent> ent, ref ChangelingBiodegradeActionEvent args)
{
// Nothing can be done :(
if (!_cuffable.IsCuffed(args.Performer) && !_snare.IsEnsnared(args.Performer))
return;

if (_pulling.GetPuller(args.Performer) is { } puller)
{
_stun.TryAddParalyzeDuration(puller, ent.Comp.PullerStunDuration);
}

List<EntityUid> toDelete = new List<EntityUid>();

_cuffable.TryGetAllCuffs(args.Performer, out var cuffs);
foreach (var cuff in cuffs.ToList())
{
_cuffable.Uncuff(args.Performer, args.Performer, cuff);
toDelete.Add(cuff);
}

toDelete.AddRange(_snare.ForceFreeAll(args.Performer));

args.Handled = true;

// How can you be ensnared/cuffed and have nothing detected??
if (toDelete.Count == 0)
return;
Comment on lines +56 to +58
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory impossible but client crashes without it. Checking for ApplyingState doesn't help.
Complains that the list is empty, so probably something with snares or cuffs being not predicted/networked correctly?
Everything else works fine.


var selfPopup = Loc.TryGetString(ent.Comp.ActivatedPopupSelf, out var self, ("user", Identity.Entity(args.Performer, EntityManager)), ("cuffs", toDelete.First())) ? self : null;
var othersPopup = Loc.TryGetString(ent.Comp.ActivatedPopup, out var others, ("user", Identity.Entity(args.Performer, EntityManager)), ("cuffs", toDelete.First())) ? others : null;

_popup.PopupPredicted(othersPopup, selfPopup, args.Performer, args.Performer, PopupType.LargeCaution);
_audio.PlayPredicted(ent.Comp.ActivatedSound, args.Performer, args.Performer);

foreach (var deleted in toDelete)
{
PredictedQueueDel(deleted);
}

// TODO: Should probably spawn a puddle of acid. But solutions are frozen due to an upcoming refactor.
}
}
5 changes: 4 additions & 1 deletion Content.Shared/Cuffs/SharedCuffableSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,11 @@ public bool TryCuffing(EntityUid user, EntityUid target, EntityUid handcuff, Han
/// /// <param name="target">The entity to be checked</param>
/// <param name="requireFullyCuffed">when true, return false if the target is only partially cuffed (for things with more than 2 hands)</param>
/// <returns></returns>
public bool IsCuffed(Entity<CuffableComponent> target, bool requireFullyCuffed = true)
public bool IsCuffed(Entity<CuffableComponent?> target, bool requireFullyCuffed = true)
{
if (!Resolve(target, ref target.Comp, false))
return false;

if (!TryComp<HandsComponent>(target, out var hands))
return false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Content.Shared.Ensnaring.Components;
/// Use this on an entity that you would like to be ensnared by anything that has the <see cref="EnsnaringComponent"/>
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[Access(typeof(SharedEnsnareableSystem))]
public sealed partial class EnsnareableComponent : Component
{
/// <summary>
Expand Down
1 change: 1 addition & 0 deletions Content.Shared/Ensnaring/Components/EnsnaringComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Content.Shared.Ensnaring.Components;
/// Use this on something you want to use to ensnare an entity with
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedEnsnareableSystem))]
public sealed partial class EnsnaringComponent : Component
{
/// <summary>
Expand Down
60 changes: 48 additions & 12 deletions Content.Shared/Ensnaring/SharedEnsnareableSystem.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using Content.Shared.Alert;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Damage.Components;
Expand Down Expand Up @@ -82,12 +83,12 @@ private void OnDoAfter(EntityUid uid, EnsnareableComponent component, DoAfterEve
return;
}

_hands.PickupOrDrop(args.Args.User, args.Args.Used.Value);

component.IsEnsnared = component.Container.ContainedEntities.Count > 0;
Dirty(uid, component);
ensnaring.Ensnared = null;

_hands.PickupOrDrop(args.Args.User, args.Args.Used.Value);

if (args.User == args.Target)
Popup.PopupPredicted(Loc.GetString("ensnare-component-try-free-complete", ("ensnare", args.Args.Used)), uid, args.User, PopupType.Medium);
else if (args.Target != null)
Expand Down Expand Up @@ -210,7 +211,7 @@ private void OnComponentRemove(EntityUid uid, EnsnaringComponent component, Comp
return;

if (ensnared.IsEnsnared)
ForceFree(uid, component);
ForceFree((uid, component));
}

private void AttemptStepTrigger(EntityUid uid, EnsnaringComponent component, ref StepTriggerAttemptEvent args)
Expand Down Expand Up @@ -273,24 +274,59 @@ public bool TryEnsnare(EntityUid target, EntityUid ensnare, EnsnaringComponent c
/// <summary>
/// Used to force free someone for things like if the <see cref="EnsnaringComponent"/> is removed
/// </summary>
public void ForceFree(EntityUid ensnare, EnsnaringComponent component)
public void ForceFree(Entity<EnsnaringComponent?> entity)
{
if (component.Ensnared == null)
if (!Resolve(entity, ref entity.Comp, false))
return;

if (!TryComp<EnsnareableComponent>(component.Ensnared, out var ensnareable))
if (!TryComp<EnsnareableComponent>(entity.Comp.Ensnared, out var ensnareable))
return;

var target = component.Ensnared.Value;
var target = entity.Comp.Ensnared.Value;

Container.Remove(entity.Owner, ensnareable.Container, force: true);

Container.Remove(ensnare, ensnareable.Container, force: true);
ensnareable.IsEnsnared = ensnareable.Container.ContainedEntities.Count > 0;
Dirty(component.Ensnared.Value, ensnareable);
component.Ensnared = null;
Dirty(entity.Comp.Ensnared.Value, ensnareable);
entity.Comp.Ensnared = null;

UpdateAlert(target, ensnareable);
var ev = new EnsnareRemoveEvent(component.WalkSpeed, component.SprintSpeed);
RaiseLocalEvent(ensnare, ev);
var ev = new EnsnareRemoveEvent(entity.Comp.WalkSpeed, entity.Comp.SprintSpeed);
RaiseLocalEvent(target, ev);
}

/// <summary>
/// Removes all ensnares from an entity.
/// </summary>
/// <param name="entity">The entity to remove snares from.</param>
/// <returns>The list of removed snares.</returns>
public List<EntityUid> ForceFreeAll(Entity<EnsnareableComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp, false))
return new List<EntityUid>();

List<EntityUid> snares = new();

foreach (var snare in entity.Comp.Container.ContainedEntities.ToList())
{
ForceFree(snare);
snares.Add(snare);
}

return snares;
}

/// <summary>
/// Checks whether an entity is currently being ensnared.
/// </summary>
/// <param name="entity">The entity to check.</param>
/// <returns>True if ensnared, otherwise False.</returns>
public bool IsEnsnared(Entity<EnsnareableComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp, false))
return false;

return entity.Comp.IsEnsnared;
}

/// <summary>
Expand Down
14 changes: 10 additions & 4 deletions Resources/Locale/en-US/changeling/changeling.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ changeling-devour-consume-complete-others = { CAPITALIZE(POSS-ADJ($user)) } unca
# 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.
changeling-flesh-clothing-removed-popop = {CAPITALIZE(THE($item))} falls apart into fleshy remains!
changeling-flesh-clothing-examine-wearer = [color=crimson]This item is a camouflaged part of your body. It will disappear if you unequip it![/color]
changeling-flesh-clothing-alert-name = Flesh Clothing Ability
changeling-flesh-clothing-alert-desc = Whether clothing transformation is enabled. Click to toggle.

# transformation BUI
changeling-transform-bui-select-entity = {$entity}
Expand All @@ -31,5 +27,15 @@ 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.

# Abilities
changeling-flesh-clothing-removed-popop = {CAPITALIZE(THE($item))} falls apart into fleshy remains!
changeling-flesh-clothing-examine-wearer = [color=crimson]This item is a camouflaged part of your body. It will disappear if you unequip it![/color]
changeling-flesh-clothing-alert-name = Flesh Clothing Ability
changeling-flesh-clothing-alert-desc = Whether clothing transformation is enabled. Click to toggle.
changeling-biodegrade-used-popup = {CAPITALIZE(THE($user))} vomits acid over {POSS-ADJ($user)} {$cuffs}!
changeling-biodegrade-used-popup-self = We vomit acid over our {$cuffs}!


# other
changeling-paused-map-name = Changeling identity storage map

3 changes: 3 additions & 0 deletions Resources/Locale/en-US/store/changeling-catalog.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ changeling-catalog-arm-blade-desc = Transform your arm into a terrifying flesh b
changeling-catalog-flesh-clothing-name = Flesh Clothing
changeling-catalog-flesh-clothing-desc = Your body's surface will adapt to mirror the clothing of any person you are transforming into. However, these clothing items are non-functional and will make you easy to identify as a changeling if someone tries to remove them. Can be toggled.
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.
18 changes: 18 additions & 0 deletions Resources/Prototypes/Actions/changeling.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@
retractSounds:
collection: gib # Placeholder

- type: entity
parent: BaseAction
id: ActionChangelingBiodegrade
name: Biodegrade
description: Vomit acid over your restraints, setting yourself free.
components:
- type: Action
useDelay: 60 # TODO: Short cooldown but chemical cost
raiseOnAction: true
itemIconStyle: BigAction
checkCanInteract: false
icon:
sprite: Interface/Actions/changeling2.rsi
state: biodegrade
- type: InstantAction
event: !type:ChangelingBiodegradeActionEvent
- type: ChangelingBiodegradeAbility

- type: entity
id: ActionChangelingDevour
name: "[color=red]Devour[/color]"
Expand Down
19 changes: 17 additions & 2 deletions Resources/Prototypes/Catalog/changeling_catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
description: changeling-catalog-arm-blade-desc
applyToMob: true
cost:
ChangelingDNA: 25
ChangelingDNA: 10 # TODO: Reasonable prices once we get DNA per devour. It's low for the sake of testing.
categories:
- ChangelingAbilities
conditions:
Expand All @@ -20,10 +20,25 @@
description: changeling-catalog-flesh-clothing-desc
icon: { sprite: /Textures/Interface/Actions/changeling2.rsi, state: flesh_clothing }
cost:
ChangelingDNA: 10
ChangelingDNA: 10 # TODO: Reasonable prices once we get DNA per devour. It's low for the sake of testing.
categories:
- ChangelingAbilities
conditions:
- !type:ListingLimitedStockCondition
stock: 1
productComponents: ChangelingFleshClothingAbilityStoreDummy

- type: listing
id: ChangelingBiodegrade
name: changeling-catalog-biodegrade-name
description: changeling-catalog-biodegrade-desc
icon: { sprite: /Textures/Interface/Actions/changeling2.rsi, state: biodegrade }
applyToMob: true
cost:
ChangelingDNA: 10 # TODO: Reasonable prices once we get DNA per devour. It's low for the sake of testing.
categories:
- ChangelingAbilities
conditions:
- !type:ListingLimitedStockCondition
stock: 1
productAction: ActionChangelingBiodegrade
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
{
"version": 1,
"license": "CC0-1.0",
"copyright": "Created by TiniestShark (github)",
"copyright": "Created by TiniestShark (github).",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "background"
},
{
"name": "frame"
},
{
"name": "transform"
},
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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.",
"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 taken from tgstation at https://github.com/tgstation/tgstation/commit/16aef3a2fd640e6cd97f5704aaaf5959d75921c3 with border/background created by TiniestShark.",
"size": {
"x": 32,
"y": 32
Expand All @@ -12,6 +12,9 @@
},
{
"name": "flesh_clothing_alt"
},
{
"name": "biodegrade"
}
]
}
Loading