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 + ] + ] + } + ] +}