419 lines
16 KiB
C#
419 lines
16 KiB
C#
using Content.Server.Actions;
|
|
using Content.Server.Humanoid;
|
|
using Content.Server.Inventory;
|
|
using Content.Server.Mind;
|
|
using Content.Server.Mind.Commands;
|
|
using Content.Server.Mind.Components;
|
|
using Content.Server.Nutrition;
|
|
using Content.Server.Polymorph.Components;
|
|
using Content.Shared.Actions;
|
|
using Content.Shared.Actions.ActionTypes;
|
|
using Content.Shared.Buckle;
|
|
using Content.Shared.Damage;
|
|
using Content.Shared.Hands.EntitySystems;
|
|
using Content.Shared.IdentityManagement;
|
|
using Content.Shared.Mobs.Components;
|
|
using Content.Shared.Mobs.Systems;
|
|
using Content.Shared.Polymorph;
|
|
using Content.Shared.Popups;
|
|
using JetBrains.Annotations;
|
|
using Robust.Server.Containers;
|
|
using Robust.Server.GameObjects;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Server.Polymorph.Systems
|
|
{
|
|
public sealed partial class PolymorphSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly IComponentFactory _compFact = default!;
|
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
|
[Dependency] private readonly IPrototypeManager _proto = default!;
|
|
[Dependency] private readonly ActionsSystem _actions = default!;
|
|
[Dependency] private readonly AudioSystem _audio = default!;
|
|
[Dependency] private readonly SharedBuckleSystem _buckle = default!;
|
|
[Dependency] private readonly ContainerSystem _container = default!;
|
|
[Dependency] private readonly DamageableSystem _damageable = default!;
|
|
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
|
|
[Dependency] private readonly MobStateSystem _mobState = default!;
|
|
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
|
|
[Dependency] private readonly ServerInventorySystem _inventory = default!;
|
|
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
|
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
|
[Dependency] private readonly TransformSystem _transform = default!;
|
|
[Dependency] private readonly MindSystem _mindSystem = default!;
|
|
|
|
private ISawmill _sawmill = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
SubscribeLocalEvent<PolymorphableComponent, ComponentStartup>(OnStartup);
|
|
SubscribeLocalEvent<PolymorphableComponent, PolymorphActionEvent>(OnPolymorphActionEvent);
|
|
SubscribeLocalEvent<PolymorphedEntityComponent, ComponentStartup>(OnStartup);
|
|
SubscribeLocalEvent<PolymorphedEntityComponent, BeforeFullyEatenEvent>(OnBeforeFullyEaten);
|
|
SubscribeLocalEvent<PolymorphedEntityComponent, BeforeFullySlicedEvent>(OnBeforeFullySliced);
|
|
SubscribeLocalEvent<PolymorphedEntityComponent, RevertPolymorphActionEvent>(OnRevertPolymorphActionEvent);
|
|
|
|
InitializeCollide();
|
|
InitializeMap();
|
|
|
|
_sawmill = Logger.GetSawmill("polymorph");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private void OnRevertPolymorphActionEvent(EntityUid uid, PolymorphedEntityComponent component, RevertPolymorphActionEvent args)
|
|
{
|
|
Revert(uid, component);
|
|
}
|
|
|
|
public void OnStartup(EntityUid uid, PolymorphedEntityComponent component, ComponentStartup args)
|
|
{
|
|
if (!_proto.TryIndex(component.Prototype, out PolymorphPrototype? proto))
|
|
{
|
|
// warning instead of error because of the all-comps one entity test.
|
|
_sawmill.Warning($"{nameof(PolymorphSystem)} encountered an improperly set up polymorph component while initializing. Entity {ToPrettyString(uid)}. Prototype: {component.Prototype}");
|
|
RemCompDeferred(uid, component);
|
|
return;
|
|
}
|
|
|
|
if (proto.Forced)
|
|
return;
|
|
|
|
var act = new InstantAction
|
|
{
|
|
Event = new RevertPolymorphActionEvent(),
|
|
EntityIcon = component.Parent,
|
|
DisplayName = Loc.GetString("polymorph-revert-action-name"),
|
|
Description = Loc.GetString("polymorph-revert-action-description"),
|
|
UseDelay = TimeSpan.FromSeconds(proto.Delay),
|
|
};
|
|
|
|
_actions.AddAction(uid, act, null);
|
|
}
|
|
|
|
private void OnBeforeFullyEaten(EntityUid uid, PolymorphedEntityComponent comp, BeforeFullyEatenEvent args)
|
|
{
|
|
if (!_proto.TryIndex<PolymorphPrototype>(comp.Prototype, out var proto))
|
|
{
|
|
_sawmill.Error("Invalid polymorph prototype {comp.Prototype}");
|
|
return;
|
|
}
|
|
|
|
if (proto.RevertOnEat)
|
|
{
|
|
args.Cancel();
|
|
Revert(uid, comp);
|
|
}
|
|
}
|
|
|
|
private void OnBeforeFullySliced(EntityUid uid, PolymorphedEntityComponent comp, BeforeFullySlicedEvent args)
|
|
{
|
|
if (!_proto.TryIndex<PolymorphPrototype>(comp.Prototype, out var proto))
|
|
{
|
|
_sawmill.Error("Invalid polymorph prototype {comp.Prototype}");
|
|
return;
|
|
}
|
|
|
|
if (proto.RevertOnEat)
|
|
{
|
|
args.Cancel();
|
|
Revert(uid, comp);
|
|
}
|
|
}
|
|
|
|
/// <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 EntityUid? PolymorphEntity(EntityUid target, string id)
|
|
{
|
|
if (!_proto.TryIndex<PolymorphPrototype>(id, out var proto))
|
|
{
|
|
_sawmill.Error("Invalid polymorph prototype {id}");
|
|
return null;
|
|
}
|
|
|
|
return PolymorphEntity(target, proto);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Polymorphs the target entity into the specific polymorph prototype
|
|
/// </summary>
|
|
/// <param name="uid">The entity that will be transformed</param>
|
|
/// <param name="proto">The polymorph prototype</param>
|
|
public EntityUid? PolymorphEntity(EntityUid uid, PolymorphPrototype proto)
|
|
{
|
|
// if it's already morphed, don't allow it again with this condition active.
|
|
if (!proto.AllowRepeatedMorphs && HasComp<PolymorphedEntityComponent>(uid))
|
|
return null;
|
|
|
|
// mostly just for vehicles
|
|
_buckle.TryUnbuckle(uid, uid, true);
|
|
|
|
var targetTransformComp = Transform(uid);
|
|
|
|
var child = Spawn(proto.Entity, targetTransformComp.Coordinates);
|
|
MakeSentientCommand.MakeSentient(child, EntityManager);
|
|
|
|
var comp = _compFact.GetComponent<PolymorphedEntityComponent>();
|
|
comp.Owner = child;
|
|
comp.Parent = uid;
|
|
comp.Prototype = proto.ID;
|
|
EntityManager.AddComponent(child, comp);
|
|
|
|
var childXform = Transform(child);
|
|
childXform.LocalRotation = targetTransformComp.LocalRotation;
|
|
|
|
if (_container.TryGetContainingContainer(uid, out var cont))
|
|
cont.Insert(child);
|
|
|
|
//Transfers all damage from the original to the new one
|
|
if (proto.TransferDamage &&
|
|
TryComp<DamageableComponent>(child, out var damageParent) &&
|
|
_mobThreshold.GetScaledDamage(uid, child, out var damage) &&
|
|
damage != null)
|
|
{
|
|
_damageable.SetDamage(child, damageParent, damage);
|
|
}
|
|
|
|
if (proto.Inventory == PolymorphInventoryChange.Transfer)
|
|
{
|
|
_inventory.TransferEntityInventories(uid, child);
|
|
foreach (var hand in _hands.EnumerateHeld(uid))
|
|
{
|
|
_hands.TryDrop(uid, hand, checkActionBlocker: false);
|
|
_hands.TryPickupAnyHand(child, hand);
|
|
}
|
|
}
|
|
else if (proto.Inventory == PolymorphInventoryChange.Drop)
|
|
{
|
|
if (_inventory.TryGetContainerSlotEnumerator(uid, out var enumerator))
|
|
{
|
|
while (enumerator.MoveNext(out var slot))
|
|
{
|
|
_inventory.TryUnequip(uid, slot.ID, true, true);
|
|
}
|
|
}
|
|
|
|
foreach (var held in _hands.EnumerateHeld(uid))
|
|
{
|
|
_hands.TryDrop(uid, held);
|
|
}
|
|
}
|
|
|
|
if (proto.TransferName &&
|
|
TryComp<MetaDataComponent>(uid, out var targetMeta) &&
|
|
TryComp<MetaDataComponent>(child, out var childMeta))
|
|
{
|
|
childMeta.EntityName = targetMeta.EntityName;
|
|
}
|
|
|
|
if (proto.TransferHumanoidAppearance)
|
|
{
|
|
_humanoid.CloneAppearance(uid, child);
|
|
}
|
|
|
|
if (_mindSystem.TryGetMind(uid, out var mind))
|
|
_mindSystem.TransferTo(mind, child);
|
|
|
|
//Ensures a map to banish the entity to
|
|
EnsurePausesdMap();
|
|
if (PausedMap != null)
|
|
_transform.SetParent(uid, targetTransformComp, PausedMap.Value);
|
|
|
|
return child;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reverts a polymorphed entity back into its original form
|
|
/// </summary>
|
|
/// <param name="uid">The entityuid of the entity being reverted</param>
|
|
/// <param name="component"></param>
|
|
public void Revert(EntityUid uid, PolymorphedEntityComponent? component = null)
|
|
{
|
|
if (Deleted(uid))
|
|
return;
|
|
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
var parent = component.Parent;
|
|
if (Deleted(parent))
|
|
return;
|
|
|
|
if (!_proto.TryIndex(component.Prototype, out PolymorphPrototype? proto))
|
|
{
|
|
_sawmill.Error($"{nameof(PolymorphSystem)} encountered an improperly initialized polymorph component while reverting. Entity {ToPrettyString(uid)}. Prototype: {component.Prototype}");
|
|
return;
|
|
}
|
|
|
|
var uidXform = Transform(uid);
|
|
var parentXform = Transform(parent);
|
|
|
|
_transform.SetParent(parent, parentXform, uidXform.ParentUid);
|
|
parentXform.Coordinates = uidXform.Coordinates;
|
|
parentXform.LocalRotation = uidXform.LocalRotation;
|
|
|
|
if (proto.TransferDamage &&
|
|
TryComp<DamageableComponent>(parent, out var damageParent) &&
|
|
_mobThreshold.GetScaledDamage(uid, parent, out var damage) &&
|
|
damage != null)
|
|
{
|
|
_damageable.SetDamage(parent, damageParent, damage);
|
|
}
|
|
|
|
if (proto.Inventory == PolymorphInventoryChange.Transfer)
|
|
{
|
|
_inventory.TransferEntityInventories(uid, parent);
|
|
foreach (var held in _hands.EnumerateHeld(uid))
|
|
{
|
|
_hands.TryDrop(uid, held);
|
|
_hands.TryPickupAnyHand(parent, held, checkActionBlocker: false);
|
|
}
|
|
}
|
|
else if (proto.Inventory == PolymorphInventoryChange.Drop)
|
|
{
|
|
if (_inventory.TryGetContainerSlotEnumerator(uid, out var enumerator))
|
|
{
|
|
while (enumerator.MoveNext(out var slot))
|
|
{
|
|
_inventory.TryUnequip(uid, slot.ID);
|
|
}
|
|
}
|
|
|
|
foreach (var held in _hands.EnumerateHeld(uid))
|
|
{
|
|
_hands.TryDrop(uid, held);
|
|
}
|
|
}
|
|
|
|
if (_mindSystem.TryGetMind(uid, out var mind))
|
|
_mindSystem.TransferTo(mind, parent);
|
|
|
|
// if an item polymorph was picked up, put it back down after reverting
|
|
Transform(parent).AttachToGridOrMap();
|
|
|
|
_popup.PopupEntity(Loc.GetString("polymorph-revert-popup-generic",
|
|
("parent", Identity.Entity(uid, EntityManager)),
|
|
("child", Identity.Entity(parent, EntityManager))),
|
|
parent);
|
|
QueueDel(uid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a sidebar action for an entity to be able to polymorph at will
|
|
/// </summary>
|
|
/// <param name="id">The string of the id of the polymorph action</param>
|
|
/// <param name="target">The entity that will be gaining the action</param>
|
|
public void CreatePolymorphAction(string id, EntityUid target)
|
|
{
|
|
if (!_proto.TryIndex<PolymorphPrototype>(id, out var polyproto))
|
|
{
|
|
_sawmill.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
|
|
{
|
|
Prototype = polyproto,
|
|
},
|
|
DisplayName = 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,
|
|
};
|
|
|
|
polycomp.PolymorphActions ??= new();
|
|
|
|
polycomp.PolymorphActions.Add(id, act);
|
|
_actions.AddAction(target, act, target);
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void RemovePolymorphAction(string id, EntityUid target, PolymorphableComponent? component = null)
|
|
{
|
|
if (!Resolve(target, ref component, false))
|
|
return;
|
|
if (component.PolymorphActions == null)
|
|
return;
|
|
if (component.PolymorphActions.TryGetValue(id, out var val))
|
|
_actions.RemoveAction(target, val);
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
var query = EntityQueryEnumerator<PolymorphedEntityComponent>();
|
|
while (query.MoveNext(out var uid, out var comp))
|
|
{
|
|
comp.Time += frameTime;
|
|
|
|
if (!_proto.TryIndex(comp.Prototype, out PolymorphPrototype? proto))
|
|
{
|
|
_sawmill.Error($"{nameof(PolymorphSystem)} encountered an improperly initialized polymorph component while updating. Entity {ToPrettyString(uid)}. Prototype: {comp.Prototype}");
|
|
RemCompDeferred(uid, comp);
|
|
continue;
|
|
}
|
|
|
|
if (proto.Duration != null && comp.Time >= proto.Duration)
|
|
{
|
|
Revert(uid, comp);
|
|
continue;
|
|
}
|
|
|
|
if (!TryComp<MobStateComponent>(uid, out var mob))
|
|
continue;
|
|
|
|
if (proto.RevertOnDeath && _mobState.IsDead(uid, mob) ||
|
|
proto.RevertOnCrit && _mobState.IsIncapacitated(uid, mob))
|
|
{
|
|
Revert(uid, comp);
|
|
}
|
|
}
|
|
|
|
UpdateCollide();
|
|
}
|
|
}
|
|
|
|
public sealed class PolymorphActionEvent : InstantActionEvent
|
|
{
|
|
/// <summary>
|
|
/// The polymorph prototype containing all the information about
|
|
/// the specific polymorph.
|
|
/// </summary>
|
|
public PolymorphPrototype Prototype = default!;
|
|
}
|
|
|
|
public sealed class RevertPolymorphActionEvent : InstantActionEvent
|
|
{
|
|
|
|
}
|
|
}
|