Changeling devour and transform (#34002)

* Initial:

Create Devour componentry, preliminary identity storage and the systems
for Devouring

* I have genuinely no idea what i'm doing

- added the radial menu, it has nothing in it.

- trying to get it to populate. the event under the event is broken,
i don't know why, but apparently it's not typed right

- Added a placeholder transform

- oh also fixed up some devour stuff and moved some things around.

* Holey moley, Transform, better devour, oh my!

- Move DnaComponent into Shared because we need it for the DNA cloning

- Make Transform MOSTLY work on the LAST identity devoured.

- Fix some issues on devour that involved prediction, canceling and
Damage exeucting (Thanks Plykiya for pointing out AttemptFrequency!)

* Proper tail stealing and Damage modifier attempt

Add check to add a wagging component on the Changeling if the victim's
species Prototype had one.

attempt to add the Damage mitigation check

* MAJOR CLEANUP AND FIXES AUGH 3 DAYS!!!

- Nullspaced a clone of a victim

- fix audio using server virtualized Pvs (i hate this)

- fix the mispredicted doafters

- Clean up a wholelotta code

- utilize clone systems to clone appearances

- Move CloneAppearance from server to shared So we can actually access
it

* Examine stuff, more cleanup, Jumpsuit ripping

- make rotting prevent the action

- Add ripping of clothing (guh why is it also server)

- add some System stuff for pushing husked corpse inspection

- clean up more badcode

* Doing things properly, UI sorta kinda works.

- Utilize Relayed events for Devour checking

- Get a UI that partially works, Says the name of identities, doesn't
show their looks

- Make use of the New Dynamic BUI assignment

- commit the sin of no client prediction cause nullspace entities aren't
networked

* Got an entity for the Frontend transform

Issue with the looks

* Stick a camera into a fake mobs forehead

- Get the UI to see the net entity in pause space by using a
ViewSubscriber to get the Pvs from the initially stored identity entity

- Remove all the other parts used to try to get this to work before hand

* Raaaaadiallllllls also fix protection coefficents

- Change FancyWindow to Radial

- Fix Issue where coeffeient checking was the wrong way round

* absolutely massive cleanup, no more camera in mobs

- cleaned up event variables that are not needed

- Removed the use of a Pause space and go back to Nullspace usage

- use a PvsOverride rather than ViewSharing

- Remove old commented out code and Lots of unused code

* Fix "Ui elements" dying  on the screen

- some minor cleanup

- don't start the entities that get cloned

* ftl, cleanup, and fixing missing transform details

- add replace functionality to TypingIndicatorSystem and
BodyEmotesSystem

- add placeholder sounds and functions to TransformBodyEmotes

- add extra Pvs handling for later use

- attributions for the funny straw sound

- Sound collections for all of the sounds

- various cleanups

* Some extra cleanup

* Fix some false assumptions about TypingIndicator

- Bubbles now transfer on spawned humans rather than used humans

- Clean up YET MORE CODE

- make it so you can't eat yourself

* Oooprs, forgot to add a Husked Corpse Loc

* Missing period in the husked corpse loc

* bad devour windup placeholder

* Husking and WIP Lungs

- Husking now will be prevented from Revival fully and will change
the appearance of players

* Add finalized Sprites for actions and final meta

- add devour on and off sprites

- add transform action sprite

- Add Armblade sprite for future use

- Credit obscenelytinyshark for the sprites <3

* Remove ling lungs, Entity<> everything

- Remove the ling lungs stuff for now... body system is overly
complicated, makes my head hurt

- Switch every method to use Entity<> from Uid, Comp format

* cleanup, admin logging, WIP Roles

* Admin verb, Roundstart, gamerule stuff

- add a Admin verb to make Changelingification easy!

- Add game rule stuff for admin verb and to tell the hapless
goober how to be a changeling... sorta

- clean up parts to make VV easy... USE THE VERB!!

* Armor Coefficent Check

- Remove bespoke changeling armor check and replace it
with a generic armor coefficient query.

* move to UnrevivableComponent instead of husked

- Move UnrevivableComponent to shared

- add Analyzable and ReasonMessage to UnrevivableComponent
to give granular control of the message and whether or not it shows up
in the analyzer

- remove the check for HuskedComponent in DefibrillatorSystem

* aaaaaaa CopyComp

- Some cleanup

- make Vocal system shared

- make VocalSystem Not make more Actions than it needs

- Use some code from ChameleonProjector so we can copy components

- partially ungod method the Transform system

* Cleanup, Moving more things to CopyComp

- TransformBodyEmotes now uses CopyComp (it's a server component so i
need to tell the server to deal with it

- TypingIndicatorComponent also now uses CopyComp

- cleaned up old, now unused "replace" methods in favor of CopyComp

- BodyEmotesSystem now has a publically accessable LoadSounds to deal
with the same problem Screaming had

* WIP

* Devour Windup noise, ForensicsSystem cleanup

* Revert VocalSystem Changes

- Reverted Moving VocalSystem to shared, copy comp acomplishes it

- added component.ScreamActionEntity = null; for copy comp

* cleanup unneeded comments

* revert an accidental line removal

* Remove duplicate SharedHumanoidAppearanceSystem

* Cleanup Typo's and import Forensics components for Dna

* Some more forensics calls

* cleanup use CopyComp for now until CopyComps

* CR cleanup

* Undo some SharedHumanoidAppearanceSystem changes

* Confound these spaces

* Some Copycomp stuff and fixing some PVS override

* use the proper TryCopyComps that are merged

* Change TransformMenu with RadialWithSector

* All sounds done, Fix lack of typing indicator issue

* Updated attributions to include used sound authors

* some ftl typos and mind_role text issue

* DNA, Screaming, appearance, grammar, wagging

- reduced all of the above using ApplyComponentChanges

- Issue still remains with bodyEmotes sticking around in the UI

* Fix UI stuff, partials, entprotoid, good practices

- bunch of partials added

- UI now has a predicted message

- EntProtoID in the admin verb

- RipClothing now uses Entity<ButcherableComponent>

- husking is now optional (off by default) for testing/till we have
hivemind/when we figure out what were doing with devour

- remove TransformGrammarSet

* More CR stuff and documentation

- Make TargetIsProtected less of a meme, with a prototype
set of DamageTypes to check

- Documenation everywhere

- Move DevourEvents into its own file

* Predicted sounds and fix the comp clone list

- Made all start and stop sounds shared

- Split out the rest of the events and UI stuff into subfiles

- Fixed some Clone comp list issues where comments had -'s causing them
to be read incorrectly

* Damage cap check, Identity Shutdown cleanup, cleanup

* Sound stuff (but actually this time)

* Missed documentation

* Missed Documentation and a EntProtoId

* Remove unused dependency

* Remove a nullcheck

* Some dummy minplayers

* CR - Husked now uses a rem/ensure

* Update Actions in the Prototype

* Fixup mindswap handover

- cleanup and handover PVS on mindswap

* Fixup Missing meta from accidental "Take-theirs"

* Add the Armblade to the roundstart-role

* Cleanup, CR (everything but the UI and renames)

* missed a spot

* missed some more whitespace

* Renames

* Primary constructor and a space in these trying times

* User interface stuff for Slime transformation

* popup prediction

* Ling devour no longer makes duplicate identities

- added a key to identities to the original victim

- Add some extra clone settings

* add guard statements to OnClones

* SentOnlyToOwner additions

* fix for sound stoppage error

* Move Organ deleter into soon to be atomized husk

* clone event inventory

* mono sounds

* lower sound volume

* Fix networked sound warning

* Clone comps thing

* review

* attributions

* Fix clobbered changes

* I'm gonna weh out

- whole bunch of CR changes

* fix some very buggy git

* okay its fixed

* address most review points

* fix inventory

* we hate entityuids

* fix test and more cleanup

* move this

* fix more stuff

* fix validation and rootable

* Remove Quickswitch due to some UI quirks

* oops left out some better explanation

* remove dangling LastConsumed component fields

* fix test fail

* try this

* cleanup cloning subscriptions, add movement speed modifier

* fix slime storage

* fix cloning setting inheritance

* Add session information to transform admin logs

* slay the integration test hydra

* dwarf size

* more volume tweaks

* comments

* improve comments and unpredict deletion due to errors when shutting down the server

* fix displancement cloning

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
poklj
2025-08-06 16:55:49 -03:00
committed by GitHub
parent 534553dddf
commit f76e3b63b7
52 changed files with 1583 additions and 93 deletions

View File

@@ -0,0 +1,35 @@
using Content.Shared.Cloning;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Changeling;
/// <summary>
/// The storage component for Changelings, it handles the link between a changeling and its consumed identities
/// that exist on a paused map.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ChangelingIdentityComponent : Component
{
/// <summary>
/// The list of entities that exist on a paused map. They are paused clones of the victims that the ling has consumed, with all relevant components copied from the original.
/// </summary>
// TODO: Store a reference to the original entity as well so you cannot infinitely devour somebody. Currently very tricky due the inability to send over EntityUid if the original is ever deleted. Can be fixed by something like WeakEntityReference.
[DataField, AutoNetworkedField]
public List<EntityUid> ConsumedIdentities = new();
/// <summary>
/// The currently assumed identity.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? CurrentIdentity;
/// <summary>
/// The cloning settings passed to the CloningSystem, contains a list of all components to copy or have handled by their
/// respective systems.
/// </summary>
public ProtoId<CloningSettingsPrototype> IdentityCloningSettings = "ChangelingCloningSettings";
public override bool SendOnlyToOwner => true;
}

View File

@@ -0,0 +1,180 @@
using System.Numerics;
using Content.Shared.Cloning;
using Content.Shared.Humanoid;
using Content.Shared.Mind.Components;
using Content.Shared.NameModifier.EntitySystems;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Shared.Changeling;
public sealed class ChangelingIdentitySystem : EntitySystem
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
[Dependency] private readonly SharedCloningSystem _cloningSystem = default!;
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SharedPvsOverrideSystem _pvsOverrideSystem = default!;
public MapId? PausedMapId;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChangelingIdentityComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChangelingIdentityComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ChangelingIdentityComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<ChangelingIdentityComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<ChangelingStoredIdentityComponent, ComponentRemove>(OnStoredRemove);
}
private void OnMindAdded(Entity<ChangelingIdentityComponent> ent, ref MindAddedMessage args)
{
if (!TryComp<ActorComponent>(args.Container.Owner, out var actor))
return;
HandOverPvsOverride(actor.PlayerSession, ent.Comp);
}
private void OnMindRemoved(Entity<ChangelingIdentityComponent> ent, ref MindRemovedMessage args)
{
CleanupPvsOverride(ent, args.Container.Owner);
}
private void OnMapInit(Entity<ChangelingIdentityComponent> ent, ref MapInitEvent args)
{
// Make a backup of our current identity so we can transform back.
var clone = CloneToPausedMap(ent, ent.Owner);
ent.Comp.CurrentIdentity = clone;
}
private void OnShutdown(Entity<ChangelingIdentityComponent> ent, ref ComponentShutdown args)
{
CleanupPvsOverride(ent, ent.Owner);
CleanupChangelingNullspaceIdentities(ent);
}
private void OnStoredRemove(Entity<ChangelingStoredIdentityComponent> ent, ref ComponentRemove args)
{
// The last stored identity is being deleted, we can clean up the map.
if (_net.IsServer && PausedMapId != null && Count<ChangelingStoredIdentityComponent>() <= 1)
_map.QueueDeleteMap(PausedMapId.Value);
}
/// <summary>
/// Cleanup all nullspaced Identities when the changeling no longer exists
/// </summary>
/// <param name="ent">the changeling</param>
public void CleanupChangelingNullspaceIdentities(Entity<ChangelingIdentityComponent> ent)
{
if (_net.IsClient)
return;
foreach (var consumedIdentity in ent.Comp.ConsumedIdentities)
{
QueueDel(consumedIdentity);
}
}
/// <summary>
/// Clone a target humanoid into nullspace and add it to the Changelings list of identities.
/// It creates a perfect copy of the target and can be used to pull components down for future use
/// </summary>
/// <param name="ent">the Changeling</param>
/// <param name="target">the targets uid</param>
public EntityUid? CloneToPausedMap(Entity<ChangelingIdentityComponent> ent, EntityUid target)
{
// Don't create client side duplicate clones or a clientside map.
if (_net.IsClient)
return null;
if (!TryComp<HumanoidAppearanceComponent>(target, out var humanoid)
|| !_prototype.Resolve(humanoid.Species, out var speciesPrototype)
|| !_prototype.Resolve(ent.Comp.IdentityCloningSettings, out var settings))
return null;
EnsurePausedMap();
var mob = Spawn(speciesPrototype.Prototype, new MapCoordinates(Vector2.Zero, PausedMapId!.Value));
var storedIdentity = EnsureComp<ChangelingStoredIdentityComponent>(mob);
storedIdentity.OriginalEntity = target; // TODO: network this once we have WeakEntityReference or the autonetworking source gen is fixed
if (TryComp<ActorComponent>(target, out var actor))
storedIdentity.OriginalSession = actor.PlayerSession;
_humanoidSystem.CloneAppearance(target, mob);
_cloningSystem.CloneComponents(target, mob, settings);
var targetName = _nameMod.GetBaseName(target);
_metaSystem.SetEntityName(mob, targetName);
ent.Comp.ConsumedIdentities.Add(mob);
Dirty(ent);
HandlePvsOverride(ent, mob);
return mob;
}
/// <summary>
/// Simple helper to add a PVS override to a Nullspace Identity
/// </summary>
/// <param name="uid"></param>
/// <param name="target"></param>
private void HandlePvsOverride(EntityUid uid, EntityUid target)
{
if (!TryComp<ActorComponent>(uid, out var actor))
return;
_pvsOverrideSystem.AddSessionOverride(target, actor.PlayerSession);
}
/// <summary>
/// Cleanup all Pvs Overrides for the owner of the ChangelingIdentity
/// </summary>
/// <param name="ent">the Changeling itself</param>
/// <param name="entityUid">Who specifically to cleanup from, usually just the same owner, but in the case of a mindswap we want to clean up the victim</param>
private void CleanupPvsOverride(Entity<ChangelingIdentityComponent> ent, EntityUid entityUid)
{
if (!TryComp<ActorComponent>(entityUid, out var actor))
return;
foreach (var identity in ent.Comp.ConsumedIdentities)
{
_pvsOverrideSystem.RemoveSessionOverride(identity, actor.PlayerSession);
}
}
/// <summary>
/// Inform another Session of the entities stored for Transformation
/// </summary>
/// <param name="session">The Session you wish to inform</param>
/// <param name="comp">The Target storage of identities</param>
public void HandOverPvsOverride(ICommonSession session, ChangelingIdentityComponent comp)
{
foreach (var entity in comp.ConsumedIdentities)
{
_pvsOverrideSystem.AddSessionOverride(entity, session);
}
}
/// <summary>
/// Create a paused map for storing devoured identities as a clone of the player.
/// </summary>
private void EnsurePausedMap()
{
if (_map.MapExists(PausedMapId))
return;
var mapUid = _map.CreateMap(out var newMapId);
_metaSystem.SetEntityName(mapUid, "Changeling identity storage map");
PausedMapId = newMapId;
_map.SetPaused(mapUid, true);
}
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.Roles;
namespace Content.Shared.Changeling;
/// <summary>
/// The Mindrole for Changeling Antags
/// </summary>
[RegisterComponent]
public sealed partial class ChangelingRoleComponent : BaseMindRoleComponent;

View File

@@ -0,0 +1,29 @@
using Robust.Shared.GameStates;
using Robust.Shared.Player;
namespace Content.Shared.Changeling;
/// <summary>
/// Marker component for cloned identities devoured by a changeling.
/// These are stored on a paused map so that the changeling can transform into them.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ChangelingStoredIdentityComponent : Component
{
/// <summary>
/// The original entity the identity was cloned from.
/// </summary>
/// <remarks>
/// TODO: Not networked at the moment because it will create PVS errors when the original is somehow deleted.
/// Use WeakEntityReference once it's merged.
/// </remarks>
[DataField]
public EntityUid? OriginalEntity;
/// <summary>
/// The player session of the original entity, if any.
/// Used for admin logging purposes.
/// </summary>
[ViewVariables]
public ICommonSession? OriginalSession;
}

View File

@@ -0,0 +1,133 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Changeling.Devour;
/// <summary>
/// Component responsible for Changelings Devour attack. Including the amount of damage
/// and how long it takes to devour someone
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(ChangelingDevourSystem))]
public sealed partial class ChangelingDevourComponent : Component
{
/// <summary>
/// The Action for devouring
/// </summary>
[DataField]
public EntProtoId? ChangelingDevourAction = "ActionChangelingDevour";
/// <summary>
/// The action entity associated with devouring
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? ChangelingDevourActionEntity;
/// <summary>
/// The whitelist of targets for devouring
/// </summary>
[DataField, AutoNetworkedField]
public EntityWhitelist? Whitelist = new()
{
Components =
[
"MobState",
"HumanoidAppearance",
],
};
/// <summary>
/// The Sound to use during consumption of a victim
/// </summary>
/// <remarks>
/// 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy.
/// 6 still allows the sound to be hearable, but not across an entire department.
/// </remarks>
[DataField, AutoNetworkedField]
public SoundSpecifier? ConsumeNoise = new SoundCollectionSpecifier("ChangelingDevourConsume", AudioParams.Default.WithMaxDistance(6));
/// <summary>
/// The Sound to use during the windup before consuming a victim
/// </summary>
/// <remarks>
/// 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy.
/// 6 still allows the sound to be hearable, but not across an entire department.
/// </remarks>
[DataField, AutoNetworkedField]
public SoundSpecifier? DevourWindupNoise = new SoundCollectionSpecifier("ChangelingDevourWindup", AudioParams.Default.WithMaxDistance(6));
/// <summary>
/// The time between damage ticks
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan DamageTimeBetweenTicks = TimeSpan.FromSeconds(1);
/// <summary>
/// The windup time before the changeling begins to engage in devouring the identity of a target
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan DevourWindupTime = TimeSpan.FromSeconds(2);
/// <summary>
/// The time it takes to FULLY consume someones identity.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan DevourConsumeTime = TimeSpan.FromSeconds(10);
/// <summary>
/// Damage cap that a target is allowed to be caused due to IdentityConsumption
/// </summary>
[DataField, AutoNetworkedField]
public float DevourConsumeDamageCap = 350f;
/// <summary>
/// The Currently active devour sound in the world
/// </summary>
[DataField]
public EntityUid? CurrentDevourSound;
/// <summary>
/// The damage profile for a single tick of devour damage
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier DamagePerTick = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Slash", 10},
{ "Piercing", 10 },
{ "Blunt", 5 },
},
};
/// <summary>
/// The list of protective damage types capable of preventing a devour if over the threshold
/// </summary>
[DataField, AutoNetworkedField]
public List<ProtoId<DamageTypePrototype>> ProtectiveDamageTypes = new()
{
"Slash",
"Piercing",
"Blunt",
};
/// <summary>
/// The next Tick to deal damage on (utilized during the consumption "do-during" (a do after with an attempt event))
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
public TimeSpan NextTick = TimeSpan.Zero;
/// <summary>
/// The percentage of ANY brute damage resistance that will prevent devouring
/// </summary>
[DataField, AutoNetworkedField]
public float DevourPreventionPercentageThreshold = 0.1f;
public override bool SendOnlyToOwner => true;
}

