Changeling devour and transform (#34002)

* Initial:

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

* I have genuinely no idea what i'm doing

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

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

- Added a placeholder transform

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

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

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

- Make Transform MOSTLY work on the LAST identity devoured.

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

* Proper tail stealing and Damage modifier attempt

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

attempt to add the Damage mitigation check

* MAJOR CLEANUP AND FIXES AUGH 3 DAYS!!!

- Nullspaced a clone of a victim

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

- fix the mispredicted doafters

- Clean up a wholelotta code

- utilize clone systems to clone appearances

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

* Examine stuff, more cleanup, Jumpsuit ripping

- make rotting prevent the action

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

- add some System stuff for pushing husked corpse inspection

- clean up more badcode

* Doing things properly, UI sorta kinda works.

- Utilize Relayed events for Devour checking

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

- Make use of the New Dynamic BUI assignment

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

* Got an entity for the Frontend transform

Issue with the looks

* Stick a camera into a fake mobs forehead

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

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

* Raaaaadiallllllls also fix protection coefficents

- Change FancyWindow to Radial

- Fix Issue where coeffeient checking was the wrong way round

* absolutely massive cleanup, no more camera in mobs

- cleaned up event variables that are not needed

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

- use a PvsOverride rather than ViewSharing

- Remove old commented out code and Lots of unused code

* Fix "Ui elements" dying  on the screen

- some minor cleanup

- don't start the entities that get cloned

* ftl, cleanup, and fixing missing transform details

- add replace functionality to TypingIndicatorSystem and
BodyEmotesSystem

- add placeholder sounds and functions to TransformBodyEmotes

- add extra Pvs handling for later use

- attributions for the funny straw sound

- Sound collections for all of the sounds

- various cleanups

* Some extra cleanup

* Fix some false assumptions about TypingIndicator

- Bubbles now transfer on spawned humans rather than used humans

- Clean up YET MORE CODE

- make it so you can't eat yourself

* Oooprs, forgot to add a Husked Corpse Loc

* Missing period in the husked corpse loc

* bad devour windup placeholder

* Husking and WIP Lungs

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

* Add finalized Sprites for actions and final meta

- add devour on and off sprites

- add transform action sprite

- Add Armblade sprite for future use

- Credit obscenelytinyshark for the sprites <3

* Remove ling lungs, Entity<> everything

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

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

* cleanup, admin logging, WIP Roles

* Admin verb, Roundstart, gamerule stuff

- add a Admin verb to make Changelingification easy!

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

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

* Armor Coefficent Check

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

* move to UnrevivableComponent instead of husked

- Move UnrevivableComponent to shared

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

- remove the check for HuskedComponent in DefibrillatorSystem

* aaaaaaa CopyComp

- Some cleanup

- make Vocal system shared

- make VocalSystem Not make more Actions than it needs

- Use some code from ChameleonProjector so we can copy components

- partially ungod method the Transform system

* Cleanup, Moving more things to CopyComp

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

- TypingIndicatorComponent also now uses CopyComp

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

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

* WIP

* Devour Windup noise, ForensicsSystem cleanup

* Revert VocalSystem Changes

- Reverted Moving VocalSystem to shared, copy comp acomplishes it

- added component.ScreamActionEntity = null; for copy comp

* cleanup unneeded comments

* revert an accidental line removal

* Remove duplicate SharedHumanoidAppearanceSystem

* Cleanup Typo's and import Forensics components for Dna

* Some more forensics calls

* cleanup use CopyComp for now until CopyComps

* CR cleanup

* Undo some SharedHumanoidAppearanceSystem changes

* Confound these spaces

* Some Copycomp stuff and fixing some PVS override

* use the proper TryCopyComps that are merged

* Change TransformMenu with RadialWithSector

* All sounds done, Fix lack of typing indicator issue

* Updated attributions to include used sound authors

* some ftl typos and mind_role text issue

* DNA, Screaming, appearance, grammar, wagging

- reduced all of the above using ApplyComponentChanges

- Issue still remains with bodyEmotes sticking around in the UI

* Fix UI stuff, partials, entprotoid, good practices

- bunch of partials added

- UI now has a predicted message

- EntProtoID in the admin verb

- RipClothing now uses Entity<ButcherableComponent>

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

- remove TransformGrammarSet

* More CR stuff and documentation

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

- Documenation everywhere

- Move DevourEvents into its own file

* Predicted sounds and fix the comp clone list

- Made all start and stop sounds shared

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

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

* Damage cap check, Identity Shutdown cleanup, cleanup

* Sound stuff (but actually this time)

* Missed documentation

* Missed Documentation and a EntProtoId

* Remove unused dependency

* Remove a nullcheck

* Some dummy minplayers

* CR - Husked now uses a rem/ensure

* Update Actions in the Prototype

* Fixup mindswap handover

- cleanup and handover PVS on mindswap

* Fixup Missing meta from accidental "Take-theirs"

* Add the Armblade to the roundstart-role

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

* missed a spot

* missed some more whitespace

* Renames

* Primary constructor and a space in these trying times

* User interface stuff for Slime transformation

* popup prediction

* Ling devour no longer makes duplicate identities

- added a key to identities to the original victim

- Add some extra clone settings

* add guard statements to OnClones

* SentOnlyToOwner additions

* fix for sound stoppage error

* Move Organ deleter into soon to be atomized husk

* clone event inventory

* mono sounds

* lower sound volume

* Fix networked sound warning

* Clone comps thing

* review

* attributions

* Fix clobbered changes

* I'm gonna weh out

- whole bunch of CR changes

* fix some very buggy git

* okay its fixed

* address most review points

* fix inventory

* we hate entityuids

* fix test and more cleanup

* move this

* fix more stuff

* fix validation and rootable

* Remove Quickswitch due to some UI quirks

* oops left out some better explanation

* remove dangling LastConsumed component fields

* fix test fail

* try this

* cleanup cloning subscriptions, add movement speed modifier

* fix slime storage

* fix cloning setting inheritance

* Add session information to transform admin logs

* slay the integration test hydra

* dwarf size

* more volume tweaks

* comments

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

* fix displancement cloning

---------

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

View File

@@ -0,0 +1,35 @@
using Content.Shared.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));
}
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
using Content.Shared.Cloning;
namespace Content.Client.Cloning;
public sealed partial class CloningSystem : SharedCloningSystem;

