diff --git a/Content.Server/Anomaly/Effects/PolymorphAnomalySystem.cs b/Content.Server/Anomaly/Effects/PolymorphAnomalySystem.cs
new file mode 100644
index 00000000000..66d6aba8344
--- /dev/null
+++ b/Content.Server/Anomaly/Effects/PolymorphAnomalySystem.cs
@@ -0,0 +1,184 @@
+using System.Linq;
+using Content.Server.Polymorph.Systems;
+using Content.Shared.Anomaly.Components;
+using Content.Shared.Anomaly.Effects.Components;
+using Content.Shared.Anomaly.Prototypes;
+using Content.Shared.FixedPoint;
+using Content.Shared.Inventory;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Polymorph;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Anomaly.Effects;
+
+///
+/// This handles , forcibly polymorphing everything
+/// alive in range into a random entity for a random duration whenever the anomaly pulses.
+///
+public sealed class PolymorphAnomalySystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
+ [Dependency] private readonly PolymorphSystem _polymorph = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+ ///
+ /// Fallback thresholds applied to any polymorph destination that turns out to have no
+ /// MobState/MobThresholds configured at all (a content gap on that specific prototype).
+ /// Without this, such a creature is structurally incapable of ever reaching Critical/Dead,
+ /// no matter how much damage it takes, and revert-on-crit/death would never fire for it.
+ /// These are rough generic values, not tuned per-species - tune the actual prototype's own
+ /// MobThresholds later and this fallback will simply stop triggering for it.
+ ///
+ private const float FallbackCriticalThreshold = 45f;
+ private const float FallbackDeadThreshold = 65f;
+
+ /// Pre-allocated and re-used collection.
+ private readonly HashSet> _targets = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnPulse);
+ SubscribeLocalEvent(OnSupercritical);
+ }
+
+ private void OnPulse(Entity ent, ref AnomalyPulseEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.PolymorphTable, out var table))
+ return;
+
+ var coordinates = _transform.GetMapCoordinates(ent);
+ PolymorphInRange(ent, table, coordinates, ent.Comp.Range, args.Severity, permanentChance: 0f);
+ }
+
+ private void OnSupercritical(Entity ent, ref AnomalySupercriticalEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.PolymorphTable, out var table))
+ return;
+
+ var coordinates = _transform.GetMapCoordinates(ent);
+
+ PolymorphInRange(ent, table, coordinates, ent.Comp.SupercriticalRange, severity: 1f, ent.Comp.SupercriticalPermanentChance);
+ }
+
+ ///
+ /// Grabs everything alive in range and forcibly polymorphs each of them independently,
+ /// rolling a separate outcome and duration per victim.
+ ///
+ private void PolymorphInRange(
+ Entity ent,
+ PolymorphAnomalyTablePrototype table,
+ MapCoordinates coordinates,
+ float range,
+ float severity,
+ float permanentChance)
+ {
+ if (table.Options.Count == 0)
+ return;
+
+ _targets.Clear();
+ _lookup.GetEntitiesInRange(coordinates, range, _targets);
+
+ foreach (var target in _targets)
+ {
+ if (!_mobState.IsAlive(target))
+ continue;
+
+ var option = PickOption(table);
+ if (!_proto.TryIndex(option.Polymorph, out var polymorphProto))
+ continue;
+
+ var permanent = permanentChance > 0f && _random.Prob(permanentChance);
+
+ // TransferEntityInventories requires BOTH the source (the thing being polymorphed)
+ // AND the destination (the new form it's spawning into) to have InventoryComponent
+ var sourceHasInventory = HasComp(target);
+ var destHasInventory = _proto.Index(polymorphProto.Configuration.Entity)
+ .TryGetComponent(out _, EntityManager.ComponentFactory);
+ var hasInventory = sourceHasInventory && destHasInventory;
+
+ var config = polymorphProto.Configuration with
+ {
+ Forced = true,
+ Inventory = hasInventory ? PolymorphInventoryChange.Transfer : PolymorphInventoryChange.None,
+ RevertOnCrit = true,
+ RevertOnDeath = true,
+ AllowRepeatedMorphs = true,
+ Duration = permanent ? null : (int)RollDuration(option, severity).TotalSeconds,
+ };
+
+ var child = _polymorph.PolymorphEntity(target, config);
+ if (child != null)
+ EnsureCanDieOrCrit(child.Value);
+ }
+
+ if (ent.Comp.PolymorphSound != null)
+ _audio.PlayPvs(ent.Comp.PolymorphSound, ent);
+ }
+
+ ///
+ /// Safety net for polymorph destinations that are missing MobState/MobThresholds entirely -
+ /// without this, RevertOnCrit/RevertOnDeath can never fire for them since there's no data
+ /// telling the game at what damage value to change state. Only fills in fallback values if
+ /// the entity has none configured at all; a prototype with its own real MobThresholds is
+ /// left completely untouched.
+ ///
+ private void EnsureCanDieOrCrit(EntityUid child)
+ {
+ if (_mobThreshold.TryGetDeadThreshold(child, out _))
+ return;
+
+ EnsureComp(child);
+ EnsureComp(child);
+
+ _mobThreshold.SetMobStateThreshold(child, FixedPoint2.Zero, MobState.Alive);
+ _mobThreshold.SetMobStateThreshold(child, FallbackCriticalThreshold, MobState.Critical);
+ _mobThreshold.SetMobStateThreshold(child, FallbackDeadThreshold, MobState.Dead);
+ }
+
+ ///
+ /// Picks a random option from the table, weighted by .
+ ///
+ private PolymorphAnomalyOption PickOption(PolymorphAnomalyTablePrototype table)
+ {
+ var totalWeight = table.Options.Sum(o => o.Weight);
+ var roll = _random.NextFloat() * totalWeight;
+
+ var cumulative = 0f;
+ foreach (var option in table.Options)
+ {
+ cumulative += option.Weight;
+ if (roll <= cumulative)
+ return option;
+ }
+
+ // Floating point rounding fallback.
+ return table.Options[^1];
+ }
+
+ ///
+ /// Rolls a duration between an option's min and max, biased towards the max end
+ /// as anomaly severity increases.
+ ///
+ private TimeSpan RollDuration(PolymorphAnomalyOption option, float severity)
+ {
+ var t = _random.NextFloat();
+
+ var exponent = MathF.Max(0.1f, 1f - Math.Clamp(severity, 0f, 1f));
+ var biasedT = MathF.Pow(t, exponent);
+
+ var seconds = MathHelper.Lerp((float)option.MinDuration.TotalSeconds, (float)option.MaxDuration.TotalSeconds, biasedT);
+ return TimeSpan.FromSeconds(seconds);
+ }
+}
diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs
index 6b769e4e5fc..158ce29ad7f 100644
--- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs
+++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs
@@ -46,10 +46,27 @@ public sealed partial class PolymorphSystem : EntitySystem
private const string RevertPolymorphId = "ActionRevertPolymorph";
+ ///
+ /// Tracks every currently-polymorphed entity ourselves, since EntityQueryEnumerator silently
+ /// skips paused entities and we don't want to force them to stay awake just to be checked -
+ /// that would undo the whole point of NPCs going to sleep when idle. Added/removed via the
+ /// component's own lifecycle, so it stays accurate regardless of pause state.
+ ///
+ private readonly HashSet _polymorphed = new();
+
+ ///
+ /// Reusable snapshot buffer. Revert() can remove entries from _polymorphed mid-iteration
+ /// (deleting the old entity fires ComponentShutdown), so iterating the live set directly
+ /// isn't safe - we copy it first each tick.
+ ///
+ private readonly List _updateBuffer = new();
+
public override void Initialize()
{
SubscribeLocalEvent(OnComponentStartup);
SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnPolymorphedStartup);
+ SubscribeLocalEvent(OnPolymorphedShutdown);
SubscribeLocalEvent(OnPolymorphActionEvent);
SubscribeLocalEvent(OnRevertPolymorphActionEvent);
@@ -61,13 +78,31 @@ public override void Initialize()
InitializeMap();
}
+ private void OnPolymorphedStartup(Entity ent, ref ComponentStartup args)
+ {
+ _polymorphed.Add(ent);
+ }
+
+ private void OnPolymorphedShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ _polymorphed.Remove(ent);
+ }
+
public override void Update(float frameTime)
{
base.Update(frameTime);
+ if (_polymorphed.Count == 0)
+ return;
+
+ // Snapshot first - Revert() can remove entries from _polymorphed mid-loop.
+ _updateBuffer.Clear();
+ _updateBuffer.AddRange(_polymorphed);
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var comp))
+ foreach (var uid in _updateBuffer)
{
+ if (!TryComp(uid, out var comp) || comp.Reverted)
+ continue;
+
comp.Time += frameTime;
if (comp.Configuration.Duration != null && comp.Time >= comp.Configuration.Duration)
@@ -82,7 +117,7 @@ public override void Update(float frameTime)
if (comp.Configuration.RevertOnDeath && _mobState.IsDead(uid, mob) ||
comp.Configuration.RevertOnCrit && _mobState.IsIncapacitated(uid, mob))
{
- Revert((uid, comp));
+ var result = Revert((uid, comp));
}
}
}
@@ -145,7 +180,7 @@ private void OnDestruction(Entity ent, ref Destructi
if (ent.Comp.Reverted || !ent.Comp.Configuration.RevertOnDeath)
return;
- Revert((ent, ent));
+ var result = Revert((ent, ent));
}
private void OnPolymorphedTerminating(Entity ent, ref EntityTerminatingEvent args)
@@ -154,7 +189,9 @@ private void OnPolymorphedTerminating(Entity ent, re
return;
if (ent.Comp.Configuration.RevertOnDelete)
- Revert(ent.AsNullable());
+ {
+ var result = Revert(ent.AsNullable());
+ }
// Remove our original entity too
// Note that Revert will set Parent to null, so reverted entities will not be deleted
@@ -181,9 +218,12 @@ private void OnPolymorphedTerminating(Entity ent, re
public EntityUid? PolymorphEntity(EntityUid uid, PolymorphConfiguration configuration)
{
// If they're morphed, check their current config to see if they can be
- // morphed again
+ // morphed again. TryComp is called unconditionally (rather than inline in the &&
+ // chain) so currentPoly is always definitely-assigned for later use tracking chained
+ // re-polymorphs, regardless of whether IgnoreAllowRepeatedMorphs short-circuits this.
+ TryComp(uid, out var currentPoly);
if (!configuration.IgnoreAllowRepeatedMorphs
- && TryComp(uid, out var currentPoly)
+ && currentPoly != null
&& !currentPoly.Configuration.AllowRepeatedMorphs)
return null;
@@ -212,8 +252,16 @@ private void OnPolymorphedTerminating(Entity ent, re
_mindSystem.MakeSentient(child);
+ // If uid is itself already a polymorphed form (a chained re-polymorph), the new form's Parent must point at the
+ // TRUE original entity, not this intermediate form - otherwise each further re-polymorph
+ // only remembers one link back, and Revert() eventually unwinds to some intermediate
+ // creature instead of all the way back to what the victim actually started as.
+ var trueOriginal = uid;
+ if (currentPoly != null && currentPoly.Parent != null)
+ trueOriginal = currentPoly.Parent.Value;
+
var polymorphedComp = Factory.GetComponent();
- polymorphedComp.Parent = uid;
+ polymorphedComp.Parent = trueOriginal;
polymorphedComp.Configuration = configuration;
AddComp(child, polymorphedComp);
@@ -268,10 +316,22 @@ private void OnPolymorphedTerminating(Entity ent, re
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);
+ if (currentPoly != null)
+ {
+ // uid was itself an intermediate polymorph form - the true original is already
+ // safely parked from the very first polymorph in this chain, so this husk serves
+ // no purpose anymore. Mark it Reverted first so OnPolymorphedTerminating doesn't
+ // try to revert it a second time
+ currentPoly.Reverted = true;
+ QueueDel(uid);
+ }
+ else
+ {
+ //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);
@@ -293,23 +353,32 @@ private void OnPolymorphedTerminating(Entity ent, re
{
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);
diff --git a/Content.Shared/Anomaly/Effects/Components/PolymorphAnomalyComponent.cs b/Content.Shared/Anomaly/Effects/Components/PolymorphAnomalyComponent.cs
new file mode 100644
index 00000000000..59979434b94
--- /dev/null
+++ b/Content.Shared/Anomaly/Effects/Components/PolymorphAnomalyComponent.cs
@@ -0,0 +1,47 @@
+using Content.Shared.Anomaly.Prototypes;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Anomaly.Effects.Components;
+
+///
+/// Anomaly effect that grabs every valid mob (players and NPCs alike) in range on pulse
+/// and forcibly polymorphs them into a random entity, for a random duration.
+/// On supercritical, the effect happens again over a larger area, and each victim has a chance
+/// for the transformation to become permanent instead of timing out.
+///
+[RegisterComponent]
+public sealed partial class PolymorphAnomalyComponent : Component
+{
+ ///
+ /// The table of possible polymorph outcomes (and their durations) that this anomaly rolls from.
+ ///
+ [DataField(required: true)]
+ public ProtoId PolymorphTable;
+
+ ///
+ /// The radius, in tiles, that a normal pulse will affect.
+ ///
+ [DataField]
+ public float Range = 5f;
+
+ ///
+ /// The radius, in tiles, that a crit event will affect.
+ ///
+ [DataField]
+ public float SupercriticalRange = 10f;
+
+ ///
+ /// Chance, per victim, that a polymorph rolled during a supercritical event becomes
+ /// permanent (i.e. its duration is cleared entirely) instead of using the normal rolled duration.
+ /// A permanent polymorph can still be reverted by death or critical condition.
+ ///
+ [DataField]
+ public float SupercriticalPermanentChance = 0.4f;
+
+ ///
+ /// Sound played at the anomaly whenever it polymorphs something.
+ ///
+ [DataField]
+ public SoundSpecifier? PolymorphSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/Magic/staff_animation.ogg");
+}
diff --git a/Content.Shared/Anomaly/Prototypes/PolymorphAnomalyTablePrototype.cs b/Content.Shared/Anomaly/Prototypes/PolymorphAnomalyTablePrototype.cs
new file mode 100644
index 00000000000..079079a7aa5
--- /dev/null
+++ b/Content.Shared/Anomaly/Prototypes/PolymorphAnomalyTablePrototype.cs
@@ -0,0 +1,53 @@
+using Content.Shared.Polymorph;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Anomaly.Prototypes;
+
+///
+/// A weighted table of possible outcomes for a polymorph-inflicting anomaly.
+///
+[Prototype]
+public sealed partial class PolymorphAnomalyTablePrototype : IPrototype
+{
+ [IdDataField]
+ public string ID { get; private set; } = default!;
+
+ ///
+ /// All possible polymorph outcomes this table can roll, along with their weight
+ /// and how long each one lasts.
+ ///
+ [DataField(required: true)]
+ public List Options = new();
+}
+
+///
+/// A single entry in a .
+///
+[DataDefinition]
+public sealed partial class PolymorphAnomalyOption
+{
+ ///
+ /// The polymorph that will be applied if this option is picked.
+ ///
+ [DataField(required: true)]
+ public ProtoId Polymorph;
+
+ ///
+ /// Relative weight of this option when randomly picking from the table.
+ /// Weights don't need to add up to any particular total - they're relative to each other.
+ ///
+ [DataField]
+ public float Weight = 1f;
+
+ ///
+ /// The minimum amount of time a victim will remain polymorphed when this option is picked.
+ ///
+ [DataField(required: true)]
+ public TimeSpan MinDuration = TimeSpan.FromSeconds(20);
+
+ ///
+ /// The maximum amount of time a victim will remain polymorphed when this option is picked.
+ ///
+ [DataField(required: true)]
+ public TimeSpan MaxDuration = TimeSpan.FromMinutes(5);
+}
diff --git a/Resources/Prototypes/Anomaly/polymorph_anomaly_tables.yml b/Resources/Prototypes/Anomaly/polymorph_anomaly_tables.yml
new file mode 100644
index 00000000000..39587811755
--- /dev/null
+++ b/Resources/Prototypes/Anomaly/polymorph_anomaly_tables.yml
@@ -0,0 +1,374 @@
+# To add a new possible polymorph outcome for the Polymorph anomaly, just add another
+# entry below. `polymorph` must point at a `polymorph` prototype
+# (see Resources/Prototypes/Polymorphs/anomaly_polymorphs.yml and polymorph.yml), `weight`
+# controls how often it's picked relative to the other entries, and minDuration/maxDuration
+# control how long a victim stays transformed if this entry is rolled (higher anomaly severity
+# biases the roll towards max).
+#
+# Deliberately excluded from this table: silicon/robot mobs (borgs, hivebots, etc.) and any
+# playable species (human, lizard-person, moth, etc.) - only animals and non-playable monsters
+# are eligible outcomes.
+
+- type: polymorphAnomalyTable
+ id: DefaultPolymorphAnomalyTable
+ options:
+ - polymorph: Chicken
+ weight: 6
+ minDuration: 30
+ maxDuration: 120
+ - polymorph: Mouse
+ weight: 6
+ minDuration: 30
+ maxDuration: 300
+ - polymorph: ArtifactMonkey
+ weight: 6
+ minDuration: 60
+ maxDuration: 600
+ - polymorph: ArtifactAnomalyCow
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyGoat
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyDuck
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyPig
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyGoose
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyFrog
+ weight: 5
+ minDuration: 20
+ maxDuration: 300
+ - polymorph: ArtifactAnomalySnake
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyCrab
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyPossum
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyRaccoon
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyFox
+ weight: 5
+ minDuration: 60
+ maxDuration: 420
+ - polymorph: ArtifactAnomalyCat
+ weight: 5
+ minDuration: 60
+ maxDuration: 420
+ - polymorph: ArtifactAnomalyCorgi
+ weight: 5
+ minDuration: 60
+ maxDuration: 420
+ - polymorph: ArtifactAnomalyHamster
+ weight: 5
+ minDuration: 120
+ maxDuration: 1200
+ - polymorph: ArtifactAnomalyParrot
+ weight: 5
+ minDuration: 60
+ maxDuration: 120
+ - polymorph: ArtifactAnomalyPenguin
+ weight: 5
+ minDuration: 60
+ maxDuration: 120
+ - polymorph: ArtifactAnomalyKangaroo
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyBat
+ weight: 4
+ minDuration: 60
+ maxDuration: 240
+ - polymorph: ArtifactAnomalyBee
+ weight: 4
+ minDuration: 60
+ maxDuration: 240
+ - polymorph: ArtifactAnomalySlug
+ weight: 4
+ minDuration: 60
+ maxDuration: 240
+ - polymorph: ArtifactLizard
+ weight: 3
+ minDuration: 30
+ maxDuration: 120
+ - polymorph: ArtifactLuminous
+ weight: 3
+ minDuration: 180
+ maxDuration: 600
+ - polymorph: ArtifactAnomalyGiantSpider
+ weight: 3
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyCarp
+ weight: 4
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyShark
+ weight: 2
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalySlimeBlue
+ weight: 4
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalySlimeGreen
+ weight: 4
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalySlimeYellow
+ weight: 4
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyWatcher
+ weight: 2
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyFleshGolem
+ weight: 3
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyTick
+ weight: 2
+ minDuration: 300
+ maxDuration: 360
+ - polymorph: ArtifactAnomalySnail
+ weight: 4
+ minDuration: 30
+ maxDuration: 120
+ - polymorph: ArtifactAnomalyCobra
+ weight: 3
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyBear
+ weight: 3
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyGoliath
+ weight: 1
+ minDuration: 300
+ maxDuration: 1200
+ - polymorph: ArtifactAnomalyBasilisk
+ weight: 1
+ minDuration: 300
+ maxDuration: 1200
+ - polymorph: ArtifactAnomalyRavager
+ weight: 2
+ minDuration: 300
+ maxDuration: 1200
+ - polymorph: ArtifactAnomalyXenoDrone
+ weight: 1
+ minDuration: 300
+ maxDuration: 1200
+ - polymorph: ArtifactAnomalyXenoRunner
+ weight: 3
+ minDuration: 300
+ maxDuration: 1200
+ - polymorph: ArtifactAnomalyXenoSpitter
+ weight: 3
+ minDuration: 300
+ maxDuration: 1200
+ - polymorph: ArtifactAnomalyAbomination
+ weight: 4
+ minDuration: 300
+ maxDuration: 1200
+ - polymorph: ArtifactAnomalyHivelord
+ weight: 1
+ minDuration: 600
+ maxDuration: 1500
+ - polymorph: ArtifactAnomalyRatKing
+ weight: 0.5
+ minDuration: 600
+ maxDuration: 1800
+ - polymorph: ArtifactAnomalyXenoQueen
+ weight: 1
+ minDuration: 600
+ maxDuration: 1800
+ - polymorph: ArtifactAnomalyHellspawn
+ weight: 0.5
+ minDuration: 900
+ maxDuration: 1800
+ - polymorph: ArtifactAnomalyQuartzCrab
+ weight: 4
+ minDuration: 120
+ maxDuration: 480
+ - polymorph: ArtifactAnomalyIronCrab
+ weight: 4
+ minDuration: 120
+ maxDuration: 480
+ - polymorph: ArtifactAnomalyCoalCrab
+ weight: 4
+ minDuration: 120
+ maxDuration: 480
+ - polymorph: ArtifactAnomalyUraniumCrab
+ weight: 4
+ minDuration: 120
+ maxDuration: 480
+ - polymorph: ArtifactAnomalyBananiumCrab
+ weight: 1
+ minDuration: 120
+ maxDuration: 480
+ - polymorph: ArtifactAnomalySilverCrab
+ weight: 4
+ minDuration: 120
+ maxDuration: 480
+ - polymorph: ArtifactAnomalyGoldCrab
+ weight: 4
+ minDuration: 120
+ maxDuration: 480
+ - polymorph: ArtifactAnomalyKobold
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyGorilla
+ weight: 4
+ minDuration: 180
+ maxDuration: 600
+ - polymorph: ArtifactAnomalyCockroach
+ weight: 6
+ minDuration: 30
+ maxDuration: 60
+ - polymorph: ArtifactAnomalyGlockroach
+ weight: 4
+ minDuration: 30
+ maxDuration: 120
+ - polymorph: ArtifactAnomalyMothroach
+ weight: 6
+ minDuration: 30
+ maxDuration: 120
+ - polymorph: ArtifactAnomalyReindeer
+ weight: 5
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyBoxingKangaroo
+ weight: 4
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyMimic
+ weight: 3
+ minDuration: 60
+ maxDuration: 120
+ - polymorph: ArtifactAnomalyMoproach
+ weight: 4
+ minDuration: 60
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyScurret
+ weight: 4
+ minDuration: 300
+ maxDuration: 1800
+ - polymorph: ArtifactAnomalyEmotionalSupportScurret
+ weight: 3
+ minDuration: 420
+ maxDuration: 1800
+ - polymorph: ArtifactAnomalyClownSpider
+ weight: 2
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyCarpHolo
+ weight: 3
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyCarpRainbow
+ weight: 1
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyLuminousObject
+ weight: 3
+ minDuration: 30
+ maxDuration: 60
+ - polymorph: ArtifactAnomalyLuminousEntity
+ weight: 3
+ minDuration: 30
+ maxDuration: 60
+ - polymorph: ArtifactAnomalyTomatoKiller
+ weight: 2
+ minDuration: 180
+ maxDuration: 600
+ - polymorph: ArtifactAnomalyArgocyteSlurva
+ weight: 3
+ minDuration: 30
+ maxDuration: 60
+ - polymorph: ArtifactAnomalyArgocyteBarrier
+ weight: 3
+ minDuration: 60
+ maxDuration: 120
+ - polymorph: ArtifactAnomalyArgocyteSkitter
+ weight: 3
+ minDuration: 60
+ maxDuration: 120
+ - polymorph: ArtifactAnomalyArgocyteSwiper
+ weight: 3
+ minDuration: 60
+ maxDuration: 120
+ - polymorph: ArtifactAnomalyArgocyteMolder
+ weight: 2
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyArgocyteGlider
+ weight: 2
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyArgocyteHarvester
+ weight: 2
+ minDuration: 300
+ maxDuration: 900
+ - polymorph: ArtifactAnomalyHivelordBrood
+ weight: 5
+ minDuration: 30
+ maxDuration: 300
+ - polymorph: ArtifactAnomalyBehonkerElectrical
+ weight: 1
+ minDuration: 600
+ maxDuration: 1500
+ - polymorph: ArtifactAnomalyBehonkerPyro
+ weight: 1
+ minDuration: 600
+ maxDuration: 1500
+ - polymorph: ArtifactAnomalyBehonkerGrav
+ weight: 1
+ minDuration: 600
+ maxDuration: 1500
+ - polymorph: ArtifactAnomalyBehonkerIce
+ weight: 1
+ minDuration: 600
+ maxDuration: 1500
+ - polymorph: ArtifactAnomalyArgocytePouncer
+ weight: 1
+ minDuration: 600
+ maxDuration: 1500
+ - polymorph: ArtifactAnomalyArgocyteCrawler
+ weight: 1
+ minDuration: 600
+ maxDuration: 1500
+ - polymorph: ArtifactAnomalyArgocyteEnforcer
+ weight: 1
+ minDuration: 600
+ maxDuration: 1800
+ - polymorph: ArtifactAnomalyArgocyteFounder
+ weight: 1
+ minDuration: 900
+ maxDuration: 1800
+ - polymorph: ArtifactAnomalyArgocyteLeviathing
+ weight: 0.5
+ minDuration: 900
+ maxDuration: 1800
+ - polymorph: ArtifactAnomalyLaserRaptor
+ weight: 0.5
+ minDuration: 900
+ maxDuration: 1800
diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/anomaly.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/anomaly.yml
index dc430527367..027814d0215 100644
--- a/Resources/Prototypes/Entities/Markers/Spawners/Random/anomaly.yml
+++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/anomaly.yml
@@ -22,6 +22,7 @@
- AnomalyFlora
- AnomalyShadow
- AnomalyTech
+ - AnomalyPolymorph
#- AnomalySanta
rareChance: 0.3
rarePrototypes:
diff --git a/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml b/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml
index 24ce8d2baba..0c94e53631d 100644
--- a/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml
+++ b/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml
@@ -1166,3 +1166,32 @@
projectilePrototype: DrinkLemonLimeCranberryCan
minProjectiles: 1
maxProjectiles: 3
+
+- type: entity
+ id: AnomalyPolymorph
+ parent: BaseAnomaly
+ suffix: Polymorph
+ components:
+ - type: Anomaly
+ corePrototype: AnomalyCorePolymorph
+ coreInertPrototype: AnomalyCorePolymorphInert
+ supercriticalSoundAtAnimationStart:
+ collection: RadiationPulse
+ - type: Sprite
+ sprite: Structures/Specific/Anomalies/polymorph_anom.rsi
+ layers:
+ - state: anom
+ map: ["enum.AnomalyVisualLayers.Base"]
+ - state: anom-pulse
+ map: ["enum.AnomalyVisualLayers.Animated"]
+ visible: false
+ - type: PointLight
+ radius: 2.0
+ energy: 5.0
+ color: "#c060ff"
+ castShadows: false
+ - type: PolymorphAnomaly
+ polymorphTable: DefaultPolymorphAnomalyTable
+ range: 5
+ supercriticalRange: 7
+ supercriticalPermanentChance: 0.2
diff --git a/Resources/Prototypes/Entities/Structures/Specific/Anomaly/cores.yml b/Resources/Prototypes/Entities/Structures/Specific/Anomaly/cores.yml
index 492ab11f0c6..0218d231c4a 100644
--- a/Resources/Prototypes/Entities/Structures/Specific/Anomaly/cores.yml
+++ b/Resources/Prototypes/Entities/Structures/Specific/Anomaly/cores.yml
@@ -372,3 +372,29 @@
energy: 2.0
color: "#fc0303"
castShadows: false
+
+- type: entity
+ parent: BaseAnomalyCore
+ id: AnomalyCorePolymorph
+ suffix: Polymorph
+ components:
+ - type: Sprite
+ sprite: Structures/Specific/Anomalies/Cores/polymorph_core.rsi
+ - type: PointLight
+ radius: 1.5
+ energy: 2.0
+ color: "#c060ff"
+ castShadows: false
+
+- type: entity
+ parent: BaseAnomalyInertCore
+ id: AnomalyCorePolymorphInert
+ suffix: Polymorph, Inert
+ components:
+ - type: Sprite
+ sprite: Structures/Specific/Anomalies/Cores/polymorph_core.rsi
+ - type: PointLight
+ radius: 1.5
+ energy: 2.0
+ color: "#c060ff"
+ castShadows: false
diff --git a/Resources/Prototypes/Polymorphs/anomaly_polymorphs.yml b/Resources/Prototypes/Polymorphs/anomaly_polymorphs.yml
new file mode 100644
index 00000000000..1e4abc18efa
--- /dev/null
+++ b/Resources/Prototypes/Polymorphs/anomaly_polymorphs.yml
@@ -0,0 +1,788 @@
+# Polymorph outcomes used by the Polymorph anomaly (see Resources/Prototypes/Anomaly/polymorph_anomaly_tables.yml).
+#
+# Duration is deliberately left unset here - PolymorphAnomalySystem always overrides Duration,
+# Forced, Inventory, RevertOnCrit, and RevertOnDeath per-victim at runtime, so whatever is set
+# (or not set) for those fields below has no effect when used through the anomaly. They're set
+# here anyway so each polymorph still behaves sensibly if ever triggered some other way (e.g. by
+# an admin smite, a future artifact node, etc).
+#
+# NOTE: some existing polymorph prototypes are reused directly by the anomaly table instead of
+# being redefined here - see Mouse, Chicken, Monkey, ArtifactLizard, ArtifactLuminous, and
+# ArtifactCluwne in polymorph.yml.
+
+# --- Common critters ---
+
+- type: polymorph
+ id: ArtifactAnomalyCow
+ configuration:
+ entity: MobCow
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyGoat
+ configuration:
+ entity: MobGoat
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyDuck
+ configuration:
+ entity: MobDuckMallard
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyPig
+ configuration:
+ entity: MobPig
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyGoose
+ configuration:
+ entity: MobGoose
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyFrog
+ configuration:
+ entity: MobFrog
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalySnake
+ configuration:
+ entity: MobSnake
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyCrab
+ configuration:
+ entity: MobCrab
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyPossum
+ configuration:
+ entity: MobPossum
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyRaccoon
+ configuration:
+ entity: MobRaccoon
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyFox
+ configuration:
+ entity: MobFox
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyCat
+ configuration:
+ entity: MobCat
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyCorgi
+ configuration:
+ entity: MobCorgi
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyHamster
+ configuration:
+ entity: MobHamster
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyParrot
+ configuration:
+ entity: MobParrot
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyPenguin
+ configuration:
+ entity: MobPenguin
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyKangaroo
+ configuration:
+ entity: MobKangaroo
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyBat
+ configuration:
+ entity: MobBat
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyBee
+ configuration:
+ entity: MobBee
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalySlug
+ configuration:
+ entity: MobSlug
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+# --- Medium threats ---
+
+- type: polymorph
+ id: ArtifactAnomalyGiantSpider
+ configuration:
+ entity: MobGiantSpider
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyCarp
+ configuration:
+ entity: MobCarp
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyShark
+ configuration:
+ entity: MobShark
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalySlimeBlue
+ configuration:
+ entity: MobAdultSlimesBlue
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalySlimeGreen
+ configuration:
+ entity: MobAdultSlimesGreen
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalySlimeYellow
+ configuration:
+ entity: MobAdultSlimesYellow
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyWatcher
+ configuration:
+ entity: MobWatcherLavaland
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyFleshGolem
+ configuration:
+ entity: MobFleshGolem
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyTick
+ configuration:
+ entity: MobTick
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalySnail
+ configuration:
+ entity: MobSnail
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyCobra
+ configuration:
+ entity: MobCobraSpace
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyBear
+ configuration:
+ entity: MobBearSpace
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyGoliath
+ configuration:
+ entity: MobGoliath
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyBasilisk
+ configuration:
+ entity: MobBasilisk
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+# --- Apex / rare ---
+
+- type: polymorph
+ id: ArtifactAnomalyRavager
+ configuration:
+ entity: MobXenoRavager
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyXenoDrone
+ configuration:
+ entity: MobXenoDrone
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyXenoRunner
+ configuration:
+ entity: MobXenoRunner
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyXenoSpitter
+ configuration:
+ entity: MobXenoSpitter
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyAbomination
+ configuration:
+ entity: MobAbomination
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyHivelord
+ configuration:
+ entity: MobHivelord
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyRatKing
+ configuration:
+ entity: MobRatKing
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyXenoQueen
+ configuration:
+ entity: MobXenoQueen
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+# NOTE: MobHellspawn is a ghost-role solo antagonist prototype. This is fine while the victim's
+# mind occupies it (PolymorphEntity transfers their mind in immediately), but if they later ghost
+# out of it, the underlying GhostRole/GhostTakeoverAvailable components mean the abandoned body
+# could be picked up by another ghost. Treated here as an intentional, extremely rare jackpot.
+- type: polymorph
+ id: ArtifactAnomalyHellspawn
+ configuration:
+ entity: MobHellspawn
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+# --- Second wave: mining elementals, oddities, argocytes, behonkers, plant anomaly spawns ---
+
+- type: polymorph
+ id: ArtifactAnomalyQuartzCrab
+ configuration:
+ entity: MobQuartzCrab
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyIronCrab
+ configuration:
+ entity: MobIronCrab
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyCoalCrab
+ configuration:
+ entity: MobCoalCrab
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyUraniumCrab
+ configuration:
+ entity: MobUraniumCrab
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyBananiumCrab
+ configuration:
+ entity: MobBananiumCrab
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalySilverCrab
+ configuration:
+ entity: MobSilverCrab
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyGoldCrab
+ configuration:
+ entity: MobGoldCrab
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyKobold
+ configuration:
+ entity: MobKobold
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyGorilla
+ configuration:
+ entity: MobGorilla
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyCockroach
+ configuration:
+ entity: MobCockroach
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyGlockroach
+ configuration:
+ entity: MobGlockroach
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyMothroach
+ configuration:
+ entity: MobMothroach
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyReindeer
+ configuration:
+ entity: MobReindeerBuck
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyBoxingKangaroo
+ configuration:
+ entity: MobBoxingKangaroo
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyClownSpider
+ configuration:
+ entity: MobClownSpider
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyCarpHolo
+ configuration:
+ entity: MobCarpHolo
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyCarpRainbow
+ configuration:
+ entity: MobCarpRainbow
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyMimic
+ configuration:
+ entity: MobMimic
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyLaserRaptor
+ configuration:
+ entity: MobLaserRaptor
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyTomatoKiller
+ configuration:
+ entity: MobTomatoKiller
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyMoproach
+ configuration:
+ entity: MobMoproach
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyScurret
+ configuration:
+ entity: MobScurret
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyEmotionalSupportScurret
+ configuration:
+ entity: MobEmotionalSupportScurret
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyHivelordBrood
+ configuration:
+ entity: MobHivelordBrood
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyLuminousObject
+ configuration:
+ entity: MobLuminousObject
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyLuminousEntity
+ configuration:
+ entity: MobLuminousEntity
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyBehonkerElectrical
+ configuration:
+ entity: MobBehonkerElectrical
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyBehonkerPyro
+ configuration:
+ entity: MobBehonkerPyro
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyBehonkerGrav
+ configuration:
+ entity: MobBehonkerGrav
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyBehonkerIce
+ configuration:
+ entity: MobBehonkerIce
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteSlurva
+ configuration:
+ entity: MobArgocyteSlurva
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteBarrier
+ configuration:
+ entity: MobArgocyteBarrier
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteSkitter
+ configuration:
+ entity: MobArgocyteSkitter
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteSwiper
+ configuration:
+ entity: MobArgocyteSwiper
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteMolder
+ configuration:
+ entity: MobArgocyteMolder
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocytePouncer
+ configuration:
+ entity: MobArgocytePouncer
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteGlider
+ configuration:
+ entity: MobArgocyteGlider
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteHarvester
+ configuration:
+ entity: MobArgocyteHarvester
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteCrawler
+ configuration:
+ entity: MobArgocyteCrawler
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteEnforcer
+ configuration:
+ entity: MobArgocyteEnforcer
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteFounder
+ configuration:
+ entity: MobArgocyteFounder
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
+
+- type: polymorph
+ id: ArtifactAnomalyArgocyteLeviathing
+ configuration:
+ entity: MobArgocyteLeviathing
+ forced: true
+ inventory: Transfer
+ revertOnCrit: true
+ revertOnDeath: true
diff --git a/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/core.png b/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/core.png
new file mode 100644
index 00000000000..27c26b23876
Binary files /dev/null and b/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/core.png differ
diff --git a/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/inhand-left.png b/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/inhand-left.png
new file mode 100644
index 00000000000..6c23f6429da
Binary files /dev/null and b/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/inhand-right.png b/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/inhand-right.png
new file mode 100644
index 00000000000..6c23f6429da
Binary files /dev/null and b/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/meta.json b/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/meta.json
new file mode 100644
index 00000000000..56bb4b6676e
--- /dev/null
+++ b/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/meta.json
@@ -0,0 +1,20 @@
+{
+ "version": 1,
+ "license": "CC0-1.0",
+ "copyright": "Kildar - .kildar(discord)",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ { "name": "core" },
+ {
+ "name": "pulse",
+ "delays": [
+ [0.15625, 0.15625, 0.15625, 0.15625]
+ ]
+ },
+ { "name": "inhand-left", "directions": 4 },
+ { "name": "inhand-right", "directions": 4 }
+ ]
+}
diff --git a/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/pulse.png b/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/pulse.png
new file mode 100644
index 00000000000..cd88bdf65c0
Binary files /dev/null and b/Resources/Textures/Structures/Specific/Anomalies/Cores/polymorph_core.rsi/pulse.png differ
diff --git a/Resources/Textures/Structures/Specific/Anomalies/polymorph_anom.rsi/anom-pulse.png b/Resources/Textures/Structures/Specific/Anomalies/polymorph_anom.rsi/anom-pulse.png
new file mode 100644
index 00000000000..1546bc70b0e
Binary files /dev/null and b/Resources/Textures/Structures/Specific/Anomalies/polymorph_anom.rsi/anom-pulse.png differ
diff --git a/Resources/Textures/Structures/Specific/Anomalies/polymorph_anom.rsi/anom.png b/Resources/Textures/Structures/Specific/Anomalies/polymorph_anom.rsi/anom.png
new file mode 100644
index 00000000000..94959ac39fd
Binary files /dev/null and b/Resources/Textures/Structures/Specific/Anomalies/polymorph_anom.rsi/anom.png differ
diff --git a/Resources/Textures/Structures/Specific/Anomalies/polymorph_anom.rsi/meta.json b/Resources/Textures/Structures/Specific/Anomalies/polymorph_anom.rsi/meta.json
new file mode 100644
index 00000000000..e379e6dfd7c
--- /dev/null
+++ b/Resources/Textures/Structures/Specific/Anomalies/polymorph_anom.rsi/meta.json
@@ -0,0 +1,61 @@
+{
+ "version": 1,
+ "license": "CC0-1.0",
+ "copyright": "Kildar - .kildar (discord)",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "anom",
+ "delays": [
+ [
+ 1.1,
+ 0.18,
+ 0.18,
+ 1.1,
+ 0.18,
+ 0.18,
+ 1.1,
+ 0.18,
+ 0.18
+ ]
+ ]
+ },
+ {
+ "name": "anom-pulse",
+ "delays": [
+ [
+ 0.06,
+ 0.06,
+ 0.07,
+ 0.11,
+ 0.07,
+ 0.06,
+ 0.06,
+ 0.08,
+ 0.08,
+ 0.06,
+ 0.06,
+ 0.07,
+ 0.11,
+ 0.07,
+ 0.06,
+ 0.06,
+ 0.08,
+ 0.08,
+ 0.06,
+ 0.06,
+ 0.07,
+ 0.11,
+ 0.07,
+ 0.06,
+ 0.06,
+ 0.08,
+ 0.08
+ ]
+ ]
+ }
+ ]
+}