View File

@@ -0,0 +1,22 @@
using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling.Devour;
/// <summary>
/// Action event for Devour, someone has initiated a devour on someone, begin to windup.
/// </summary>
public sealed partial class ChangelingDevourActionEvent : EntityTargetActionEvent;
/// <summary>
/// A windup has either successfully been completed or has been canceled. If successful start the devouring DoAfter.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class ChangelingDevourWindupDoAfterEvent : SimpleDoAfterEvent;
/// <summary>
/// The Consumption DoAfter has either successfully been completed or was canceled.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class ChangelingDevourConsumeDoAfterEvent : SimpleDoAfterEvent;

View File

@@ -0,0 +1,276 @@
using Content.Shared.Actions;
using Content.Shared.Administration.Logs;
using Content.Shared.Armor;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Body.Components;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Inventory;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Storage;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Shared.Changeling.Devour;
public sealed class ChangelingDevourSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly ChangelingIdentitySystem _changelingIdentitySystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChangelingDevourComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourActionEvent>(OnDevourAction);
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourWindupDoAfterEvent>(OnDevourWindup);
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourConsumeDoAfterEvent>(OnDevourConsume);
SubscribeLocalEvent<ChangelingDevourComponent, DoAfterAttemptEvent<ChangelingDevourConsumeDoAfterEvent>>(OnConsumeAttemptTick);
SubscribeLocalEvent<ChangelingDevourComponent, ComponentShutdown>(OnShutdown);
}
private void OnMapInit(Entity<ChangelingDevourComponent> ent, ref MapInitEvent args)
{
_actionsSystem.AddAction(ent, ref ent.Comp.ChangelingDevourActionEntity, ent.Comp.ChangelingDevourAction);
}
private void OnShutdown(Entity<ChangelingDevourComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.ChangelingDevourActionEntity != null)
{
_actionsSystem.RemoveAction(ent.Owner, ent.Comp.ChangelingDevourActionEntity);
}
}
//TODO: Allow doafters to have proper update loop support. Attempt events should not be doing state changes.
private void OnConsumeAttemptTick(Entity<ChangelingDevourComponent> ent,
ref DoAfterAttemptEvent<ChangelingDevourConsumeDoAfterEvent> eventData)
{
var curTime = _timing.CurTime;
if (curTime < ent.Comp.NextTick)
return;
ConsumeDamageTick(eventData.Event.Target, ent.Comp, eventData.Event.User);
ent.Comp.NextTick += ent.Comp.DamageTimeBetweenTicks;
Dirty(ent, ent.Comp);
}
private void ConsumeDamageTick(EntityUid? target, ChangelingDevourComponent comp, EntityUid? user)
{
if (target == null)
return;
if (!TryComp<DamageableComponent>(target, out var damage))
return;
foreach (var damagePoints in comp.DamagePerTick.DamageDict)
{
if (damage.Damage.DamageDict.TryGetValue(damagePoints.Key, out var val) && val > comp.DevourConsumeDamageCap)
return;
}
_damageable.TryChangeDamage(target, comp.DamagePerTick, true, true, damage, user);
}
/// <summary>
/// Checkes if the targets outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured.
/// </summary>
/// <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)
{
var ev = new CoefficientQueryEvent(SlotFlags.OUTERCLOTHING);
RaiseLocalEvent(target, ev);
foreach (var compProtectiveDamageType in ent.Comp.ProtectiveDamageTypes)
{
if (!ev.DamageModifiers.Coefficients.TryGetValue(compProtectiveDamageType, out var coefficient))
continue;
if (coefficient < 1f - ent.Comp.DevourPreventionPercentageThreshold)
return true;
}
return false;
}
private void OnDevourAction(Entity<ChangelingDevourComponent> ent, ref ChangelingDevourActionEvent args)
{
if (args.Handled || _whitelistSystem.IsWhitelistFailOrNull(ent.Comp.Whitelist, args.Target)
|| !HasComp<ChangelingIdentityComponent>(ent))
return;
args.Handled = true;
var target = args.Target;
if (target == ent.Owner)
return; // don't eat yourself
if (HasComp<RottingComponent>(target))
{
_popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-rotting"), args.Performer, args.Performer, PopupType.Medium);
return;
}
if (IsTargetProtected(target, ent))
{
_popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-protected"), ent, ent, PopupType.Medium);
return;
}
if (_net.IsServer)
{
var pvsSound = _audio.PlayPvs(ent.Comp.DevourWindupNoise, ent);
if (pvsSound != null)
ent.Comp.CurrentDevourSound = pvsSound.Value.Entity;
}
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ent:player} started changeling devour windup against {target:player}");
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, ent.Comp.DevourWindupTime, new ChangelingDevourWindupDoAfterEvent(), ent, target: target, used: ent)
{
BreakOnMove = true,
BlockDuplicate = true,
DuplicateCondition = DuplicateConditions.None,
});
var selfMessage = Loc.GetString("changeling-devour-begin-windup-self", ("user", Identity.Entity(ent.Owner, EntityManager)));
var othersMessage = Loc.GetString("changeling-devour-begin-windup-others", ("user", Identity.Entity(ent.Owner, EntityManager)));
_popupSystem.PopupPredicted(
selfMessage,
othersMessage,
args.Performer,
args.Performer,
PopupType.MediumCaution);
}
private void OnDevourWindup(Entity<ChangelingDevourComponent> ent, ref ChangelingDevourWindupDoAfterEvent args)
{
var curTime = _timing.CurTime;
args.Handled = true;
if (!EntityManager.EntityExists(ent.Comp.CurrentDevourSound))
_audio.Stop(ent.Comp.CurrentDevourSound!);
if (args.Cancelled)
return;
var selfMessage = Loc.GetString("changeling-devour-begin-consume-self", ("user", Identity.Entity(ent.Owner, EntityManager)));
var othersMessage = Loc.GetString("changeling-devour-begin-consume-others", ("user", Identity.Entity(ent.Owner, EntityManager)));
_popupSystem.PopupPredicted(
selfMessage,
othersMessage,
args.User,
args.User,
PopupType.LargeCaution);
if (_net.IsServer)
{
var pvsSound = _audio.PlayPvs(ent.Comp.ConsumeNoise, ent);
if (pvsSound != null)
ent.Comp.CurrentDevourSound = pvsSound.Value.Entity;
}
ent.Comp.NextTick = curTime + ent.Comp.DamageTimeBetweenTicks;
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} began to devour {ToPrettyString(args.Target):player} identity");
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
ent,
ent.Comp.DevourConsumeTime,
new ChangelingDevourConsumeDoAfterEvent(),
ent,
target: args.Target,
used: ent)
{
AttemptFrequency = AttemptFrequency.EveryTick,
BreakOnMove = true,
BlockDuplicate = true,
DuplicateCondition = DuplicateConditions.None,
});
}
private void OnDevourConsume(Entity<ChangelingDevourComponent> ent, ref ChangelingDevourConsumeDoAfterEvent args)
{
args.Handled = true;
var target = args.Target;
if (target == null)
return;
if (EntityManager.EntityExists(ent.Comp.CurrentDevourSound))
_audio.Stop(ent.Comp.CurrentDevourSound!);
if (args.Cancelled)
return;
if (!_mobState.IsDead((EntityUid)target))
{
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} unsuccessfully devoured {ToPrettyString(args.Target):player}'s identity");
_popupSystem.PopupClient(Loc.GetString("changeling-devour-consume-failed-not-dead"), args.User, args.User, PopupType.Medium);
return;
}
var selfMessage = Loc.GetString("changeling-devour-consume-complete-self", ("user", Identity.Entity(args.User, EntityManager)));
var othersMessage = Loc.GetString("changeling-devour-consume-complete-others", ("user", Identity.Entity(args.User, EntityManager)));
_popupSystem.PopupPredicted(
selfMessage,
othersMessage,
args.User,
args.User,
PopupType.LargeCaution);
if (_mobState.IsDead(target.Value)
&& TryComp<BodyComponent>(target, out var body)
&& HasComp<HumanoidAppearanceComponent>(target)
&& TryComp<ChangelingIdentityComponent>(args.User, out var identityStorage))
{
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(args.Target):player}'s identity");
_changelingIdentitySystem.CloneToPausedMap((ent, identityStorage), target.Value);
if (_inventorySystem.TryGetSlotEntity(target.Value, "jumpsuit", out var item)
&& TryComp<ButcherableComponent>(item, out var butcherable))
RipClothing(target.Value, (item.Value, butcherable));
}
Dirty(ent);
}
private void RipClothing(EntityUid victim, Entity<ButcherableComponent> item)
{
var spawnEntities = EntitySpawnCollection.GetSpawns(item.Comp.SpawnedEntities, _robustRandom);
foreach (var proto in spawnEntities)
{
// TODO: once predictedRandom is in, make this a Coordinate offset of 0.25f from the victims position
PredictedSpawnNextToOrDrop(proto, victim);
}
PredictedQueueDel(item.Owner);
}
}

