CatStation/Content.Server/Polymorph/Systems/PolymorphSystem.cs
kosticia d9e523370a
Unite LogComponent, FoodSlicableComponent and partially ButcherableComponent into one. Sharp component removal. (#36895)
* Yay

* Do a LOT changes in yml

* and again

* Finale changes

* yml and locale

* locale tweak

* yml and charp cleanup

* fix carpets

* and some-some

* And some

* I want to believe that its last🙏🙏

* 😭

* 😭😭

* clean

* fix

* AAHHAHAHAHAHAH

* fix

* FINAL

* thisguyusa...

* LAST_ONE

* wawa

* merge2

* sdobfjibwdsjaqaksmzxsowaw2edqw84

* aoaoaooaoaao

* this hardcode took 30 minutes of my life

* fix aaaa

* fix test

* FIX BUILD

* oops

* oops

* aaooaoaoa

* fuck

* oops

* refactor: cleanup after merge + use protoId in KitchenSpikeComponent

* refactor: fix butcherable stuff again!~

* fix: now actively checks for EdibleComponent on slicing, predicted items splitting

* fix: changed knife.yaml to use Slicing tool

* fix: no more sounds spam on chop-chop

* refactor: cleanups

* refactor: default time value for slicable

* refactor: whitespaces and extract variables

* fix: slicable no longer prevents InteractUsingEvent by marking handled w/o actually handling

* feat: chopping trees now requires sawing tool, same for slicing logs

* refactor: cleanup after merge

* refactor: fix non-related PR changes

* refactor: rename locid

* refactor: remove unneeded red 'slice' verb name when slicing entities with mob-state.

* refactor: simplify test code changes

* refactor: better readability for test

* refactor: linebreak for unary op

* refactor: xml-doc for SliceableComponent corrected to be more fitting.

* refactor: cleanup SliceableSystem some more

* refactor: use HasComp instead of TryComp in AddSliceVerb

* refactor: extracted methods to improve readability

* refactor: remove unchecked operator - not needed when casting from uint

* refactor: SliceableSystem cleanups, fix logs not being chopped due to solution splitting

* fix: slice verb is now showing up even if disabled, slicing is done even if there is no solution to split, replaced direct dependency for sliceable component to food component with default solution-name

* refactor: comment about RandomPredicted + toy knife now fits into clown shoes

* refactor: remove EdibleComponent dependency from SliceableSystem

* refactor: moved SliceableSystem into ToolRefinableSystem

* refactor: basic renaming for comps

* refactor: cleanup after merge

* refactor: reorder methods, renaming, simplify polymorph related conditions on slicing

* refactor: xml-doc + reorder yaml fields

* refactor: fixing yaml + method renaming, unused usings removed

* refactor: removed unused usings

* refactor: fix typo

* refactor: remove reference for deleted meat proto

* refactor: clean up yaml after merge

* refactor: fix multiple typos from mass replace

* refactor: fix missed field renaming

* refactor: ensure all entities that had ToolRefinable got BaseSlicingRefinable (aftermerge cleanup)

* refactor: undo solution-related changes for animals.yml

* refactor: fix invalid parent set for corgi + fix amount of slices for the rest of pies

* refactor: revert Solution to SolutionContainerManager changes after merge

* refactor: revert Solution to SolutionContainerManager changes after merge

* refactor: fix invalid property name for toolMissingQualityTooltip in yml, fixed missing entity on which AttemptToolRefineEvent is raised

* refactor: remove over-indent for components of BaseSlicingRefinable and for Slicing tool quality

* refactor: remove over-indentation on MobCarpHolo

* empty collection

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: Fildrance <fildrance@gmail.com>
Co-authored-by: iaada <iaada@users.noreply.github.com>
2026-05-28 20:47:49 +00:00

423 lines
16 KiB
C#

using Content.Server.Actions;
using Content.Server.Inventory;
using Content.Server.Polymorph.Components;
using Content.Shared.Body;
using Content.Shared.Buckle;
using Content.Shared.Coordinates;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Destructible;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Mind;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Polymorph;
using Content.Shared.Popups;
using Content.Shared.Tools.Systems;
using Robust.Server.Audio;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Polymorph.Systems;
public sealed partial class PolymorphSystem : EntitySystem
{
[Dependency] private SharedMapSystem _map = default!;
[Dependency] private IPrototypeManager _proto = default!;
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private ActionsSystem _actions = default!;
[Dependency] private AudioSystem _audio = default!;
[Dependency] private SharedBuckleSystem _buckle = default!;
[Dependency] private ContainerSystem _container = default!;
[Dependency] private DamageableSystem _damageable = default!;
[Dependency] private MobStateSystem _mobState = default!;
[Dependency] private MobThresholdSystem _mobThreshold = default!;
[Dependency] private ServerInventorySystem _inventory = default!;
[Dependency] private SharedHandsSystem _hands = default!;
[Dependency] private SharedPopupSystem _popup = default!;
[Dependency] private TransformSystem _transform = default!;
[Dependency] private SharedVisualBodySystem _visualBody = default!;
[Dependency] private SharedMindSystem _mindSystem = default!;
[Dependency] private MetaDataSystem _metaData = default!;
private const string RevertPolymorphId = "ActionRevertPolymorph";
public override void Initialize()
{
SubscribeLocalEvent<PolymorphableComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<PolymorphedEntityComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PolymorphableComponent, PolymorphActionEvent>(OnPolymorphActionEvent);
SubscribeLocalEvent<PolymorphedEntityComponent, RevertPolymorphActionEvent>(OnRevertPolymorphActionEvent);
SubscribeLocalEvent<PolymorphedEntityComponent, BeforeToolRefinedEvent>(OnBeforeToolRefined);
SubscribeLocalEvent<PolymorphedEntityComponent, DestructionEventArgs>(OnDestruction);
SubscribeLocalEvent<PolymorphedEntityComponent, EntityTerminatingEvent>(OnPolymorphedTerminating);
InitializeMap();
}
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 (comp.Configuration.Duration != null && comp.Time >= comp.Configuration.Duration)
{
Revert((uid, comp));
continue;
}
if (!TryComp<MobStateComponent>(uid, out var mob))
continue;
if (comp.Configuration.RevertOnDeath && _mobState.IsDead(uid, mob) ||
comp.Configuration.RevertOnCrit && _mobState.IsIncapacitated(uid, mob))
{
Revert((uid, comp));
}
}
}
private void OnComponentStartup(Entity<PolymorphableComponent> ent, ref ComponentStartup args)
{
if (ent.Comp.InnatePolymorphs != null)
{
foreach (var morph in ent.Comp.InnatePolymorphs)
{
CreatePolymorphAction(morph, ent);
}
}
}
private void OnMapInit(Entity<PolymorphedEntityComponent> ent, ref MapInitEvent args)
{
var (uid, component) = ent;
if (component.Configuration.Forced)
return;
if (_actions.AddAction(uid, ref component.Action, out var action, RevertPolymorphId))
{
_actions.SetEntityIcon((component.Action.Value, action), component.Parent);
_actions.SetUseDelay(component.Action.Value, TimeSpan.FromSeconds(component.Configuration.Delay));
}
}
private void OnPolymorphActionEvent(Entity<PolymorphableComponent> ent, ref PolymorphActionEvent args)
{
if (!_proto.Resolve(args.ProtoId, out var prototype) || args.Handled)
return;
PolymorphEntity(ent, prototype.Configuration);
args.Handled = true;
}
private void OnRevertPolymorphActionEvent(Entity<PolymorphedEntityComponent> ent,
ref RevertPolymorphActionEvent args)
{
Revert((ent, ent));
}
private void OnBeforeToolRefined(Entity<PolymorphedEntityComponent> ent, ref BeforeToolRefinedEvent args)
{
if (ent.Comp.Reverted || !ent.Comp.Configuration.RevertOnEat)
return;
args.Cancelled = true;
Revert((ent, ent));
}
/// <summary>
/// It is possible to be polymorphed into an entity that can't "die", but is instead
/// destroyed. This handler ensures that destruction is treated like death.
/// </summary>
private void OnDestruction(Entity<PolymorphedEntityComponent> ent, ref DestructionEventArgs args)
{
if (ent.Comp.Reverted || !ent.Comp.Configuration.RevertOnDeath)
return;
Revert((ent, ent));
}
private void OnPolymorphedTerminating(Entity<PolymorphedEntityComponent> ent, ref EntityTerminatingEvent args)
{
if (ent.Comp.Reverted)
return;
if (ent.Comp.Configuration.RevertOnDelete)
Revert(ent.AsNullable());
// Remove our original entity too
// Note that Revert will set Parent to null, so reverted entities will not be deleted
QueueDel(ent.Comp.Parent);
}
/// <summary>
/// Polymorphs the target entity into the specific polymorph prototype
/// </summary>
/// <param name="uid">The entity that will be transformed</param>
/// <param name="protoId">The id of the polymorph prototype</param>
public EntityUid? PolymorphEntity(EntityUid uid, ProtoId<PolymorphPrototype> protoId)
{
var config = _proto.Index(protoId).Configuration;
return PolymorphEntity(uid, config);
}
/// <summary>
/// Polymorphs the target entity into another.
/// </summary>
/// <param name="uid">The entity that will be transformed</param>
/// <param name="configuration">The new polymorph configuration</param>
/// <returns>The new entity, or null if the polymorph failed.</returns>
public EntityUid? PolymorphEntity(EntityUid uid, PolymorphConfiguration configuration)
{
// If they're morphed, check their current config to see if they can be
// morphed again
if (!configuration.IgnoreAllowRepeatedMorphs
&& TryComp<PolymorphedEntityComponent>(uid, out var currentPoly)
&& !currentPoly.Configuration.AllowRepeatedMorphs)
return null;
// If this polymorph has a cooldown, check if that amount of time has passed since the
// last polymorph ended.
if (TryComp<PolymorphableComponent>(uid, out var polymorphableComponent) &&
polymorphableComponent.LastPolymorphEnd != null &&
_gameTiming.CurTime < polymorphableComponent.LastPolymorphEnd + configuration.Cooldown)
return null;
// mostly just for vehicles
_buckle.TryUnbuckle(uid, uid, true);
var targetTransformComp = Transform(uid);
if (configuration.PolymorphSound != null)
_audio.PlayPvs(configuration.PolymorphSound, targetTransformComp.Coordinates);
var child = Spawn(configuration.Entity, _transform.GetMapCoordinates(uid, targetTransformComp), rotation: _transform.GetWorldRotation(uid));
if (configuration.PolymorphPopup != null)
_popup.PopupEntity(Loc.GetString(configuration.PolymorphPopup,
("parent", Identity.Entity(uid, EntityManager)),
("child", Identity.Entity(child, EntityManager))),
child);
_mindSystem.MakeSentient(child);
var polymorphedComp = Factory.GetComponent<PolymorphedEntityComponent>();
polymorphedComp.Parent = uid;
polymorphedComp.Configuration = configuration;
AddComp(child, polymorphedComp);
var childXform = Transform(child);
_transform.SetLocalRotation(child, targetTransformComp.LocalRotation, childXform);
if (_container.TryGetContainingContainer((uid, targetTransformComp, null), out var cont))
_container.Insert(child, cont);
//Transfers all damage from the original to the new one
if (configuration.TransferDamage &&
TryComp<DamageableComponent>(child, out var damageParent) &&
_mobThreshold.GetScaledDamage(uid, child, out var damage) &&
damage != null)
{
_damageable.SetDamage((child, damageParent), damage);
}
if (configuration.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 (configuration.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 (configuration.TransferName && TryComp(uid, out MetaDataComponent? targetMeta))
_metaData.SetEntityName(child, targetMeta.EntityName);
if (configuration.TransferHumanoidAppearance)
{
_visualBody.CopyAppearanceFrom(uid, child);
}
if (_mindSystem.TryGetMind(uid, out var mindId, out var mind))
_mindSystem.TransferTo(mindId, child, mind: mind);
//Ensures a map to banish the entity to
EnsurePausedMap();
if (PausedMap != null)
_transform.SetParent(uid, targetTransformComp, PausedMap.Value);
// Raise an event to inform anything that wants to know about the entity swap
var ev = new PolymorphedEvent(uid, child, false);
RaiseLocalEvent(uid, ref ev);
// visual effect spawn
if (configuration.EffectProto != null)
SpawnAttachedTo(configuration.EffectProto, child.ToCoordinates());
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 EntityUid? Revert(Entity<PolymorphedEntityComponent?> ent)
{
var (uid, component) = ent;
if (!Resolve(ent, ref component))
return null;
if (Deleted(uid))
return null;
if (component.Parent is not { } parent)
return null;
if (Deleted(parent))
return null;
var uidXform = Transform(uid);
var parentXform = Transform(parent);
// Don't swap back onto a terminating grid
if (TerminatingOrDeleted(uidXform.ParentUid))
return null;
if (component.Configuration.ExitPolymorphSound != null)
_audio.PlayPvs(component.Configuration.ExitPolymorphSound, uidXform.Coordinates);
_transform.SetParent(parent, parentXform, uidXform.ParentUid);
_transform.SetCoordinates(parent, parentXform, uidXform.Coordinates, uidXform.LocalRotation);
component.Reverted = true;
if (component.Configuration.TransferDamage &&
TryComp<DamageableComponent>(parent, out var damageParent) &&
_mobThreshold.GetScaledDamage(uid, parent, out var damage) &&
damage != null)
{
_damageable.SetDamage((parent, damageParent), damage);
}
if (component.Configuration.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 (_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 mindId, out var mind))
_mindSystem.TransferTo(mindId, parent, mind: mind);
if (TryComp<PolymorphableComponent>(parent, out var polymorphableComponent))
polymorphableComponent.LastPolymorphEnd = _gameTiming.CurTime;
// if an item polymorph was picked up, put it back down after reverting
_transform.AttachToGridOrMap(parent, parentXform);
// Raise an event to inform anything that wants to know about the entity swap
var ev = new PolymorphedEvent(uid, parent, true);
RaiseLocalEvent(uid, ref ev);
// visual effect spawn
if (component.Configuration.EffectProto != null)
SpawnAttachedTo(component.Configuration.EffectProto, parent.ToCoordinates());
if (component.Configuration.ExitPolymorphPopup != null)
_popup.PopupEntity(Loc.GetString(component.Configuration.ExitPolymorphPopup,
("parent", Identity.Entity(uid, EntityManager)),
("child", Identity.Entity(parent, EntityManager))),
parent);
QueueDel(uid);
return parent;
}
/// <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(ProtoId<PolymorphPrototype> id, Entity<PolymorphableComponent> target)
{
target.Comp.PolymorphActions ??= new();
if (target.Comp.PolymorphActions.ContainsKey(id))
return;
if (!_proto.Resolve(id, out var polyProto))
return;
var entProto = _proto.Index(polyProto.Configuration.Entity);
EntityUid? actionId = default!;
if (!_actions.AddAction(target, ref actionId, RevertPolymorphId, target))
return;
target.Comp.PolymorphActions.Add(id, actionId.Value);
var metaDataCache = MetaData(actionId.Value);
_metaData.SetEntityName(actionId.Value, Loc.GetString("polymorph-self-action-name", ("target", entProto.Name)), metaDataCache);
_metaData.SetEntityDescription(actionId.Value, Loc.GetString("polymorph-self-action-description", ("target", entProto.Name)), metaDataCache);
if (_actions.GetAction(actionId) is not {} action)
return;
_actions.SetIcon((action, action.Comp), new SpriteSpecifier.EntityPrototype(polyProto.Configuration.Entity));
_actions.SetEvent(action, new PolymorphActionEvent(id));
}
public void RemovePolymorphAction(ProtoId<PolymorphPrototype> id, Entity<PolymorphableComponent> target)
{
if (target.Comp.PolymorphActions is not {} actions)
return;
if (actions.TryGetValue(id, out var action))
_actions.RemoveAction(target.Owner, action);
}
}