From 1b7bb5fefc8aa19c7d6541d5a09aa0db30559fa7 Mon Sep 17 00:00:00 2001 From: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Date: Sat, 15 Aug 2026 09:13:57 +0000 Subject: [PATCH] Add EntitySpawnVariationPass as an EntityEffect (#44436) * Initial pass on making graffiti into an entity effect * Refine code to not consider the entire AABB and eligible tiles * Rename to OnMapInit, * Change default values * Add missing summary * Update into using separate function, entitytable --- .../EntitySpawnVariationPassEntityEffect.cs | 111 ++++++++++++++++++ .../EntityEffectOnMapInitComponent.cs | 15 +++ .../SharedEntityEffectsSystem.cs | 6 + 3 files changed, 132 insertions(+) create mode 100644 Content.Shared/EntityEffects/Effects/VariationPass/EntitySpawnVariationPassEntityEffect.cs create mode 100644 Content.Shared/EntityEffects/EntityEffectOnMapInitComponent.cs diff --git a/Content.Shared/EntityEffects/Effects/VariationPass/EntitySpawnVariationPassEntityEffect.cs b/Content.Shared/EntityEffects/Effects/VariationPass/EntitySpawnVariationPassEntityEffect.cs new file mode 100644 index 0000000000..cdfad8e848 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/VariationPass/EntitySpawnVariationPassEntityEffect.cs @@ -0,0 +1,111 @@ +using System.Linq; +using Content.Shared.EntityTable; +using Content.Shared.EntityTable.EntitySelectors; +using Content.Shared.Physics; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics; +using Robust.Shared.Random; + +namespace Content.Shared.EntityEffects.Effects.VariationPass; + +/// +/// Used for spawning entities randomly dotted around the grid. +/// +/// +public sealed partial class EntitySpawnVariationPassEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private SharedMapSystem _map = default!; + [Dependency] private IRobustRandom _random = default!; + [Dependency] private EntityLookupSystem _lookup = default!; + [Dependency] private EntityTableSystem _tables = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var tiles = _map.GetAllTiles(entity, entity).ToList(); + var totalTiles = tiles.Count(); + + var dirtyMod = _random.NextGaussian(args.Effect.TilesPerEntityAverage, args.Effect.TilesPerEntityStdDev); + var trashTiles = Math.Max((int) (totalTiles * (1 / dirtyMod)), 0); + + for (var i = 0; i < trashTiles; i++) + { + if (TryFindRandomTile(entity, tiles, out var coords)) + { + var ents = _tables.GetSpawns(args.Effect.Table, _random); + foreach (var spawn in ents) + { + SpawnAtPosition(spawn, coords); + } + } + } + } + + /// Attempts to find an empty tile 10 times, returns true if successful. + private bool TryFindRandomTile(Entity grid, + List tiles, + out EntityCoordinates targetCoords) + { + targetCoords = EntityCoordinates.Invalid; + + for (var i = 0; i < 10; i++) + { + var tile = _random.Pick(tiles); + var tileCoords = tile.GridIndices; + + if (CheckTileEntities(grid, tileCoords)) + { + targetCoords = _map.GridTileToLocal(grid, grid.Comp, tileCoords); + return true; + } + } + + return false; + } + + /// Returns false if the tile is occupied. + private bool CheckTileEntities(Entity grid, Vector2i tileCoords) + { + var intersectingEntities = new HashSet(); + _lookup.GetLocalEntitiesIntersecting(grid, tileCoords, intersectingEntities, -0.05f, LookupFlags.Uncontained); + + foreach (var ent in intersectingEntities) + { + if (TryComp(ent, out var fixtures)) + { + foreach (var fixture in fixtures.Fixtures.Values) + { + // Continue if no collision is possible + if (!fixture.Hard || fixture.CollisionLayer <= 0 || (fixture.CollisionLayer & (int) CollisionGroup.Impassable) == 0) + continue; + + return false; + } + } + } + + return true; + } +} + +/// +public sealed partial class EntitySpawnVariationPass : EntityEffectBase +{ + /// + /// Number of tiles before we spawn one entity on average. + /// + [DataField] + public float TilesPerEntityAverage = 50f; + + /// + /// Standard deviation for the randomness selection. + /// + [DataField] + public float TilesPerEntityStdDev = 7f; + + /// + /// Spawn table for each chosen location. + /// + [DataField(required: true)] + public EntityTableSelector Table = default!; +} diff --git a/Content.Shared/EntityEffects/EntityEffectOnMapInitComponent.cs b/Content.Shared/EntityEffects/EntityEffectOnMapInitComponent.cs new file mode 100644 index 0000000000..64d51dfdb4 --- /dev/null +++ b/Content.Shared/EntityEffects/EntityEffectOnMapInitComponent.cs @@ -0,0 +1,15 @@ +namespace Content.Shared.EntityEffects; + +/// +/// Applies a set of EntityEffects to the entity upon map initialization. +/// Useful for e.g. station spawning. +/// +[RegisterComponent] +public sealed partial class EntityEffectOnMapInitComponent : Component +{ + /// + /// Effects that should be applied upon the map being initiated. + /// + [DataField(required: true)] + public EntityEffect[] Effects { get; set; } = default!; +} diff --git a/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs b/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs index a7c3fe1768..a1d862b7d3 100644 --- a/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs +++ b/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs @@ -22,6 +22,7 @@ public sealed partial class SharedEntityEffectsSystem : EntitySystem, IEntityEff public override void Initialize() { SubscribeLocalEvent(OnReactive); + SubscribeLocalEvent(OnMapInit); } private void OnReactive(Entity entity, ref ReactionEntityEvent args) @@ -58,6 +59,11 @@ public sealed partial class SharedEntityEffectsSystem : EntitySystem, IEntityEff } } + private void OnMapInit(Entity entity, ref MapInitEvent args) + { + ApplyEffects(entity, entity.Comp.Effects); + } + /// public void ApplyEffects(EntityUid target, EntityEffect[] effects, FixedPoint2 scale, EntityUid? user = null) {