Polymorphs and Transformation (#8185)

This commit is contained in:
EmoGarbage404
2022-05-18 00:05:22 -04:00
committed by GitHub
parent dac8540705
commit 2697bbf8c7
16 changed files with 625 additions and 0 deletions

View File

@@ -58,6 +58,8 @@ namespace Content.Client.Entry
"ResearchClient",
"IdCardConsole",
"MimePowers",
"Polymorphable",
"PolymorphedEntity",
"ThermalRegulator",
"DiseaseMachineRunning",
"DiseaseMachine",

View File

@@ -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<IEntityManager>();
var polySystem = entityManager.EntitySysManager.GetEntitySystem<PolymorphableSystem>();
entityManager.EnsureComponent<PolymorphableComponent>(entityUid);
polySystem.CreatePolymorphAction(args[1], entityUid);
}
}

View File

@@ -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<IPrototypeManager>();
if (!protoManager.TryIndex<PolymorphPrototype>(args[1], out var polyproto))
{
shell.WriteError(Loc.GetString("polymorph-not-valid-prototype-error"));
return;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
var polySystem = entityManager.EntitySysManager.GetEntitySystem<PolymorphableSystem>();
entityManager.EnsureComponent<PolymorphableComponent>(entityUid);
polySystem.PolymorphEntity(entityUid, polyproto);
}
}

View File

@@ -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
{
/// <summary>
/// A list of all the polymorphs that the entity has.
/// Used to manage them and remove them if needed.
/// </summary>
public Dictionary<string, InstantAction>? PolymorphActions = null;
/// <summary>
/// The polymorphs that the entity starts out being able to do.
/// </summary>
[DataField("innatePolymorphs", customTypeSerializer : typeof(PrototypeIdListSerializer<PolymorphPrototype>))]
public List<string>? InnatePolymorphs = null;
}
}

View File

@@ -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
{
/// <summary>
/// The polymorph prototype, used to track various information
/// about the polymorph
/// </summary>
public PolymorphPrototype Prototype = default!;
/// <summary>
/// The original entity that the player will revert back into
/// </summary>
[DataField("parent", required: true)]
public EntityUid Parent = new();
/// <summary>
/// The amount of time that has passed since the entity was created
/// used for tracking the duration
/// </summary>
[DataField("time")]
public float Time = 0;
}
}

View File

@@ -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;
/// <summary>
/// Used to subscribe to the round restart event
/// </summary>
private void InitializeMap()
{
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
}
private void OnRoundRestart(RoundRestartCleanupEvent _)
{
if (PausedMap == null || !Exists(PausedMap))
return;
EntityManager.DeleteEntity(PausedMap.Value);
}
/// <summary>
/// Used internally to ensure a paused map that is
/// stores polymorphed entities.
/// </summary>
private void EnsurePausesdMap()
{
if (PausedMap != null && Exists(PausedMap))
return;
var newmap = _mapManager.CreateMap();
_mapManager.SetMapPaused(newmap, true);
PausedMap = _mapManager.GetMapEntityId(newmap);
Dirty(PausedMap.Value);
}
}
}

View File