View 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();

View 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)
@@ -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()
{

View 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;
@@ -14,46 +19,57 @@ 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.
/// </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);
}
}

View 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,6 +122,7 @@ public sealed partial class CloningSystem : EntitySystem
}
// If the original does not have the component, then the clone shouldn't have it either.
if (!HasComp(original, componentRegistration.Type))
RemComp(clone, componentRegistration.Type);
}

View 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;
}

View 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);
}
}

View File

@@ -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"));
}
}

View File

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

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

View 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

View 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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View File

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

View 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

View File

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

View 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
@@ -26,14 +26,6 @@
- PermanentBlindness
- Snoring
- Unrevivable
# job specific
- BibleUser
- CommandStaff
- Clumsy
- MindShield
- MimePowers
- SpaceNinja
- Thieving
# accents
- Accentless
- BackwardsAccent
@@ -62,6 +54,30 @@
- SouthernAccent
- SpanishAccent
- StutteringAccent
# for job-specific traits etc.
- type: cloningSettings
id: Special
components:
- BibleUser
- CommandStaff
- MindShield
- MimePowers
- SpaceNinja
- Thieving
# antag roles
- type: cloningSettings
id: Antag
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
@@ -69,26 +85,47 @@
- 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
- type: cloningSettings
id: Antag
parent: BaseClone
components:
- HeadRevolutionary
- Revolutionary
- NukeOperative
# 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

View File

@@ -225,6 +225,30 @@
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

View File

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

View File

@@ -307,3 +307,16 @@
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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -7,6 +7,15 @@
"y": 32
},
"states": [
{
"name": "transform"
},
{
"name": "devour"
},
{
"name": "devour_on"
},
{
"name": "armblade"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB