]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Changeling devour and transform (#34002)
authorpoklj <compgeek223@gmail.com>
Wed, 6 Aug 2025 19:55:49 +0000 (16:55 -0300)
committerGitHub <noreply@github.com>
Wed, 6 Aug 2025 19:55:49 +0000 (21:55 +0200)
* 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>
52 files changed:
Content.Client/Changeling/Transform/ChangelingTransformBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml [new file with mode: 0644]
Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml.cs [new file with mode: 0644]
Content.Client/Cloning/CloningSystem.cs [new file with mode: 0644]
Content.Client/Storage/Systems/StorageSystem.cs
Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
Content.Server/Cloning/CloningSystem.Subscriptions.cs
Content.Server/Cloning/CloningSystem.cs
Content.Server/Emoting/Components/BodyEmotesComponent.cs
Content.Server/Emoting/Systems/BodyEmotesSystem.cs
Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs
Content.Server/Speech/EntitySystems/VocalSystem.cs
Content.Server/Wagging/WaggingSystem.cs
Content.Shared/Changeling/ChangelingIdentityComponent.cs [new file with mode: 0644]
Content.Shared/Changeling/ChangelingIdentitySystem.cs [new file with mode: 0644]
Content.Shared/Changeling/ChangelingRoleComponent.cs [new file with mode: 0644]
Content.Shared/Changeling/ChangelingStoredIdentityComponent.cs [new file with mode: 0644]
Content.Shared/Changeling/Devour/ChangelingDevourComponent.cs [new file with mode: 0644]
Content.Shared/Changeling/Devour/ChangelingDevourSystem.Events.cs [new file with mode: 0644]
Content.Shared/Changeling/Devour/ChangelingDevourSystem.cs [new file with mode: 0644]
Content.Shared/Changeling/Transform/ChangelingTransformComponent.cs [new file with mode: 0644]
Content.Shared/Changeling/Transform/ChangelingTransformSystem.Events.cs [new file with mode: 0644]
Content.Shared/Changeling/Transform/ChangelingTransformSystem.UI.cs [new file with mode: 0644]
Content.Shared/Changeling/Transform/ChangelingTransformSystem.cs [new file with mode: 0644]
Content.Shared/Cloning/SharedCloningSystem.cs [new file with mode: 0644]
Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
Content.Shared/Inventory/InventorySystem.Slots.cs
Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs
Content.Shared/Rootable/SharedRootableSystem.cs
Content.Shared/Sericulture/SericultureSystem.cs
Content.Shared/Speech/SpeechComponent.cs
Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs
Resources/Audio/Effects/Changeling/attributions.yml [new file with mode: 0644]
Resources/Audio/Effects/Changeling/changeling_transform.ogg [new file with mode: 0644]
Resources/Audio/Effects/Changeling/devour_consume.ogg [new file with mode: 0644]
Resources/Audio/Effects/Changeling/devour_suck.ogg [new file with mode: 0644]
Resources/Audio/Effects/Changeling/devour_windup.ogg [new file with mode: 0644]
Resources/Locale/en-US/administration/antag.ftl
Resources/Locale/en-US/changeling/changeling.ftl [new file with mode: 0644]
Resources/Locale/en-US/mind/role-types.ftl
Resources/Prototypes/Antag/changeling.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Mobs/Player/clone.yml
Resources/Prototypes/GameRules/roundstart.yml
Resources/Prototypes/Roles/Antags/changeling.yml [new file with mode: 0644]
Resources/Prototypes/Roles/MindRoles/mind_roles.yml
Resources/Prototypes/SoundCollections/changeling.yml [new file with mode: 0644]
Resources/Textures/Interface/Actions/changeling.rsi/devour.png [new file with mode: 0644]
Resources/Textures/Interface/Actions/changeling.rsi/devour_on.png [new file with mode: 0644]
Resources/Textures/Interface/Actions/changeling.rsi/meta.json
Resources/Textures/Interface/Actions/changeling.rsi/transform.png [new file with mode: 0644]

diff --git a/Content.Client/Changeling/Transform/ChangelingTransformBoundUserInterface.cs b/Content.Client/Changeling/Transform/ChangelingTransformBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..8e383bc
--- /dev/null
@@ -0,0 +1,35 @@
+using Content.Shared.Changeling.Transform;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Changeling.Transform;
+
+[UsedImplicitly]
+public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+    private ChangelingTransformMenu? _window;
+
+    protected override void Open()
+    {
+        base.Open();
+
+        _window = this.CreateWindow<ChangelingTransformMenu>();
+
+        _window.OnIdentitySelect += SendIdentitySelect;
+    }
+
+    protected override void UpdateState(BoundUserInterfaceState state)
+    {
+        base.UpdateState(state);
+
+        if (state is not ChangelingTransformBoundUserInterfaceState current)
+            return;
+
+        _window?.UpdateState(current);
+    }
+
+    public void SendIdentitySelect(NetEntity identityId)
+    {
+        SendPredictedMessage(new ChangelingTransformIdentitySelectMessage(identityId));
+    }
+}
diff --git a/Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml b/Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml
new file mode 100644 (file)
index 0000000..38ae0ec
--- /dev/null
@@ -0,0 +1,8 @@
+<ui:RadialMenu
+    xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
+    CloseButtonStyleClass="RadialMenuCloseButton"
+    VerticalExpand="True"
+    HorizontalExpand="True">
+    <ui:RadialContainer Name="Main">
+    </ui:RadialContainer>
+</ui:RadialMenu>
diff --git a/Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml.cs b/Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml.cs
new file mode 100644 (file)
index 0000000..beef9ae
--- /dev/null
@@ -0,0 +1,60 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Changeling.Transform;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Changeling.Transform;
+
+[GenerateTypedNameReferences]
+public sealed partial class ChangelingTransformMenu : RadialMenu
+{
+    [Dependency] private readonly IEntityManager _entity = default!;
+    public event Action<NetEntity>? OnIdentitySelect;
+
+    public ChangelingTransformMenu()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+    }
+
+    public void UpdateState(ChangelingTransformBoundUserInterfaceState state)
+    {
+        Main.DisposeAllChildren();
+        foreach (var identity in state.Identites)
+        {
+            var identityUid = _entity.GetEntity(identity);
+
+            if (!_entity.TryGetComponent<MetaDataComponent>(identityUid, out var metadata))
+                continue;
+
+            var identityName = metadata.EntityName;
+
+            var button = new ChangelingTransformMenuButton()
+            {
+                StyleClasses = { "RadialMenuButton" },
+                SetSize = new Vector2(64, 64),
+                ToolTip = identityName,
+            };
+
+            var entView = new SpriteView()
+            {
+                SetSize = new Vector2(48, 48),
+                VerticalAlignment = VAlignment.Center,
+                HorizontalAlignment = HAlignment.Center,
+                Stretch = SpriteView.StretchMode.Fill,
+            };
+            entView.SetEntity(identityUid);
+            button.OnButtonUp += _ =>
+            {
+                OnIdentitySelect?.Invoke(identity);
+                Close();
+            };
+            button.AddChild(entView);
+            Main.AddChild(button);
+        }
+    }
+}
+
+public sealed class ChangelingTransformMenuButton : RadialMenuTextureButtonWithSector;
diff --git a/Content.Client/Cloning/CloningSystem.cs b/Content.Client/Cloning/CloningSystem.cs
new file mode 100644 (file)
index 0000000..9bfa230
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Cloning;
+
+namespace Content.Client.Cloning;
+
+public sealed partial class CloningSystem : SharedCloningSystem;
index bd6659de01c4d6b4bd87bc9c0db6c2d0128e059b..1a21de4b999d35a7da369a40ac591dc38e0d521d 100644 (file)
@@ -40,6 +40,11 @@ public sealed class StorageSystem : SharedStorageSystem
         component.MaxItemSize = state.MaxItemSize;
         component.Whitelist = state.Whitelist;
         component.Blacklist = state.Blacklist;