@@ -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<PolymorphableComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<PolymorphableComponent, PolymorphActionEvent>(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);
}
/// <summary>
/// Polymorphs the target entity into the specific polymorph prototype
/// </summary>
/// <param name="target">The entity that will be transformed</param>
/// <param name="id">The id of the polymorph prototype</param>
public void PolymorphEntity(EntityUid target, String id)
{
if (!_proto.TryIndex<PolymorphPrototype>(id, out var proto))
{
_saw.Error("Invalid polymorph prototype");
return;
}
PolymorphEntity(target, proto);
}
/// <summary>
/// Polymorphs the target entity into the specific polymorph prototype
/// </summary>
/// <param name="target">The entity that will be transformed</param>
/// <param name="proto">The polymorph prototype</param>
public void PolymorphEntity(EntityUid target, PolymorphPrototype proto)
{
// mostly just for vehicles
if (TryComp<BuckleComponent>(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<PolymorphedEntityComponent>(child);
comp.Parent = target;
comp.Prototype = proto;
RaiseLocalEvent(child, new PolymorphComponentSetupEvent());
//Transfers all damage from the original to the new one
if (TryComp<DamageableComponent>(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<MindComponent>(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<PolymorphPrototype>(id, out var polyproto))
{
_saw.Error("Invalid polymorph prototype");
return;
}
if (!TryComp<PolymorphableComponent>(target, out var polycomp))
return;
var entproto = _proto.Index<EntityPrototype>(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<PolymorphPrototype>(id, out var polyproto))
return;
if (!TryComp<PolymorphableComponent>(target, out var comp))
return;
if (comp.PolymorphActions == null)
return;
comp.PolymorphActions.TryGetValue(id, out var val);
if (val != null)
_actions.RemoveAction(target, val);
}
}
/// <summary>
/// Used after the polymorphedEntity component has it's data set up.
/// </summary>
public sealed class PolymorphComponentSetupEvent : InstantActionEvent { };
public sealed class PolymorphActionEvent : InstantActionEvent
{
/// <summary>
/// The polymorph prototype containing all the information about
/// the specific polymorph.
/// </summary>
public readonly PolymorphPrototype Prototype;
public PolymorphActionEvent(PolymorphPrototype prototype)
{
Prototype = prototype;
}
};
}

View File

@@ -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<PolymorphedEntityComponent, PolymorphComponentSetupEvent>(OnInit);
SubscribeLocalEvent<PolymorphedEntityComponent, RevertPolymorphActionEvent>(OnRevertPolymorphActionEvent);
}
private void OnRevertPolymorphActionEvent(EntityUid uid, PolymorphedEntityComponent component, RevertPolymorphActionEvent args)
{
Revert(uid);
}
/// <summary>
/// Reverts a polymorphed entity back into its original form
/// </summary>
/// <param name="uid">The entityuid of the entity being reverted</param>
public void Revert(EntityUid uid)
{
if (!TryComp<PolymorphedEntityComponent>(uid, out var component))
return;
var uidXform = Transform(uid);
var parentXform = Transform(component.Parent);
parentXform.AttachParent(uidXform.ParentUid);
parentXform.Coordinates = uidXform.Coordinates;
if (TryComp<DamageableComponent>(component.Parent, out var damageParent) &&
_damageable.GetScaledDamage(uid, component.Parent, out var damage) &&
damage != null)
{
_damageable.SetDamage(damageParent, damage);
}
if (TryComp<MindComponent>(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<PolymorphedEntityComponent>())
{
entity.Time += frameTime;
if(entity.Prototype.Duration != null && entity.Time >= entity.Prototype.Duration)
Revert(entity.Owner);
if (!TryComp<MobStateComponent>(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 { };
}

View File

@@ -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);
}
}
/// <summary>
/// Takes the damage from one entity and scales it relative to the health of another
/// </summary>
/// <param name="ent1">The entity whose damage will be scaled</param>
/// <param name="ent2">The entity whose health the damage will scale to</param>
/// <param name="damage">The newly scaled damage. Can be null</param>
public bool GetScaledDamage(EntityUid ent1, EntityUid ent2, out DamageSpecifier? damage)
{
damage = null;
if (!TryComp<DamageableComponent>(ent1, out var olddamage))
return false;
if (!TryComp<MobStateComponent>(ent1, out var oldstate) ||
!TryComp<MobStateComponent>(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;
}
}
/// <summary>

View File

@@ -0,0 +1,74 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Polymorph
{
/// <summary>
/// Polymorphs generally describe any type of transformation that can be applied to an entity.
/// </summary>
[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<PolymorphPrototype>))]
public string? Parent { get; private set; }
[NeverPushInheritance]
[AbstractDataFieldAttribute]
public bool Abstract { get; private set; }
/// <summary>
/// What entity the polymorph will turn the target into
/// must be in here because it makes no sense if it isn't
/// </summary>
[DataField("entity", required: true, serverOnly: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Entity = string.Empty;
/// <summary>
/// The delay between the polymorph's uses in seconds
/// Slightly weird as of right now.
/// </summary>
[DataField("delay", serverOnly: true)]
public int Delay = 60;
/// <summary>
/// The duration of the transformation in seconds
/// can be null if there is not one
/// </summary>
[DataField("duration", serverOnly: true)]
public int? Duration = null;
/// <summary>
/// whether or not the target can transform as will
/// set to true for things like polymorph spells and curses
/// </summary>
[DataField("forced", serverOnly: true)]
public bool Forced = false;
/// <summary>
/// Whether or not the target will drop their inventory
/// when they are polymorphed (includes items in hands)
/// </summary>
[DataField("dropInventory", serverOnly: true)]
public bool DropInventory = false;
/// <summary>
/// Whether or not the polymorph reverts when the entity goes into crit.
/// </summary>
[DataField("revertOnCrit", serverOnly: true)]
public bool RevertOnCrit = true;
/// <summary>
/// Whether or not the polymorph reverts when the entity dies.
/// </summary>
[DataField("revertOnDeath", serverOnly: true)]
public bool RevertOnDeath = true;
}
}

View File

@@ -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 <id> <polymorph prototype
add-polymorph-action-command-description = Takes an entity and gives them a voluntary polymorph.
add-polymorph-action-command-help-text = addpolymorphaction <id> <polymorph prototype
polymorph-not-valid-prototype-error = Polymorph prototype is not valid.

View File

@@ -0,0 +1,8 @@
polymorph-self-action-name = Polymorph ({CAPITALIZE($target)})
polymorph-self-action-description = Instantly polymorph yourself into {$target}.
polymorph-revert-action-name = Revert
polymorph-revert-action-description = Revert back into your original form.
polymorph-popup-generic = {CAPITALIZE($parent)} turned into {$child}.
polymorph-revert-popup-generic = {CAPITALIZE($parent)} reverted back into {$child}.

View File

@@ -123,6 +123,7 @@
- type: DoAfter
- type: Climbing
- type: Flashable
- type: Polymorphable
- type: Pullable
- type: Buckle
- type: Recyclable

View File

@@ -22,6 +22,7 @@
effects:
- !type:WashCreamPieReaction
- type: Flashable
- type: Polymorphable
- type: Hands
- type: MovementSpeedModifier
- type: MovedByPressure

View File

@@ -21,6 +21,7 @@
effects:
- !type:WashCreamPieReaction
- type: Flashable
- type: Polymorphable
- type: Hands
- type: MovementSpeedModifier
- type: MovedByPressure

View File

@@ -0,0 +1,19 @@
- type: polymorph
id: Mouse
entity: MobMouse
forced: true
duration: 30
- type: polymorph
id: Chicken
entity: MobChicken
forced: true
dropInventory: true
- type: polymorph
id: Monkey
entity: MobMonkey
forced: true
dropInventory: true
revertOnCrit: true
revertOnDeath: true