View File

@@ -0,0 +1,54 @@
using Content.Shared.Cloning;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Changeling.Transform;
/// <summary>
/// The component containing information about Changelings Transformation action
/// Like how long their windup is, the sounds as well as the Target Cloning settings for changing between identities
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(ChangelingTransformSystem))]
public sealed partial class ChangelingTransformComponent : Component
{
/// <summary>
/// The action Prototype for Transforming
/// </summary>
[DataField]
public EntProtoId? ChangelingTransformAction = "ActionChangelingTransform";
/// <summary>
/// The Action Entity for transforming associated with this Component
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? ChangelingTransformActionEntity;
/// <summary>
/// Time it takes to Transform
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan TransformWindup = TimeSpan.FromSeconds(5);
/// <summary>
/// The noise used when attempting to transform
/// </summary>
[DataField, AutoNetworkedField]
public SoundSpecifier? TransformAttemptNoise = new SoundCollectionSpecifier("ChangelingTransformAttempt", AudioParams.Default.WithMaxDistance(6)); // 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy. 6 still allows the sound to be hearable, but not across an entire department.
/// <summary>
/// The currently active transform in the world
/// </summary>
[DataField]
public EntityUid? CurrentTransformSound;
/// <summary>
/// The cloning settings passed to the CloningSystem, contains a list of all components to copy or have handled by their
/// respective systems.
/// </summary>
public ProtoId<CloningSettingsPrototype> TransformCloningSettings = "ChangelingCloningSettings";
public override bool SendOnlyToOwner => true;
}