+        component.StorageInsertSound = state.StorageInsertSound;
+        component.StorageRemoveSound = state.StorageRemoveSound;
+        component.StorageOpenSound = state.StorageOpenSound;
+        component.StorageCloseSound = state.StorageCloseSound;
+        component.DefaultStorageOrientation = state.DefaultStorageOrientation;
 
         _oldStoredItems.Clear();
 
index 672ae695bf8a5bc4025822185943a032b9fe62b2..2b5ea90b1295f3d97426a986de72e4010f3ee3c5 100644 (file)
@@ -27,9 +27,9 @@ public sealed partial class AdminVerbSystem
     private static readonly EntProtoId DefaultNukeOpRule = "LoneOpsSpawn";
     private static readonly EntProtoId DefaultRevsRule = "Revolutionary";
     private static readonly EntProtoId DefaultThiefRule = "Thief";
-    private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
-
+    private static readonly EntProtoId DefaultChangelingRule = "Changeling";
     private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
+    private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
 
     // All antag verbs have names so invokeverb works.
     private void AddAntagVerbs(GetVerbsEvent<Verb> args)
@@ -58,7 +58,7 @@ public sealed partial class AdminVerbSystem
                 _antag.ForceMakeAntag<TraitorRuleComponent>(targetPlayer, DefaultTraitorRule);
             },
             Impact = LogImpact.High,
-            Message = string.Join(": ", traitorName,  Loc.GetString("admin-verb-make-traitor")),
+            Message = string.Join(": ", traitorName, Loc.GetString("admin-verb-make-traitor")),
         };
         args.Verbs.Add(traitor);
 
@@ -153,6 +153,21 @@ public sealed partial class AdminVerbSystem
         };
         args.Verbs.Add(thief);
 
+        var changelingName = Loc.GetString("admin-verb-text-make-changeling");
+        Verb changeling = new()
+        {
+            Text = changelingName,
+            Category = VerbCategory.Antag,
+            Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Objects/Weapons/Melee/armblade.rsi"), "icon"),
+            Act = () =>
+            {
+                _antag.ForceMakeAntag<ChangelingRuleComponent>(targetPlayer, DefaultChangelingRule);
+            },
+            Impact = LogImpact.High,
+            Message = string.Join(": ", changelingName, Loc.GetString("admin-verb-make-changeling")),
+        };
+        args.Verbs.Add(changeling);
+
         var paradoxCloneName = Loc.GetString("admin-verb-text-make-paradox-clone");
         Verb paradox = new()
         {
index eba806ceb843fc6f4707b4f14e885c685a31105b..84ef0503051ccdbae249b3c813fa5adb1c03a018 100644 (file)
@@ -1,11 +1,16 @@
 using Content.Server.Forensics;
+using Content.Server.Speech.EntitySystems;
 using Content.Shared.Cloning.Events;
-using Content.Shared.Clothing.Components;
 using Content.Shared.FixedPoint;
+using Content.Shared.Inventory;
 using Content.Shared.Labels.Components;
 using Content.Shared.Labels.EntitySystems;
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Systems;
 using Content.Shared.Paper;
 using Content.Shared.Stacks;
+using Content.Shared.Speech.Components;
+using Content.Shared.Storage;
 using Content.Shared.Store;
 using Content.Shared.Store.Components;
 using Robust.Shared.Prototypes;
@@ -13,47 +18,58 @@ using Robust.Shared.Prototypes;
 namespace Content.Server.Cloning;
 
 /// <summary>
-///     The part of item cloning responsible for copying over important components.
-///     This is used for <see cref="CopyItem"/>.
-///     Anything not copied over here gets reverted to the values the item had in its prototype.
+/// The part of item cloning responsible for copying over important components.
 /// </summary>
 /// <remarks>
-///     This method of copying items is of course not perfect as we cannot clone every single component, which would be pretty much impossible with our ECS.
-///     We only consider the most important components so the paradox clone gets similar equipment.
-///     This method of using subscriptions was chosen to make it easy for forks to add their own custom components that need to be copied.
+/// These are all not part of their corresponding systems because we don't want systems every system to depend on a CloningSystem namespace import, which is still heavily coupled to med code.
+/// TODO: Create a more generic "CopyEntity" method/event (probably in RT) that doesn't have this problem and then move all these subscriptions.
 /// </remarks>
-public sealed partial class CloningSystem : EntitySystem
+public sealed partial class CloningSystem
 {
     [Dependency] private readonly SharedStackSystem _stack = default!;
     [Dependency] private readonly LabelSystem _label = default!;
     [Dependency] private readonly ForensicsSystem _forensics = default!;
     [Dependency] private readonly PaperSystem _paper = default!;
+    [Dependency] private readonly VocalSystem _vocal = default!;
+    [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<StackComponent, CloningItemEvent>(OnCloneStack);
-        SubscribeLocalEvent<LabelComponent, CloningItemEvent>(OnCloneLabel);
-        SubscribeLocalEvent<PaperComponent, CloningItemEvent>(OnClonePaper);
-        SubscribeLocalEvent<ForensicsComponent, CloningItemEvent>(OnCloneForensics);
-        SubscribeLocalEvent<StoreComponent, CloningItemEvent>(OnCloneStore);
+        // These are used for <see cref="CopyItem"/>.
+        // Anything not copied over here gets reverted to the values the item had in its prototype.
+        // This method of copying items is of course not perfect as we cannot clone every single component, which would be pretty much impossible with our ECS.
+        // We only consider the most important components so the paradox clone gets similar equipment.
+        // This method of using subscriptions was chosen to make it easy for forks to add their own custom components that need to be copied.
+        SubscribeLocalEvent<StackComponent, CloningItemEvent>(OnCloneItemStack);
+        SubscribeLocalEvent<LabelComponent, CloningItemEvent>(OnCloneItemLabel);
+        SubscribeLocalEvent<PaperComponent, CloningItemEvent>(OnCloneItemPaper);
+        SubscribeLocalEvent<ForensicsComponent, CloningItemEvent>(OnCloneItemForensics);
+        SubscribeLocalEvent<StoreComponent, CloningItemEvent>(OnCloneItemStore);
+
+        // These are for cloning components that cannot be cloned using CopyComp.
+        // Put them into CloningSettingsPrototype.EventComponents to have them be applied to the clone.
+        SubscribeLocalEvent<VocalComponent, CloningEvent>(OnCloneVocal);
+        SubscribeLocalEvent<StorageComponent, CloningEvent>(OnCloneStorage);
+        SubscribeLocalEvent<InventoryComponent, CloningEvent>(OnCloneInventory);
+        SubscribeLocalEvent<MovementSpeedModifierComponent, CloningEvent>(OnCloneInventory);
     }
 
-    private void OnCloneStack(Entity<StackComponent> ent, ref CloningItemEvent args)
+    private void OnCloneItemStack(Entity<StackComponent> ent, ref CloningItemEvent args)
     {
         // if the clone is a stack as well, adjust the count of the copy
         if (TryComp<StackComponent>(args.CloneUid, out var cloneStackComp))
             _stack.SetCount(args.CloneUid, ent.Comp.Count, cloneStackComp);
     }
 
-    private void OnCloneLabel(Entity<LabelComponent> ent, ref CloningItemEvent args)
+    private void OnCloneItemLabel(Entity<LabelComponent> ent, ref CloningItemEvent args)
     {
         // copy the label
         _label.Label(args.CloneUid, ent.Comp.CurrentLabel);
     }
 
-    private void OnClonePaper(Entity<PaperComponent> ent, ref CloningItemEvent args)
+    private void OnCloneItemPaper(Entity<PaperComponent> ent, ref CloningItemEvent args)
     {
         // copy the text and any stamps
         if (TryComp<PaperComponent>(args.CloneUid, out var clonePaperComp))
@@ -63,13 +79,13 @@ public sealed partial class CloningSystem : EntitySystem
         }
     }
 
-    private void OnCloneForensics(Entity<ForensicsComponent> ent, ref CloningItemEvent args)
+    private void OnCloneItemForensics(Entity<ForensicsComponent> ent, ref CloningItemEvent args)
     {
         // copy any forensics to the cloned item
         _forensics.CopyForensicsFrom(ent.Comp, args.CloneUid);
     }
 
-    private void OnCloneStore(Entity<StoreComponent> ent, ref CloningItemEvent args)
+    private void OnCloneItemStore(Entity<StoreComponent> ent, ref CloningItemEvent args)
     {
         // copy the current amount of currency in the store
         // at the moment this takes care of uplink implants and the portable nukie uplinks
@@ -80,4 +96,35 @@ public sealed partial class CloningSystem : EntitySystem
         }
     }
 
+    private void OnCloneVocal(Entity<VocalComponent> ent, ref CloningEvent args)
+    {
+        if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
+            return;
+
+        _vocal.CopyComponent(ent.AsNullable(), args.CloneUid);
+    }
+
+    private void OnCloneStorage(Entity<StorageComponent> ent, ref CloningEvent args)
+    {
+        if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
+            return;
+
+        _storage.CopyComponent(ent.AsNullable(), args.CloneUid);
+    }
+
+    private void OnCloneInventory(Entity<InventoryComponent> ent, ref CloningEvent args)
+    {
+        if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
+            return;
+
+        _inventory.CopyComponent(ent.AsNullable(), args.CloneUid);
+    }
+
+    private void OnCloneInventory(Entity<MovementSpeedModifierComponent> ent, ref CloningEvent args)
+    {
+        if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
+            return;
+
+        _movementSpeedModifier.CopyComponent(ent.AsNullable(), args.CloneUid);
+    }
 }
index 97ab41e7b1c538189197f29f85c2b6e7489be0c5..b0d62be5239cac8af6ae6965a5e2cee271236232 100644 (file)
@@ -24,7 +24,7 @@ namespace Content.Server.Cloning;
 ///     System responsible for making a copy of a humanoid's body.
 ///     For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
 /// </summary>
-public sealed partial class CloningSystem : EntitySystem
+public sealed partial class CloningSystem : SharedCloningSystem
 {
     [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
     [Dependency] private readonly InventorySystem _inventory = default!;
@@ -84,13 +84,7 @@ public sealed partial class CloningSystem : EntitySystem
         return true;
     }
 
-    /// <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 void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings)
+    public override void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings)
     {
         var componentsToCopy = settings.Components;
         var componentsToEvent = settings.EventComponents;
@@ -128,7 +122,8 @@ public sealed partial class CloningSystem : EntitySystem
             }
 
             // If the original does not have the component, then the clone shouldn't have it either.
-            RemComp(clone, componentRegistration.Type);
+            if (!HasComp(original, componentRegistration.Type))
+                RemComp(clone, componentRegistration.Type);
         }
 
         var cloningEv = new CloningEvent(settings, clone);
index 3fd71def0d08c57277bbfc73a2faa82d25969431..d911a89ec9cf9636f0c1ff2730e5467a2e90db1a 100644 (file)
@@ -1,6 +1,6 @@
 using Content.Server.Emoting.Systems;
 using Content.Shared.Chat.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.Emoting.Components;
 
@@ -14,11 +14,6 @@ public sealed partial class BodyEmotesComponent : Component
     /// <summary>
     ///     Emote sounds prototype id for body emotes.
     /// </summary>
-    [DataField("soundsId", customTypeSerializer: typeof(PrototypeIdSerializer<EmoteSoundsPrototype>))]
-    public string? SoundsId;
-
-    /// <summary>
-    ///     Loaded emote sounds prototype used for body emotes.
-    /// </summary>
-    public EmoteSoundsPrototype? Sounds;
+    [DataField]
+    public ProtoId<EmoteSoundsPrototype>? SoundsId;
 }
index 594eb0ec6d396e0559848ae6d3697092e56f696a..aef79f1419b6b453784e31ca1f48f25433ace517 100644 (file)
@@ -14,15 +14,8 @@ public sealed class BodyEmotesSystem : EntitySystem
     public override void Initialize()
     {
         base.Initialize();
-        SubscribeLocalEvent<BodyEmotesComponent, ComponentStartup>(OnStartup);
-        SubscribeLocalEvent<BodyEmotesComponent, EmoteEvent>(OnEmote);
-    }
 
-    private void OnStartup(EntityUid uid, BodyEmotesComponent component, ComponentStartup args)
-    {
-        if (component.SoundsId == null)
-            return;
-        _proto.TryIndex(component.SoundsId, out component.Sounds);
+        SubscribeLocalEvent<BodyEmotesComponent, EmoteEvent>(OnEmote);
     }
 
     private void OnEmote(EntityUid uid, BodyEmotesComponent component, ref EmoteEvent args)
@@ -43,6 +36,9 @@ public sealed class BodyEmotesSystem : EntitySystem
         if (!TryComp(uid, out HandsComponent? hands) || hands.Count <= 0)
             return false;
 
-        return _chat.TryPlayEmoteSound(uid, component.Sounds, emote);
+        if (!_proto.Resolve(component.SoundsId, out var sounds))
+            return false;
+
+        return _chat.TryPlayEmoteSound(uid, sounds, emote);
     }
 }
diff --git a/Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs b/Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs
new file mode 100644 (file)
index 0000000..a64b0e9
--- /dev/null
@@ -0,0 +1,23 @@
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Roles;
+using Content.Shared.Changeling;
+
+namespace Content.Server.GameTicking.Rules;
+
+/// <summary>
+/// Game rule system for Changelings
+/// </summary>
+public sealed class ChangelingRuleSystem : GameRuleSystem<ChangelingRuleComponent>
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<ChangelingRoleComponent, GetBriefingEvent>(OnGetBriefing);
+    }
+
+    private void OnGetBriefing(Entity<ChangelingRoleComponent> ent, ref GetBriefingEvent args)
+    {
+        args.Append(Loc.GetString("changeling-briefing"));
+    }
+}
diff --git a/Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs
new file mode 100644 (file)
index 0000000..13891c1
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Server.GameTicking.Rules.Components;
+
+/// <summary>
+/// Gamerule component for handling a changeling antagonist.
+/// </summary>
+[RegisterComponent]
+public sealed partial class ChangelingRuleComponent : Component;
index e28a0bc35fd55078e803abfcb7eacc4547bd23cd..f1e8e41258c9404f87452835ed3a4b7ab8883d02 100644 (file)
@@ -14,7 +14,7 @@ public sealed partial class ParadoxCloneRuleComponent : Component
     ///     Cloning settings to be used.
     /// </summary>
     [DataField]
-    public ProtoId<CloningSettingsPrototype> Settings = "Antag";
+    public ProtoId<CloningSettingsPrototype> Settings = "ParadoxCloningSettings";
 
     /// <summary>
     ///     Visual effect spawned when gibbing at round end.
index fb882382886afc8fbb922fc4316f324689e15a1f..275140ff5b35b159702bdad0eae172a3f5788822 100644 (file)
@@ -2,6 +2,7 @@ using Content.Server.Actions;
 using Content.Server.Chat.Systems;
 using Content.Server.Speech.Components;
 using Content.Shared.Chat.Prototypes;
+using Content.Shared.Cloning.Events;
 using Content.Shared.Humanoid;
 using Content.Shared.Speech;
 using Content.Shared.Speech.Components;
@@ -31,6 +32,25 @@ public sealed class VocalSystem : EntitySystem
         SubscribeLocalEvent<VocalComponent, ScreamActionEvent>(OnScreamAction);
     }
 
+    /// <summary>
+    /// Copy this component's datafields from one entity to another.
+    /// This can't use CopyComp because of the ScreamActionEntity DataField, which should not be copied.
+    /// <summary>
+    public void CopyComponent(Entity<VocalComponent?> source, EntityUid target)
+    {
+        if (!Resolve(source, ref source.Comp))
+            return;
+
+        var targetComp = EnsureComp<VocalComponent>(target);
+        targetComp.Sounds = source.Comp.Sounds;
+        targetComp.ScreamId = source.Comp.ScreamId;
+        targetComp.Wilhelm = source.Comp.Wilhelm;
+        targetComp.WilhelmProbability = source.Comp.WilhelmProbability;
+        LoadSounds(target, targetComp);
+
+        Dirty(target, targetComp);
+    }
+
     private void OnMapInit(EntityUid uid, VocalComponent component, MapInitEvent args)
     {
         // try to add scream action when vocal comp added
index 7ccc19e20c69af8e6b85025760352cd76fb3f605..88b82a9ca5d534dde03e9ab426fa1250f64a1076 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.Actions;
 using Content.Server.Humanoid;
+using Content.Shared.Cloning.Events;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Mobs;
@@ -26,6 +27,15 @@ public sealed class WaggingSystem : EntitySystem
         SubscribeLocalEvent<WaggingComponent, ComponentShutdown>(OnWaggingShutdown);
         SubscribeLocalEvent<WaggingComponent, ToggleActionEvent>(OnWaggingToggle);
         SubscribeLocalEvent<WaggingComponent, MobStateChangedEvent>(OnMobStateChanged);
+        SubscribeLocalEvent<WaggingComponent, CloningEvent>(OnCloning);
+    }
+
+    private void OnCloning(Entity<WaggingComponent> ent, ref CloningEvent args)
+    {
+        if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
+            return;
+
+        EnsureComp<WaggingComponent>(args.CloneUid);
     }
 
     private void OnWaggingMapInit(EntityUid uid, WaggingComponent component, MapInitEvent args)
