diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index 12aa84413b..8e9f26105a 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -58,6 +58,8 @@ namespace Content.Client.Entry "ResearchClient", "IdCardConsole", "MimePowers", + "Polymorphable", + "PolymorphedEntity", "ThermalRegulator", "DiseaseMachineRunning", "DiseaseMachine", diff --git a/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs b/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs new file mode 100644 index 0000000000..d40644499d --- /dev/null +++ b/Content.Server/Administration/Commands/AddPolymorphActionCommand.cs @@ -0,0 +1,37 @@ +using Content.Server.Polymorph.Components; +using Content.Server.Polymorph.Systems; +using Content.Shared.Administration; +using Robust.Shared.Console; + +namespace Content.Server.Administration.Commands; + +[AdminCommand(AdminFlags.Fun)] +public sealed class AddPolymorphActionCommand : IConsoleCommand +{ + public string Command => "addpolymorphaction"; + + public string Description => Loc.GetString("add-polymorph-action-command-description"); + + public string Help => Loc.GetString("add-polymorph-action-command-help"); + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 2) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!EntityUid.TryParse(args[0], out var entityUid)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + + var entityManager = IoCManager.Resolve(); + var polySystem = entityManager.EntitySysManager.GetEntitySystem(); + + entityManager.EnsureComponent(entityUid); + polySystem.CreatePolymorphAction(args[1], entityUid); + } +} diff --git a/Content.Server/Administration/Commands/PolymorphCommand.cs b/Content.Server/Administration/Commands/PolymorphCommand.cs new file mode 100644 index 0000000000..3e79e85426 --- /dev/null +++ b/Content.Server/Administration/Commands/PolymorphCommand.cs @@ -0,0 +1,47 @@ +using Content.Server.Polymorph.Components; +using Content.Server.Polymorph.Systems; +using Content.Shared.Administration; +using Content.Shared.Polymorph; +using Robust.Shared.Console; +using Robust.Shared.Prototypes; + +namespace Content.Server.Administration.Commands; + +[AdminCommand(AdminFlags.Fun)] +public sealed class PolymorphCommand : IConsoleCommand +{ + public string Command => "polymorph"; + + public string Description => Loc.GetString("polymorph-command-description"); + + public string Help => Loc.GetString("polymorph-command-help"); + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 2) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!EntityUid.TryParse(args[0], out var entityUid)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + + var protoManager = IoCManager.Resolve(); + + if (!protoManager.TryIndex(args[1], out var polyproto)) + { + shell.WriteError(Loc.GetString("polymorph-not-valid-prototype-error")); + return; + } + + var entityManager = IoCManager.Resolve(); + var polySystem = entityManager.EntitySysManager.GetEntitySystem(); + + entityManager.EnsureComponent(entityUid); + polySystem.PolymorphEntity(entityUid, polyproto); + } +} diff --git a/Content.Server/Polymorph/Components/PolymorphableComponent.cs b/Content.Server/Polymorph/Components/PolymorphableComponent.cs new file mode 100644 index 0000000000..882b9d453b --- /dev/null +++ b/Content.Server/Polymorph/Components/PolymorphableComponent.cs @@ -0,0 +1,22 @@ +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Polymorph; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Server.Polymorph.Components +{ + [RegisterComponent] + public sealed class PolymorphableComponent : Component + { + /// + /// A list of all the polymorphs that the entity has. + /// Used to manage them and remove them if needed. + /// + public Dictionary? PolymorphActions = null; + + /// + /// The polymorphs that the entity starts out being able to do. + /// + [DataField("innatePolymorphs", customTypeSerializer : typeof(PrototypeIdListSerializer))] + public List? InnatePolymorphs = null; + } +} diff --git a/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs b/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs new file mode 100644 index 0000000000..817559d2bd --- /dev/null +++ b/Content.Server/Polymorph/Components/PolymorphedEntityComponent.cs @@ -0,0 +1,28 @@ +using Content.Shared.Polymorph; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Polymorph.Components +{ + [RegisterComponent] + public sealed class PolymorphedEntityComponent : Component + { + /// + /// The polymorph prototype, used to track various information + /// about the polymorph + /// + public PolymorphPrototype Prototype = default!; + + /// + /// The original entity that the player will revert back into + /// + [DataField("parent", required: true)] + public EntityUid Parent = new(); + + /// + /// The amount of time that has passed since the entity was created + /// used for tracking the duration + /// + [DataField("time")] + public float Time = 0; + } +} diff --git a/Content.Server/Polymorph/Systems/PolymorphableSystem.Map.cs b/Content.Server/Polymorph/Systems/PolymorphableSystem.Map.cs new file mode 100644 index 0000000000..fee16f822e --- /dev/null +++ b/Content.Server/Polymorph/Systems/PolymorphableSystem.Map.cs @@ -0,0 +1,41 @@ +using Content.Shared.GameTicking; + +namespace Content.Server.Polymorph.Systems +{ + public sealed partial class PolymorphableSystem : EntitySystem + { + public EntityUid? PausedMap { get; private set; } = null; + + /// + /// Used to subscribe to the round restart event + /// + private void InitializeMap() + { + SubscribeLocalEvent(OnRoundRestart); + } + + private void OnRoundRestart(RoundRestartCleanupEvent _) + { + if (PausedMap == null || !Exists(PausedMap)) + return; + + EntityManager.DeleteEntity(PausedMap.Value); + } + + /// + /// Used internally to ensure a paused map that is + /// stores polymorphed entities. + /// + private void EnsurePausesdMap() + { + if (PausedMap != null && Exists(PausedMap)) + return; + + var newmap = _mapManager.CreateMap(); + _mapManager.SetMapPaused(newmap, true); + PausedMap = _mapManager.GetMapEntityId(newmap); + + Dirty(PausedMap.Value); + } + } +} diff --git a/Content.Server/Polymorph/Systems/PolymorphableSystem.cs b/Content.Server/Polymorph/Systems/PolymorphableSystem.cs new file mode 100644 index 0000000000..3b16dcbecc --- /dev/null +++ b/Content.Server/Polymorph/Systems/PolymorphableSystem.cs @@ -0,0 +1,194 @@ +using Content.Server.Actions; +using Content.Server.Buckle.Components; +using Content.Server.Inventory; +using Content.Server.Mind.Commands; +using Content.Server.Mind.Components; +using Content.Server.Polymorph.Components; +using Content.Server.Popups; +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Damage; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Polymorph; +using Robust.Shared.Containers; +using Robust.Shared.Map; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server.Polymorph.Systems +{ + public sealed partial class PolymorphableSystem : EntitySystem + { + private readonly ISawmill _saw = default!; + + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly ServerInventorySystem _inventory = default!; + [Dependency] private readonly SharedHandsSystem _sharedHands = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnPolymorphActionEvent); + + InitializeMap(); + } + + private void OnStartup(EntityUid uid, PolymorphableComponent component, ComponentStartup args) + { + if (component.InnatePolymorphs != null) + { + foreach (var morph in component.InnatePolymorphs) + { + CreatePolymorphAction(morph, uid); + } + } + } + + private void OnPolymorphActionEvent(EntityUid uid, PolymorphableComponent component, PolymorphActionEvent args) + { + PolymorphEntity(uid, args.Prototype); + } + + /// + /// Polymorphs the target entity into the specific polymorph prototype + /// + /// The entity that will be transformed + /// The id of the polymorph prototype + public void PolymorphEntity(EntityUid target, String id) + { + if (!_proto.TryIndex(id, out var proto)) + { + _saw.Error("Invalid polymorph prototype"); + return; + } + + PolymorphEntity(target, proto); + } + + /// + /// Polymorphs the target entity into the specific polymorph prototype + /// + /// The entity that will be transformed + /// The polymorph prototype + public void PolymorphEntity(EntityUid target, PolymorphPrototype proto) + { + // mostly just for vehicles + if (TryComp(target, out var buckle)) + buckle.TryUnbuckle(target, true); + + var targetTransformComp = Transform(target); + + var child = Spawn(proto.Entity, targetTransformComp.Coordinates); + MakeSentientCommand.MakeSentient(child, EntityManager); + + var comp = EnsureComp(child); + comp.Parent = target; + comp.Prototype = proto; + RaiseLocalEvent(child, new PolymorphComponentSetupEvent()); + + //Transfers all damage from the original to the new one + if (TryComp(child, out var damageParent) && + _damageable.GetScaledDamage(target, child, out var damage) && + damage != null) + { + _damageable.SetDamage(damageParent, damage); + } + + if (proto.DropInventory) + { + //drops everything in the user's inventory + if (_inventory.TryGetContainerSlotEnumerator(target, out var enumerator)) + { + while (enumerator.MoveNext(out var containerSlot)) + { + containerSlot.EmptyContainer(); + } + } + //drops everything in the user's hands + foreach (var hand in _sharedHands.EnumerateHeld(target)) + { + hand.TryRemoveFromContainer(); + } + } + + if (TryComp(target, out var mind) && mind.Mind != null) + mind.Mind.TransferTo(child); + + //Ensures a map to banish the entity to + EnsurePausesdMap(); + if(PausedMap != null) + targetTransformComp.AttachParent(Transform(PausedMap.Value)); + + _popup.PopupEntity(Loc.GetString("polymorph-popup-generic", ("parent", target), ("child", child)), child, Filter.Pvs(child)); + } + + public void CreatePolymorphAction(string id, EntityUid target) + { + if (!_proto.TryIndex(id, out var polyproto)) + { + _saw.Error("Invalid polymorph prototype"); + return; + } + + if (!TryComp(target, out var polycomp)) + return; + + var entproto = _proto.Index(polyproto.Entity); + + var act = new InstantAction() + { + Event = new PolymorphActionEvent(polyproto), + Name = Loc.GetString("polymorph-self-action-name", ("target", entproto.Name)), + Description = Loc.GetString("polymorph-self-action-description", ("target", entproto.Name)), + Icon = new SpriteSpecifier.EntityPrototype(polyproto.Entity), + ItemIconStyle = ItemActionIconStyle.NoItem, + }; + + if (polycomp.PolymorphActions == null) + polycomp.PolymorphActions = new(); + + polycomp.PolymorphActions.Add(id, act); + _actions.AddAction(target, act, target); + } + + public void RemovePolymorphAction(string id, EntityUid target) + { + if (!_proto.TryIndex(id, out var polyproto)) + return; + if (!TryComp(target, out var comp)) + return; + if (comp.PolymorphActions == null) + return; + + comp.PolymorphActions.TryGetValue(id, out var val); + if (val != null) + _actions.RemoveAction(target, val); + } + } + + /// + /// Used after the polymorphedEntity component has it's data set up. + /// + public sealed class PolymorphComponentSetupEvent : InstantActionEvent { }; + + public sealed class PolymorphActionEvent : InstantActionEvent + { + /// + /// The polymorph prototype containing all the information about + /// the specific polymorph. + /// + public readonly PolymorphPrototype Prototype; + + public PolymorphActionEvent(PolymorphPrototype prototype) + { + Prototype = prototype; + } + }; +} diff --git a/Content.Server/Polymorph/Systems/PolymorphedEntitySystem.cs b/Content.Server/Polymorph/Systems/PolymorphedEntitySystem.cs new file mode 100644 index 0000000000..94ada93c6a --- /dev/null +++ b/Content.Server/Polymorph/Systems/PolymorphedEntitySystem.cs @@ -0,0 +1,102 @@ +using Content.Server.Actions; +using Content.Server.Mind.Components; +using Content.Server.Polymorph.Components; +using Content.Server.Popups; +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Damage; +using Content.Shared.MobState.Components; +using Robust.Shared.Player; + +namespace Content.Server.Polymorph.Systems +{ + public sealed class PolymorphedEntitySystem : EntitySystem + { + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRevertPolymorphActionEvent); + } + + private void OnRevertPolymorphActionEvent(EntityUid uid, PolymorphedEntityComponent component, RevertPolymorphActionEvent args) + { + Revert(uid); + } + + /// + /// Reverts a polymorphed entity back into its original form + /// + /// The entityuid of the entity being reverted + public void Revert(EntityUid uid) + { + if (!TryComp(uid, out var component)) + return; + + var uidXform = Transform(uid); + var parentXform = Transform(component.Parent); + + parentXform.AttachParent(uidXform.ParentUid); + parentXform.Coordinates = uidXform.Coordinates; + + if (TryComp(component.Parent, out var damageParent) && + _damageable.GetScaledDamage(uid, component.Parent, out var damage) && + damage != null) + { + _damageable.SetDamage(damageParent, damage); + } + + if (TryComp(uid, out var mind) && mind.Mind != null) + { + mind.Mind.TransferTo(component.Parent); + } + + _popup.PopupEntity(Loc.GetString("polymorph-revert-popup-generic", ("parent", uid), ("child", component.Parent)), component.Parent, Filter.Pvs(component.Parent)); + QueueDel(uid); + } + + private void OnInit(EntityUid uid, PolymorphedEntityComponent component, PolymorphComponentSetupEvent args) + { + if (component.Prototype.Forced) + return; + + var act = new InstantAction() + { + Event = new RevertPolymorphActionEvent(), + EntityIcon = component.Parent, + Name = Loc.GetString("polymorph-revert-action-name"), + Description = Loc.GetString("polymorph-revert-action-description"), + UseDelay = TimeSpan.FromSeconds(component.Prototype.Delay), + }; + + _actions.AddAction(uid, act, null); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var entity in EntityQuery()) + { + entity.Time += frameTime; + + if(entity.Prototype.Duration != null && entity.Time >= entity.Prototype.Duration) + Revert(entity.Owner); + + if (!TryComp(entity.Owner, out var mob)) + continue; + + if ((entity.Prototype.RevertOnDeath && mob.IsDead()) || + (entity.Prototype.RevertOnCrit && mob.IsCritical())) + Revert(entity.Owner); + } + } + } + + public sealed class RevertPolymorphActionEvent : InstantActionEvent { }; +} diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs index 625fe1b371..966e11fafd 100644 --- a/Content.Shared/Damage/Systems/DamageableSystem.cs +++ b/Content.Shared/Damage/Systems/DamageableSystem.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Shared.Damage.Prototypes; using Content.Shared.FixedPoint; using Content.Shared.Inventory; +using Content.Shared.MobState.Components; using Content.Shared.Radiation.Events; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -234,6 +235,45 @@ namespace Content.Shared.Damage DamageChanged(component, delta); } } + + /// + /// Takes the damage from one entity and scales it relative to the health of another + /// + /// The entity whose damage will be scaled + /// The entity whose health the damage will scale to + /// The newly scaled damage. Can be null + public bool GetScaledDamage(EntityUid ent1, EntityUid ent2, out DamageSpecifier? damage) + { + damage = null; + + if (!TryComp(ent1, out var olddamage)) + return false; + + if (!TryComp(ent1, out var oldstate) || + !TryComp(ent2, out var newstate)) + return false; + + int ent1DeadState = 0; + foreach (var state in oldstate._highestToLowestStates) + { + if (state.Value.IsDead()) + { + ent1DeadState = state.Key; + } + } + + int ent2DeadState = 0; + foreach (var state in newstate._highestToLowestStates) + { + if (state.Value.IsDead()) + { + ent2DeadState = state.Key; + } + } + + damage = (olddamage.Damage / ent1DeadState) * ent2DeadState; + return true; + } } /// diff --git a/Content.Shared/Polymorph/PolymorphPrototype.cs b/Content.Shared/Polymorph/PolymorphPrototype.cs new file mode 100644 index 0000000000..31cdf22933 --- /dev/null +++ b/Content.Shared/Polymorph/PolymorphPrototype.cs @@ -0,0 +1,74 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Polymorph +{ + /// + /// Polymorphs generally describe any type of transformation that can be applied to an entity. + /// + [Prototype("polymorph")] + [DataDefinition] + public sealed class PolymorphPrototype : IPrototype, IInheritingPrototype + { + [ViewVariables] + [IdDataFieldAttribute] + public string ID { get; } = default!; + + [DataField("name")] + public string Name { get; } = string.Empty; + + [ParentDataField(typeof(AbstractPrototypeIdSerializer))] + public string? Parent { get; private set; } + + [NeverPushInheritance] + [AbstractDataFieldAttribute] + public bool Abstract { get; private set; } + + /// + /// What entity the polymorph will turn the target into + /// must be in here because it makes no sense if it isn't + /// + [DataField("entity", required: true, serverOnly: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string Entity = string.Empty; + + /// + /// The delay between the polymorph's uses in seconds + /// Slightly weird as of right now. + /// + [DataField("delay", serverOnly: true)] + public int Delay = 60; + + /// + /// The duration of the transformation in seconds + /// can be null if there is not one + /// + [DataField("duration", serverOnly: true)] + public int? Duration = null; + + /// + /// whether or not the target can transform as will + /// set to true for things like polymorph spells and curses + /// + [DataField("forced", serverOnly: true)] + public bool Forced = false; + + /// + /// Whether or not the target will drop their inventory + /// when they are polymorphed (includes items in hands) + /// + [DataField("dropInventory", serverOnly: true)] + public bool DropInventory = false; + + /// + /// Whether or not the polymorph reverts when the entity goes into crit. + /// + [DataField("revertOnCrit", serverOnly: true)] + public bool RevertOnCrit = true; + + /// + /// Whether or not the polymorph reverts when the entity dies. + /// + [DataField("revertOnDeath", serverOnly: true)] + public bool RevertOnDeath = true; + } +} diff --git a/Resources/Locale/en-US/administration/commands/polymorph-command.ftl b/Resources/Locale/en-US/administration/commands/polymorph-command.ftl new file mode 100644 index 0000000000..54e6fae0c3 --- /dev/null +++ b/Resources/Locale/en-US/administration/commands/polymorph-command.ftl @@ -0,0 +1,8 @@ +polymorph-command-description = For when you need someone to stop being a person. Takes an entity and a polymorph prototype. +polymorph-command-help-text = polymorph