View File

@@ -0,0 +1,16 @@
using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling.Transform;
/// <summary>
/// Action event for opening the changeling transformation radial menu.
/// </summary>
public sealed partial class ChangelingTransformActionEvent : InstantActionEvent;
/// <summary>
/// DoAfterevent used to transform a changeling into one of their stored identities.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class ChangelingTransformDoAfterEvent : SimpleDoAfterEvent;

View File

@@ -0,0 +1,33 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling.Transform;
/// <summary>
/// Send when a player selects an intentity to transform into in the radial menu.
/// </summary>
[Serializable, NetSerializable]
public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage
{
/// <summary>
/// The uid of the cloned identity.
/// </summary>
public readonly NetEntity TargetIdentity = targetIdentity;
}
// TODO: Replace with component states.
// We are already networking the ChangelingIdentityComponent, which contains all this information,
// so we can just read it from them from the component and update the UI in an AfterAuotHandleState subscription.
[Serializable, NetSerializable]
public sealed class ChangelingTransformBoundUserInterfaceState(List<NetEntity> identities) : BoundUserInterfaceState
{
/// <summary>
/// The uids of the cloned identities.
/// </summary>
public readonly List<NetEntity> Identites = identities;
}
[Serializable, NetSerializable]
public enum TransformUI : byte
{
Key,
}

View File

@@ -0,0 +1,180 @@
using Content.Shared.Actions;
using Content.Shared.Administration.Logs;
using Content.Shared.Cloning;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Popups;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Shared.Changeling.Transform;
public sealed partial class ChangelingTransformSystem : EntitySystem
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearanceSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedCloningSystem _cloningSystem = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private const string ChangelingBuiXmlGeneratedName = "ChangelingTransformBoundUserInterface";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChangelingTransformComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformActionEvent>(OnTransformAction);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformDoAfterEvent>(OnSuccessfulTransform);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformIdentitySelectMessage>(OnTransformSelected);
SubscribeLocalEvent<ChangelingTransformComponent, ComponentShutdown>(OnShutdown);
}
private void OnMapInit(Entity<ChangelingTransformComponent> ent, ref MapInitEvent init)
{
_actionsSystem.AddAction(ent, ref ent.Comp.ChangelingTransformActionEntity, ent.Comp.ChangelingTransformAction);
var userInterfaceComp = EnsureComp<UserInterfaceComponent>(ent);
_uiSystem.SetUi((ent, userInterfaceComp), TransformUI.Key, new InterfaceData(ChangelingBuiXmlGeneratedName));
}
private void OnShutdown(Entity<ChangelingTransformComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.ChangelingTransformActionEntity != null)
{
_actionsSystem.RemoveAction(ent.Owner, ent.Comp.ChangelingTransformActionEntity);
}
}
private void OnTransformAction(Entity<ChangelingTransformComponent> ent,
ref ChangelingTransformActionEvent args)
{
if (!TryComp<UserInterfaceComponent>(ent, out var userInterfaceComp))
return;
if (!TryComp<ChangelingIdentityComponent>(ent, out var userIdentity))
return;
if (!_uiSystem.IsUiOpen((ent, userInterfaceComp), TransformUI.Key, args.Performer))
{
_uiSystem.OpenUi((ent, userInterfaceComp), TransformUI.Key, args.Performer);
var identityData = new List<NetEntity>();
foreach (var consumedIdentity in userIdentity.ConsumedIdentities)
{
identityData.Add(GetNetEntity(consumedIdentity));
}
_uiSystem.SetUiState((ent, userInterfaceComp), TransformUI.Key, new ChangelingTransformBoundUserInterfaceState(identityData));
} //TODO: Can add a Else here with TransformInto and CloseUI to make a quick switch,
// issue right now is that Radials cover the Action buttons so clicking the action closes the UI (due to clicking off a radial causing it to close, even with UI)
// but pressing the number does.
}
/// <summary>
/// Transform the changeling into another identity.
/// This can be any cloneable humanoid and doesn't have to be stored in the ChangelingIdentiyComponent,
/// so make sure to validate the target before.
/// </summary>
public void TransformInto(Entity<ChangelingTransformComponent?> ent, EntityUid targetIdentity)
{
if (!Resolve(ent, ref ent.Comp))
return;
var selfMessage = Loc.GetString("changeling-transform-attempt-self", ("user", Identity.Entity(ent.Owner, EntityManager)));
var othersMessage = Loc.GetString("changeling-transform-attempt-others", ("user", Identity.Entity(ent.Owner, EntityManager)));
_popupSystem.PopupPredicted(
selfMessage,
othersMessage,
ent,
ent,
PopupType.MediumCaution);
if (_net.IsServer)
ent.Comp.CurrentTransformSound = _audio.PlayPvs(ent.Comp.TransformAttemptNoise, ent)?.Entity;
if(TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player}) ");
else
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\"");
var result = _doAfterSystem.TryStartDoAfter(new DoAfterArgs(
EntityManager,
ent,
ent.Comp.TransformWindup,
new ChangelingTransformDoAfterEvent(),
ent,
target: targetIdentity)
{
BreakOnMove = true,
BreakOnWeightlessMove = true,
DuplicateCondition = DuplicateConditions.None,
RequireCanInteract = false,
DistanceThreshold = null,
});
}
private void OnTransformSelected(Entity<ChangelingTransformComponent> ent,
ref ChangelingTransformIdentitySelectMessage args)
{
_uiSystem.CloseUi(ent.Owner, TransformUI.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.Contains(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)
{
args.Handled = true;
if (EntityManager.EntityExists(ent.Comp.CurrentTransformSound))
_audio.Stop(ent.Comp.CurrentTransformSound);
if (args.Cancelled)
return;
if (!_prototype.Resolve(ent.Comp.TransformCloningSettings, out var settings))
return;
if (args.Target is not { } targetIdentity)
return;
_humanoidAppearanceSystem.CloneAppearance(targetIdentity, args.User);
_cloningSystem.CloneComponents(targetIdentity, args.User, settings);
if(TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player})");
else
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\"");
_metaSystem.SetEntityName(ent, Name(targetIdentity), raiseEvents: false);
Dirty(ent);
if (TryComp<ChangelingIdentityComponent>(ent, out var identity)) // in case we ever get changelings that don't store identities
{
identity.CurrentIdentity = targetIdentity;
Dirty(ent.Owner, identity);
}
}
}