diff --git a/Content.Shared/Changeling/ChangelingIdentityComponent.cs b/Content.Shared/Changeling/ChangelingIdentityComponent.cs
new file mode 100644 (file)
index 0000000..461315f
--- /dev/null
@@ -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;
+}
diff --git a/Content.Shared/Changeling/ChangelingIdentitySystem.cs b/Content.Shared/Changeling/ChangelingIdentitySystem.cs
new file mode 100644 (file)
index 0000000..8f704c4
--- /dev/null
@@ -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);
+    }
+}
diff --git a/Content.Shared/Changeling/ChangelingRoleComponent.cs b/Content.Shared/Changeling/ChangelingRoleComponent.cs
new file mode 100644 (file)
index 0000000..d2e9c1e
--- /dev/null
@@ -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;
diff --git a/Content.Shared/Changeling/ChangelingStoredIdentityComponent.cs b/Content.Shared/Changeling/ChangelingStoredIdentityComponent.cs
new file mode 100644 (file)
index 0000000..4458319
--- /dev/null
@@ -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;
+}
diff --git a/Content.Shared/Changeling/Devour/ChangelingDevourComponent.cs b/Content.Shared/Changeling/Devour/ChangelingDevourComponent.cs
new file mode 100644 (file)
index 0000000..7798c6f
--- /dev/null
@@ -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;
+}
diff --git a/Content.Shared/Changeling/Devour/ChangelingDevourSystem.Events.cs b/Content.Shared/Changeling/Devour/ChangelingDevourSystem.Events.cs
new file mode 100644 (file)
index 0000000..d063737
--- /dev/null
@@ -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;
diff --git a/Content.Shared/Changeling/Devour/ChangelingDevourSystem.cs b/Content.Shared/Changeling/Devour/ChangelingDevourSystem.cs
new file mode 100644 (file)
index 0000000..83a589a
--- /dev/null
@@ -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);
+    }
+}
diff --git a/Content.Shared/Changeling/Transform/ChangelingTransformComponent.cs b/Content.Shared/Changeling/Transform/ChangelingTransformComponent.cs
new file mode 100644 (file)
index 0000000..0a3b3f1
--- /dev/null
@@ -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;
+}
+
diff --git a/Content.Shared/Changeling/Transform/ChangelingTransformSystem.Events.cs b/Content.Shared/Changeling/Transform/ChangelingTransformSystem.Events.cs
new file mode 100644 (file)
index 0000000..cfe2a56
--- /dev/null
@@ -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;
diff --git a/Content.Shared/Changeling/Transform/ChangelingTransformSystem.UI.cs b/Content.Shared/Changeling/Transform/ChangelingTransformSystem.UI.cs
new file mode 100644 (file)
index 0000000..0383867
--- /dev/null
@@ -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,
+}
diff --git a/Content.Shared/Changeling/Transform/ChangelingTransformSystem.cs b/Content.Shared/Changeling/Transform/ChangelingTransformSystem.cs
new file mode 100644 (file)
index 0000000..dbc5356
--- /dev/null
@@ -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);
+        }
+    }
+}
diff --git a/Content.Shared/Cloning/SharedCloningSystem.cs b/Content.Shared/Cloning/SharedCloningSystem.cs
new file mode 100644 (file)
index 0000000..d8ab8a2
--- /dev/null
@@ -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)
+    {
+    }
+}
index 3d3af84a3073f06f28c4c163787a1ace51e238e4..1df46e53d6367ab1d6802f2b56e35d1d39cf45d3 100644 (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;
-
-        if (TryComp<GrammarComponent>(target, out var grammar))
-            _grammarSystem.SetGender((target, grammar), sourceHumanoid.Gender);
+        SetSex(target, sourceHumanoid.Sex, false, targetHumanoid);
+        SetGender((target, targetHumanoid), 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.
index e9fb62f2adc8bf2f0362dd434a51bb5e4352ca12..09c3bbc45bd5192c605106aecadc56229fd6890a 100644 (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);
index 4584e4401aba20589d51a5e5e9a811230dccf62e..d0faad8b505d264794b4db3397923b8bc201205d 100644 (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))
index c3deca07699bcc94b78fc417390ca9b6b21d12aa..9165c3c111780e08006fc4299430640f68ca422c 100644 (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)
index e5942a433e0b9400038a101bbd2a17b0fa6ed44f..e6086e67c20d9c3d17a436e279a1e2daaa9a93e6 100644 (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);
index 8c12fc918a01cafaac4257f0a9c357a2617a61f8..fddb41753e9dce35ddac92cebb8c2e83ba7ebf2a 100644 (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;
 
index f3c90559104e535bcba3f1d39910f07a38ca270e..d2d80a632fa5554572212e78201e6df0c859450c 100644 (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;
     }
 }
diff --git a/Resources/Audio/Effects/Changeling/attributions.yml b/Resources/Audio/Effects/Changeling/attributions.yml
new file mode 100644 (file)
index 0000000..d7d7931
--- /dev/null
@@ -0,0 +1,19 @@
+- files: ["devour_suck.ogg"]
+  license: "CC0-1.0"
+  copyright: "4Cairnz on Freesound: June 5th 2023"
+  source: "https://freesound.org/people/4Cairnz/sounds/689640/"
+
+- files: ["devour_windup.ogg "]
+  license: "CC-BY-SA-3.0"
+  copyright: "Made by @DarkIcedCoffee on Discord for SS14, utilizing sounds from Caitlin_100, jedg and EricsSoundschmiede on freesound"
+  source: "https://youtu.be/iviCUO2xH_E"
+
+- files: ["devour_consume.ogg"]
+  license: "CC-BY-SA-3.0"
+  copyright: "Made by @DarkIcedCoffee on Discord for SS14, utilizing sounds from jedg and reg7783 on freesound."
+  source: "https://youtu.be/iviCUO2xH_E"
+
+- files: ["changeling_transform.ogg"]
+  license: "CC-BY-SA-3.0"
+  copyright: "Made by @DarkIcedCoffee on Discord for SS14"
+  source: "https://youtu.be/iviCUO2xH_E"
diff --git a/Resources/Audio/Effects/Changeling/changeling_transform.ogg b/Resources/Audio/Effects/Changeling/changeling_transform.ogg
new file mode 100644 (file)
index 0000000..23379d2
Binary files /dev/null and b/Resources/Audio/Effects/Changeling/changeling_transform.ogg differ
diff --git a/Resources/Audio/Effects/Changeling/devour_consume.ogg b/Resources/Audio/Effects/Changeling/devour_consume.ogg
new file mode 100644 (file)
index 0000000..9d05c9f
Binary files /dev/null and b/Resources/Audio/Effects/Changeling/devour_consume.ogg differ
diff --git a/Resources/Audio/Effects/Changeling/devour_suck.ogg b/Resources/Audio/Effects/Changeling/devour_suck.ogg
new file mode 100644 (file)
index 0000000..d756b0a
Binary files /dev/null and b/Resources/Audio/Effects/Changeling/devour_suck.ogg differ
diff --git a/Resources/Audio/Effects/Changeling/devour_windup.ogg b/Resources/Audio/Effects/Changeling/devour_windup.ogg
new file mode 100644 (file)
index 0000000..7c81d9a
Binary files /dev/null and b/Resources/Audio/Effects/Changeling/devour_windup.ogg differ
index 1433cc1dc481f7960f407eefca1005027179d00c..161054aca22d668c30e5006e8eb5b04b5e8b82dc 100644 (file)
@@ -7,6 +7,8 @@ admin-verb-make-pirate = Make the target into a pirate. Note this doesn't config
 admin-verb-make-head-rev = Make the target into a Head Revolutionary.
 admin-verb-make-thief = Make the target into a thief.
 admin-verb-make-paradox-clone = Create a Paradox Clone ghost role of the target.
+admin-verb-make-changeling = Make the target into a Changeling.
+
 
 admin-verb-text-make-traitor = Make Traitor
 admin-verb-text-make-initial-infected = Make Initial Infected
@@ -16,5 +18,6 @@ admin-verb-text-make-pirate = Make Pirate
 admin-verb-text-make-head-rev = Make Head Rev
 admin-verb-text-make-thief = Make Thief
 admin-verb-text-make-paradox-clone = Create Paradox Clone
+admin-verb-text-make-changeling = Make Changeling (WIP)
 
 admin-overlay-antag-classic = ANTAG
diff --git a/Resources/Locale/en-US/changeling/changeling.ftl b/Resources/Locale/en-US/changeling/changeling.ftl
new file mode 100644 (file)
index 0000000..aa38436
--- /dev/null
@@ -0,0 +1,20 @@
+roles-antag-changeling-name = Changeling
+roles-antag-changeling-objective = A intelligent predator that assumes the identities of its victims.
+
+changeling-role-greeting = You are a Changeling, a highly intelligent predator. Your only goal is to escape the station alive via assuming the identities of the denizens of this station. You are hungry and will not make it long without sustenance... kill, consume, hide, survive.
+changeling-briefing = You are a changeling, your goal is to survive. Consume humanoids to gain biomass and utilize it to evade termination. You are able to utilize and assume the identities of those you consume to evade a grim fate.
+
+changeling-devour-attempt-failed-rotting = This corpse has only rotted biomass.
+changeling-devour-attempt-failed-protected = This victim's biomass is protected.
+
+changeling-devour-begin-windup-self = Our uncanny mouth reveals itself with otherworldly hunger.
+changeling-devour-begin-windup-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth reveals itself with otherworldly hunger.
+changeling-devour-begin-consume-self = The uncanny mouth digs deep into its victim.
+changeling-devour-begin-consume-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth digs deep into { POSS-ADJ($user) } victim.
+
+changeling-devour-consume-failed-not-dead = This body yet lives! We cannot consume it alive!
+changeling-devour-consume-complete-self = Our uncanny mouth retreats, biomass consumed.
+changeling-devour-consume-complete-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth retreats.
+
+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.
index 7d568fd68687c8ad97fb0452a117596b6293d4b4..4614d20a47779620570560acf6a4044a45e6d090 100644 (file)
@@ -33,3 +33,4 @@ role-subtype-survivor = Survivor
 role-subtype-subverted = Subverted
 role-subtype-paradox-clone = Paradox
 role-subtype-wizard = Wizard
+role-subtype-changeling = Changeling
diff --git a/Resources/Prototypes/Antag/changeling.yml b/Resources/Prototypes/Antag/changeling.yml
new file mode 100644 (file)
index 0000000..b4c8b2e
--- /dev/null
@@ -0,0 +1,35 @@
+- type: entity
+  parent: MobHuman
+  id: MobLing
+  name: Urist McLing
+  suffix: Non-Antag
+  components:
+  - type: ChangelingDevour
+  - type: ChangelingIdentity
+  - type: ChangelingTransform
+  - type: ActionGrant
+    actions:
+    - ActionRetractableItemArmBlade # Temporary addition, will inevitably be a purchasable in the bio-store
+
+- type: entity
+  id: ActionChangelingDevour
+  name: "[color=red]Devour[/color]"
+  description: Consume the essence of your victims and subsume their identity and mind into your own.
+  components:
+  - type: Action
+    icon: { sprite : Interface/Actions/changeling.rsi, state: "devour" }
+    iconOn: { sprite : Interface/Actions/changeling.rsi, state: "devour_on" }
+    priority: 1
+  - type: TargetAction
+  - type: EntityTargetAction
+    event: !type:ChangelingDevourActionEvent
+
+- type: entity
+  id: ActionChangelingTransform
+  name: "[color=red]Transform[/color]"
+  description: Transform and assume the identities of those you have devoured.
+  components:
+  - type: Action
+    icon: { sprite : Interface/Actions/changeling.rsi, state: "transform" }
+  - type: InstantAction
+    event: !type:ChangelingTransformActionEvent
index bdf741e58a7294532ceb0c4e56bb4ec4d62aa9cf..49c05ba3f11536d46a305f97ef06da6aa3cb3785 100644 (file)
@@ -1,13 +1,12 @@
 # Settings for cloning bodies
 # If you add a new trait, job specific component or a component doing visual/examination changes for humanoids
 # then add it here to the correct prototype.
-# The datafields of the components are only shallow copied using CopyComp.
+# The datafields of the components copied using CopyComp.
 # Subscribe to CloningEvent instead if that is not enough.
 
-# for basic traits etc.
-# used by the random clone spawner
+# for basic physical traits
 - type: cloningSettings
-  id: BaseClone
+  id: Body
   components:
   # general
   - DetailExaminable
@@ -15,8 +14,9 @@
   - Fingerprint
   - NpcFactionMember
   # traits
-  # - LegsParalyzed (you get healed)
   - BlackAndWhiteOverlay
+  - Clumsy
+  # - LegsParalyzed (you get healed)
   - LightweightDrunk
   - Muted
   - Narcolepsy
   - PermanentBlindness
   - Snoring
   - Unrevivable
-  # job specific
-  - BibleUser
-  - CommandStaff
-  - Clumsy
-  - MindShield
-  - MimePowers
-  - SpaceNinja
-  - Thieving
   # accents
   - Accentless
   - BackwardsAccent
   - SouthernAccent
   - SpanishAccent
   - StutteringAccent
-  blacklist:
-    components:
-    - AttachedClothing # helmets, which are part of the suit
-    - HumanoidAppearance # will cause problems for downstream felinids getting cloned as Urists
-    - Implanter # they will spawn full again, but you already get the implant. And we can't do item slot copying yet
-    - VirtualItem
 
-# all antagonist roles
+# for job-specific traits etc.
+- type: cloningSettings
+  id: Special
+  components:
+  - BibleUser
+  - CommandStaff
+  - MindShield
+  - MimePowers
+  - SpaceNinja
+  - Thieving
+
+# antag roles
 - type: cloningSettings
   id: Antag
-  parent: BaseClone
   components:
   - HeadRevolutionary
   - Revolutionary
   - NukeOperative
 
+# a full clone with all traits and items, but no antag roles
+- type: cloningSettings
+  id: BaseClone
+  parent: [Body, Special]
+  blacklist:
+    components:
+    - AttachedClothing # helmets, which are part of the suit
+    - HumanoidAppearance # will cause problems for downstream felinids getting cloned as Urists
+    - Implanter # they will spawn full again, but you already get the implant. And we can't do item slot copying yet
+    - VirtualItem
+
 # for cloning pods
 - type: cloningSettings
   id: CloningPod
-  parent: Antag
+  parent: [BaseClone, Antag]
   forceCloning: false
   copyEquipment: null
   copyInternalStorage: false
   copyImplants: false
 
-# spawner
+# for paradox clones
+- type: cloningSettings
+  id: ParadoxCloningSettings
+  parent: [BaseClone, Antag]
 
+# changeling identity copying
+- type: cloningSettings
+  id: ChangelingCloningSettings
+  parent: Body
+  components:
+  # These are already part of the base species prototype that is spawned for the clone,
+  # that means we only need to copy them over when switching between species.
+  # So these don't need to be part of the Body settings, unless someone makes a trait that adjusts these components.
+  - BodyEmotes
+  - Fixtures
+  - Speech
+  - TypingIndicator
+  - ScaleVisuals # for dwarf height
+  eventComponents:
+  # these need special treatment in the event subscription
+  - Inventory # arachnid pockets and diona feet
+  - Wagging # lizard tails
+  - Vocal # voice sounds
+  - Storage # slime storage
+  - Rootable # diona
+  - Sericulture # arachnids
+  - MovementSpeedModifier # moths when weightless
+  copyEquipment: null
+  copyInternalStorage: false
+  copyImplants: false
+
+# spawner
 - type: entity
   id: RandomCloneSpawner
   name: Random Clone
index 08d088a08cc309920dd20a2db2a60b2e8e9fa3e2..71a249ed486e1722a643615e90e13eb4d2e13372 100644 (file)
       mindRoles:
       - MindRoleTraitorReinforcement
 
+- type: entity
+  id: Changeling
+  parent: BaseGameRule
+  components:
+  - type: GameRule
+    minPlayers: 25
+  - type: AntagSelection
+    definitions:
+    - prefRoles: [ Changeling ]
+      max: 3
+      playerRatio: 15
+      briefing:
+        text: changeling-role-greeting
+        color: Red
+      components:
+      - type: ChangelingDevour
+      - type: ChangelingIdentity
+      - type: ChangelingTransform
+      - type: ActionGrant
+        actions:
+        - ActionRetractableItemArmBlade # Temporary addition, will inevitably be a purchasable in the bio-store
+      mindRoles:
+      - MindRoleChangeling
+
 - type: entity
   id: Revolutionary
   parent: BaseGameRule
diff --git a/Resources/Prototypes/Roles/Antags/changeling.yml b/Resources/Prototypes/Roles/Antags/changeling.yml
new file mode 100644 (file)
index 0000000..3b19c99
--- /dev/null
@@ -0,0 +1,6 @@
+- type: antag
+  id: Changeling
+  name: roles-antag-changeling-name
+  antagonist: true
+  setPreference: false # TODO: set this to true once Changeling exits WIP status
+  objective: roles-antag-changeling-objective
index 79223ed67da2a17412a73c74e2328b86dc20c949..95d49c1b83d13c6236dd8bc552b39c677c608a41 100644 (file)
     exclusiveAntag: true
     subtype: role-subtype-zombie
   - type: ZombieRole
+
+# Changeling
+- type: entity
+  parent: BaseMindRoleAntag
+  id: MindRoleChangeling
+  name: Changeling Role
+  components:
+  - type: MindRole
+    antagPrototype: Changeling
+    exclusiveAntag: true
+    roleType: SoloAntagonist
+    subtype: role-subtype-changeling
+  - type: ChangelingRole
diff --git a/Resources/Prototypes/SoundCollections/changeling.yml b/Resources/Prototypes/SoundCollections/changeling.yml
new file mode 100644 (file)
index 0000000..4edb07d
--- /dev/null
@@ -0,0 +1,14 @@
+- type: soundCollection
+  id: ChangelingDevourConsume
+  files:
+  - /Audio/Effects/Changeling/devour_consume.ogg
+
+- type: soundCollection
+  id: ChangelingDevourWindup
+  files:
+  - /Audio/Effects/Changeling/devour_windup.ogg
+
+- type: soundCollection
+  id: ChangelingTransformAttempt
+  files:
+  - /Audio/Effects/Changeling/changeling_transform.ogg
diff --git a/Resources/Textures/Interface/Actions/changeling.rsi/devour.png b/Resources/Textures/Interface/Actions/changeling.rsi/devour.png
new file mode 100644 (file)
index 0000000..f5acc7d
Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling.rsi/devour.png differ
diff --git a/Resources/Textures/Interface/Actions/changeling.rsi/devour_on.png b/Resources/Textures/Interface/Actions/changeling.rsi/devour_on.png
new file mode 100644 (file)
index 0000000..65d63eb
Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling.rsi/devour_on.png differ
index babe7766125da06a5af8c95fe8e54f4d609b0ce9..20c9ed1720d222cd5e2e161f57a85cc78e073eb8 100644 (file)
@@ -7,6 +7,15 @@
         "y": 32
     },
     "states": [
+        {
+            "name": "transform"
+        },
+        {
+            "name": "devour"
+        },
+        {
+            "name": "devour_on"
+        },
         {
             "name": "armblade"
         }
diff --git a/Resources/Textures/Interface/Actions/changeling.rsi/transform.png b/Resources/Textures/Interface/Actions/changeling.rsi/transform.png
new file mode 100644 (file)
index 0000000..6657790
Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling.rsi/transform.png differ