View File

@@ -0,0 +1,14 @@
namespace Content.Shared.Cloning;
public abstract partial class SharedCloningSystem : EntitySystem
{
/// <summary>
/// Copy components from one entity to another based on a CloningSettingsPrototype.
/// </summary>
/// <param name="original">The orignal Entity to clone components from.</param>
/// <param name="clone">The target Entity to clone components to.</param>
/// <param name="settings">The clone settings prototype containing the list of components to clone.</param>
public virtual void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings)
{
}
}

View File

@@ -11,6 +11,7 @@ using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects.Components.Localization;
using Robust.Shared.Network;
using Robust.Shared.Player;
@@ -152,16 +153,12 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
targetHumanoid.SkinColor = sourceHumanoid.SkinColor;
targetHumanoid.EyeColor = sourceHumanoid.EyeColor;
targetHumanoid.Age = sourceHumanoid.Age;
SetSex(target, sourceHumanoid.Sex, false, targetHumanoid);
targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers);
targetHumanoid.MarkingSet = new(sourceHumanoid.MarkingSet);
targetHumanoid.Gender = sourceHumanoid.Gender;
SetSex(target, sourceHumanoid.Sex, false, targetHumanoid);
SetGender((target, targetHumanoid), sourceHumanoid.Gender);
if (TryComp<GrammarComponent>(target, out var grammar))
_grammarSystem.SetGender((target, grammar), sourceHumanoid.Gender);
_identity.QueueIdentityUpdate(target);
Dirty(target, targetHumanoid);
}
@@ -264,6 +261,23 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
Dirty(uid, humanoid);
}
/// <summary>
/// Sets the gender in the entity's HumanoidAppearanceComponent and GrammarComponent.
/// </summary>
public void SetGender(Entity<HumanoidAppearanceComponent?> ent, Gender gender)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.Gender = gender;
Dirty(ent);
if (TryComp<GrammarComponent>(ent, out var grammar))
_grammarSystem.SetGender((ent, grammar), gender);
_identity.QueueIdentityUpdate(ent);
}
/// <summary>
/// Sets the skin color of this humanoid mob. Will only affect base layers that are not custom,
/// custom base layers should use <see cref="SetBaseLayerColor"/> instead.

View File

@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.DisplacementMap;
using Content.Shared.Inventory.Events;
using Content.Shared.Storage;
using Robust.Shared.Containers;
@@ -55,6 +56,24 @@ public partial class InventorySystem : EntitySystem
return false;
}
/// <summary>
/// Copy this component's datafields from one entity to another.
/// This can't use CopyComp because the template needs to be applied using the API method.
/// <summary>
public void CopyComponent(Entity<InventoryComponent?> source, EntityUid target)
{
if (!Resolve(source, ref source.Comp))
return;
var targetComp = EnsureComp<InventoryComponent>(target);
targetComp.SpeciesId = source.Comp.SpeciesId;
targetComp.Displacements = new Dictionary<string, DisplacementData>(source.Comp.Displacements);
targetComp.FemaleDisplacements = new Dictionary<string, DisplacementData>(source.Comp.FemaleDisplacements);
targetComp.MaleDisplacements = new Dictionary<string, DisplacementData>(source.Comp.MaleDisplacements);
SetTemplateId((target, targetComp), source.Comp.TemplateId);
Dirty(target, targetComp);
}
private void OnInit(Entity<InventoryComponent> ent, ref ComponentInit args)
{
UpdateInventoryTemplate(ent);

View File

@@ -54,6 +54,21 @@ namespace Content.Shared.Movement.Systems
RefreshMovementSpeedModifiers(entity);
}
/// <summary>
/// Copy this component's datafields from one entity to another.
/// This needs to refresh the modifiers after using CopyComp.
/// <summary>
public void CopyComponent(Entity<MovementSpeedModifierComponent?> source, EntityUid target)
{
if (!Resolve(source, ref source.Comp))
return;
CopyComp(source, target, source.Comp);
RefreshWeightlessModifiers(target);
RefreshMovementSpeedModifiers(target);
RefreshFrictionModifiers(target);
}
public void RefreshWeightlessModifiers(EntityUid uid, MovementSpeedModifierComponent? move = null)
{
if (!Resolve(uid, ref move, false))

View File

@@ -1,6 +1,7 @@
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Alert;
using Content.Shared.Cloning.Events;
using Content.Shared.Coordinates;
using Content.Shared.Fluids.Components;
using Content.Shared.Gravity;
@@ -50,6 +51,20 @@ public abstract class SharedRootableSystem : EntitySystem
SubscribeLocalEvent<RootableComponent, IsWeightlessEvent>(OnIsWeightless);
SubscribeLocalEvent<RootableComponent, SlipAttemptEvent>(OnSlipAttempt);
SubscribeLocalEvent<RootableComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeed);
SubscribeLocalEvent<RootableComponent, CloningEvent>(OnCloning);
}
private void OnCloning(Entity<RootableComponent> ent, ref CloningEvent args)
{
if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
return;
var cloneComp = EnsureComp<RootableComponent>(args.CloneUid);
cloneComp.TransferRate = ent.Comp.TransferRate;
cloneComp.TransferFrequency = ent.Comp.TransferFrequency;
cloneComp.SpeedModifier = ent.Comp.SpeedModifier;
cloneComp.RootSound = ent.Comp.RootSound;
Dirty(args.CloneUid, cloneComp);
}
private void OnRootableMapInit(Entity<RootableComponent> entity, ref MapInitEvent args)
@@ -68,6 +83,7 @@ public abstract class SharedRootableSystem : EntitySystem
var actions = new Entity<ActionsComponent?>(entity, comp);
_actions.RemoveAction(actions, entity.Comp.ActionEntity);
_alerts.ClearAlert(entity, entity.Comp.RootedAlert);
}
private void OnRootableToggle(Entity<RootableComponent> entity, ref ToggleActionEvent args)

View File

@@ -38,7 +38,7 @@ public abstract partial class SharedSericultureSystem : EntitySystem
private void OnClone(Entity<SericultureComponent> ent, ref CloningEvent args)
{
if(!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
return;
var comp = EnsureComp<SericultureComponent>(args.CloneUid);

View File

@@ -16,29 +16,26 @@ namespace Content.Shared.Speech
[Access(typeof(SpeechSystem), Friend = AccessPermissions.ReadWrite, Other = AccessPermissions.Read)]
public bool Enabled = true;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
[DataField, AutoNetworkedField]
public ProtoId<SpeechSoundsPrototype>? SpeechSounds;
/// <summary>
/// What speech verb prototype should be used by default for displaying this entity's messages?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
[DataField, AutoNetworkedField]
public ProtoId<SpeechVerbPrototype> SpeechVerb = "Default";
/// <summary>
/// What emotes allowed to use event if emote <see cref="EmotePrototype.Available"/> is false
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
[DataField, AutoNetworkedField]
public List<ProtoId<EmotePrototype>> AllowedEmotes = new();
/// <summary>
/// A mapping from chat suffixes loc strings to speech verb prototypes that should be conditionally used.
/// For things like '?' changing to 'asks' or '!!' making text bold and changing to 'yells'. Can be overridden if necessary.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public Dictionary<string, ProtoId<SpeechVerbPrototype>> SuffixSpeechVerbs = new()
{
{ "chat-speech-verb-suffix-exclamation-strong", "DefaultExclamationStrong" },
@@ -51,7 +48,6 @@ namespace Content.Shared.Speech
[DataField]
public AudioParams AudioParams = AudioParams.Default.WithVolume(-2f).WithRolloffFactor(4.5f);
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float SoundCooldownTime { get; set; } = 0.5f;

View File

@@ -232,7 +232,14 @@ public abstract class SharedStorageSystem : EntitySystem
StoredItems = storedItems,
SavedLocations = component.SavedLocations,
Whitelist = component.Whitelist,
Blacklist = component.Blacklist
Blacklist = component.Blacklist,
QuickInsert = component.QuickInsert,
AreaInsert = component.AreaInsert,
StorageInsertSound = component.StorageInsertSound,
StorageRemoveSound = component.StorageRemoveSound,
StorageOpenSound = component.StorageOpenSound,
StorageCloseSound = component.StorageCloseSound,
DefaultStorageOrientation = component.DefaultStorageOrientation,
};
}
@@ -348,6 +355,44 @@ public abstract class SharedStorageSystem : EntitySystem
args.Verbs.Add(verb);
}
/// <summary>
/// Copy this component's datafields from one entity to another.
/// This can't use CopyComp because we don't want to copy the references to the items inside the storage.
/// <summary>
public void CopyComponent(Entity<StorageComponent?> source, EntityUid target)
{
if (!Resolve(source, ref source.Comp))
return;
var targetComp = EnsureComp<StorageComponent>(target);
targetComp.Grid = new List<Box2i>(source.Comp.Grid);
targetComp.MaxItemSize = source.Comp.MaxItemSize;
targetComp.QuickInsert = source.Comp.QuickInsert;
targetComp.QuickInsertCooldown = source.Comp.QuickInsertCooldown;
targetComp.OpenUiCooldown = source.Comp.OpenUiCooldown;
targetComp.ClickInsert = source.Comp.ClickInsert;
targetComp.OpenOnActivate = source.Comp.OpenOnActivate;
targetComp.AreaInsert = source.Comp.AreaInsert;
targetComp.AreaInsertRadius = source.Comp.AreaInsertRadius;
targetComp.Whitelist = source.Comp.Whitelist;
targetComp.Blacklist = source.Comp.Blacklist;
targetComp.StorageInsertSound = source.Comp.StorageInsertSound;
targetComp.StorageRemoveSound = source.Comp.StorageRemoveSound;
targetComp.StorageOpenSound = source.Comp.StorageOpenSound;
targetComp.StorageCloseSound = source.Comp.StorageCloseSound;
targetComp.DefaultStorageOrientation = source.Comp.DefaultStorageOrientation;
targetComp.HideStackVisualsWhenClosed = source.Comp.HideStackVisualsWhenClosed;
targetComp.SilentStorageUserTag = source.Comp.SilentStorageUserTag;
targetComp.ShowVerb = source.Comp.ShowVerb;
UpdateOccupied((target, targetComp));
Dirty(target, targetComp);
var targetUI = EnsureComp<UserInterfaceComponent>(target);
UI.SetUi((target, targetUI), StorageComponent.StorageUiKey.Key, new InterfaceData("StorageBoundUserInterface"));
}
/// <summary>
/// Tries to get the storage location of an item.
/// </summary>
@@ -1957,15 +2002,17 @@ public abstract class SharedStorageSystem : EntitySystem
protected sealed class StorageComponentState : ComponentState
{
public Dictionary<NetEntity, ItemStorageLocation> StoredItems = new();
public Dictionary<string, List<ItemStorageLocation>> SavedLocations = new();
public List<Box2i> Grid = new();
public ProtoId<ItemSizePrototype>? MaxItemSize;
public EntityWhitelist? Whitelist;
public EntityWhitelist? Blacklist;
public bool QuickInsert;
public bool AreaInsert;
public SoundSpecifier? StorageInsertSound;
public SoundSpecifier? StorageRemoveSound;
public SoundSpecifier? StorageOpenSound;
public SoundSpecifier? StorageCloseSound;
public StorageDefaultOrientation? DefaultStorageOrientation;
}
}