Decal Spawners (#37066)

Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
This commit is contained in:
Southbridge
2025-05-27 19:55:48 -04:00
committed by GitHub
parent d28c64f76a
commit 755ce6f59b
10 changed files with 756 additions and 1 deletions

View File

@@ -0,0 +1,122 @@
using Robust.Shared.Prototypes;
using Content.Shared.Maps;
using Content.Shared.Decals;
namespace Content.Server.Spawners.Components;
/// <summary>
/// This component spawns decals around the entity on MapInit.
/// See doc strings for the various parameters for more information.
/// </summary>
[RegisterComponent, EntityCategory("Spawner")]
public sealed partial class RandomDecalSpawnerComponent : Component
{
/// <summary>
/// A list of decals to randomly select from when spawning.
/// </summary>
[DataField]
public List<ProtoId<DecalPrototype>> Decals = new();
/// <summary>
/// Radius (in tiles) to spawn decals in. 0 will target only the tile the entity is on.
/// </summary>
[DataField]
public float Radius = 1f;
/// <summary>
/// Probability that a particular decal gets spawned.
/// </summary>
[DataField]
public float Prob = 1f;
/// <summary>
/// The maximum amount of decals to spawn across the entire radius.
/// </summary>
[DataField]
public int MaxDecals = 1;
/// <summary>
/// The maximum amount of decals to spawn within a tile.
/// </summary>
/// <remarks>
/// A value <= 0 or null is considered unlimited.
/// </remarks>
[DataField]
public int? MaxDecalsPerTile = null;
/// <summary>
/// Whether decals should have a random rotation applied to them.
/// </summary>
[DataField]
public bool RandomRotation = false;
/// <summary>
/// Whether decals should snap to 90 degree orientations, does nothing if RandomRotation is false.
/// </summary>
[DataField]
public bool SnapRotation = false;
/// <summary>
/// Whether decals should snap to the center omf a grid space or be placed randoly.
/// </summary>
/// <remarks>
/// A null value will cause this to attempt to use the default value (DefaultSnap) for the decal.
/// </remarks>
[DataField]
public bool? SnapPosition = false;
/// <summary>
/// zIndex for the generated decals
/// </summary>
[DataField]
public int ZIndex = 0;
/// <summary>
/// Color for the generated decals. Does nothing if RandomColorList is set.
/// </summary>
[DataField]
public Color Color = Color.White;
/// <summary>
/// A random color to select from. Overrides Color if set.
/// </summary>
[DataField]
public List<Color>? RandomColorList = new();
/// <summary>
/// Whether the new decals are cleanable or not
/// </summary>
/// <remarks>
/// A null value will cause this to attempt to use the default value (DefaultCleanable) for the decal.
/// </remarks>
[DataField]
public bool? Cleanable = null;
/// <summary>
/// A list of tile prototype IDs to only place decals on.
/// </summary>
/// <remarks>
/// Causes the TileBlacklist to be ignored if this is set.
/// Note that due to the nature of tile-based placement, it's possible for decals to "spill over" onto nearby tiles.
/// This is mostly so dirt decals don't go on diagonal tiles that won't work for them.
/// </remarks>
[DataField]
public List<ProtoId<ContentTileDefinition>> TileWhitelist = new();
/// <summary>
/// A list of tile prototype IDs to avoid placing decals on.
/// </summary>
/// <remarks>
/// Ignored if TileWhitelist is set.
/// Note that due to the nature of tile-based placement, it's possible for decals to "spill over" onto nearby tiles.
/// This is mostly so dirt decals don't go on diagonal tiles that won't work for them.
/// </remarks>
[DataField]
public List<ProtoId<ContentTileDefinition>> TileBlacklist = new();
/// <summary>
/// Sets whether to delete the entity with this component after the spawner is finished.
/// </summary>
[DataField]
public bool DeleteSpawnerAfterSpawn = false;
}

View File

@@ -0,0 +1,130 @@
using System.Numerics;
using Content.Server.Decals;
using Content.Server.Spawners.Components;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Spawners.EntitySystems;
public sealed class RandomDecalSpawnerSystem : EntitySystem
{
[Dependency] private readonly DecalSystem _decal = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefs = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RandomDecalSpawnerComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(EntityUid uid, RandomDecalSpawnerComponent component, MapInitEvent args)
{
TrySpawn(uid);
if (component.DeleteSpawnerAfterSpawn)
QueueDel(uid);
}
public bool TrySpawn(Entity<RandomDecalSpawnerComponent?> ent)
{
if (!TryComp<RandomDecalSpawnerComponent>(ent, out var comp))
return false;
if (comp.Decals.Count == 0)
return false;
var tileWhitelist = new List<ITileDefinition>();
if (comp.TileWhitelist.Count > 0)
{
foreach (var tileProto in comp.TileWhitelist)
{
if (_tileDefs.TryGetDefinition(tileProto, out var tileDef))
tileWhitelist.Add(tileDef);
}
}
else if (comp.TileBlacklist.Count > 0)
{
foreach (var tileDef in _tileDefs)
{
if (!comp.TileBlacklist.Contains(tileDef.ID))
tileWhitelist.Add(tileDef);
}
}
var xform = Transform(ent);
if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
return false;
var addedDecals = new Dictionary<string, int>();
for (var i = 0; i < comp.MaxDecals; i++)
{
if (comp.Prob < 1f && _random.NextFloat() > comp.Prob)
continue;
// The vector added here is just to center the generated decals to the tile the spawner is on.
var localPos = xform.Coordinates.Position + _random.NextVector2(comp.Radius) + new Vector2(-0.5f, -0.5f);
var position = new EntityCoordinates(xform.GridUid.Value, localPos);
var tileRef = _map.GetTileRef(xform.GridUid.Value, grid, position);
if (tileWhitelist.Count > 0)
{
_tileDefs.TryGetDefinition(tileRef.Tile.TypeId, out var currTileDef);
if (currTileDef is null || !tileWhitelist.Contains(currTileDef))
continue;
}
var tileRefStr = tileRef.ToString();
if (comp.MaxDecalsPerTile is > 0)
{
addedDecals.TryAdd(tileRefStr, 0);
if (addedDecals[tileRefStr] >= comp.MaxDecalsPerTile)
continue;
}
var decalProtoId = _random.Pick(comp.Decals);
var decalProto = _prototypes.Index(decalProtoId);
var snapPosition = comp.SnapPosition ?? decalProto.DefaultSnap;
if (snapPosition)
{
position = position.WithPosition(tileRef.GridIndices * grid.TileSize);
}
var cleanable = comp.Cleanable ?? decalProto.DefaultCleanable;
var rotation = Angle.Zero;
if (comp.RandomRotation)
{
if (comp.SnapRotation)
rotation = new Angle((MathF.PI / 2f) * _random.Next(3));
else
rotation = _random.NextAngle();
}
var color = comp.Color;
if (comp.RandomColorList != null && comp.RandomColorList.Count != 0)
color = _random.Pick(comp.RandomColorList);
_decal.TryAddDecal(
decalProtoId,
position,
out _,
color,
rotation,
comp.ZIndex,
cleanable
);
if (comp.MaxDecalsPerTile is > 0)
addedDecals[tileRefStr]++;
}
return true;
}
}

View File

@@ -0,0 +1,10 @@
- type: entity
parent: MarkerBase
id: DecalSpawnerBase
name: Decal Spawner
abstract: true
components:
- type: Sprite
layers:
- state: green
- state: decal

View File

@@ -0,0 +1,157 @@
- type: entity
parent: DecalSpawnerBase
id: DecalSpawnerDirtBase
abstract: true
components:
- type: RandomDecalSpawner
decals:
- DirtLight
- DirtMedium
- DirtHeavy
maxDecalsPerTile: 1
snapPosition: true
zIndex: 1
prob: 0.8
color: '#FFFFFF7F'
cleanable: true
tileBlacklist: # Everything here just looks bad if it gets regular dirt on it
- FloorSteelMono
- FloorSteelDiagonal
- FloorSteelOffset
- FloorSteelDiagonalMini
- FloorWood
- FloorWoodLarge
- FloorWhiteMono
- FloorWhiteDiagonal
- FloorWhiteOffset
- FloorWhiteDiagonalMini
- FloorDarkMono
- FloorDarkDiagonal
- FloorDarkOffset
- FloorDarkDiagonalMini
- FloorArcadeBlue
- FloorArcadeBlue2
- FloorArcadeRed
- FloorEighties
- FloorCarpetClown
- FloorCarpetOffice
- FloorBoxing
- FloorGym
- FloorGlass
- FloorRGlass
- FloorAstroGrass
- FloorMowedAstroGrass
- FloorJungleAstroGrass
- FloorAstroIce
- FloorAstroSnow
- FloorAstroAsteroidSand
- FloorFlesh
- FloorAsteroidSandUnvariantized
- FloorAsteroidIronsandUnvariantized
- FloorCave
- FloorAsteroidIronsand
- FloorAsteroidTile
- FloorAsteroidSandDug
- FloorAsteroidSand
- FloorDirt
- FloorGrassLight
- FloorGrassDark
- FloorGrassJungle
- FloorGrass
- FloorAsphalt
- FloorReinforced
- FloorLino
deleteSpawnerAfterSpawn: true
- type: entity
parent: DecalSpawnerDirtBase
id: DecalSpawnerDirtSingle
suffix: Dirt, 0 Radius
components:
- type: RandomDecalSpawner
radius: 0
prob: 1.0
maxDecals: 1
- type: entity
parent: DecalSpawnerDirtBase
id: DecalSpawnerDirtNear
suffix: Dirt, 1.5 Radius
components:
- type: RandomDecalSpawner
radius: 1.5
maxDecals: 5
- type: entity
parent: DecalSpawnerDirtBase
id: DecalSpawnerDirtWide
suffix: Dirt, 3 Radius
components:
- type: RandomDecalSpawner
radius: 3
maxDecals: 20
maxDecalsPerTile: 2
- type: entity
parent: DecalSpawnerDirtBase
id: DecalSpawnerDirtMonospace
suffix: Dirt, Monospace, 1 Radius
components:
- type: RandomDecalSpawner
radius: 1
maxDecals: 5
decals:
- Dirt
- DirtHeavyMonotile
tileBlacklist:
- FloorSteelDiagonal
- FloorSteelOffset
- FloorSteelDiagonalMini
- FloorWhiteDiagonal
- FloorWhiteOffset
- FloorWhiteDiagonalMini
- FloorDarkDiagonal
- FloorDarkOffset
- FloorDarkDiagonalMini
- FloorArcadeBlue
- FloorArcadeBlue2
- FloorArcadeRed
- FloorEighties
- FloorCarpetClown
- FloorCarpetOffice
- FloorBoxing
- FloorGym
- FloorAstroGrass
- FloorMowedAstroGrass
- FloorJungleAstroGrass
- FloorAstroIce
- FloorAstroSnow
- FloorAstroAsteroidSand
- FloorFlesh
- FloorAsteroidSandUnvariantized
- FloorAsteroidIronsandUnvariantized
- FloorCave
- FloorAsteroidIronsand
- FloorAsteroidTile
- FloorAsteroidSandDug
- FloorAsteroidSand
- FloorDirt
- FloorGrassLight
- FloorGrassDark
- FloorGrassJungle
- FloorGrass
- FloorAsphalt
- FloorReinforced
- type: entity
parent: DecalSpawnerDirtMonospace
id: DecalSpawnerBurns
suffix: Burns, 1 Radius
components:
- type: RandomDecalSpawner
decals:
- burnt1
- burnt2
- burnt3
- burnt4
radius: 1

View File

@@ -0,0 +1,183 @@
- type: entity
parent: DecalSpawnerBase
id: DecalSpawnerFloraBase
abstract: true
components:
- type: RandomDecalSpawner
radius: 0.3
zIndex: 1
deleteSpawnerAfterSpawn: true
tileWhitelist:
- FloorAstroGrass
- FloorMowedAstroGrass
- FloorJungleAstroGrass
- FloorAstroIce
- FloorAstroSnow
- FloorAstroAsteroidSand
- FloorAsteroidSandUnvariantized
- FloorAsteroidIronsandUnvariantized
- FloorCave
- FloorAsteroidIronsand
- FloorAsteroidTile
- FloorAsteroidSandDug
- FloorAsteroidSand
- FloorDirt
- FloorGrassLight
- FloorGrassDark
- FloorGrassJungle
- FloorGrass
- type: entity
parent: DecalSpawnerFloraBase
id: DecalSpawnerGrassyRock
suffix: Grassy Rock
components:
- type: Sprite
layers:
- state: green
- state: decal
- sprite: Decals/Flora/flora_rocks.rsi
state: rock01
- type: RandomDecalSpawner
decals:
- Rock01
- Rock02
- Rock03
- Rock04
- Rock05
- Rock06
- Rock07
- type: entity
parent: DecalSpawnerFloraBase
id: DecalSpawnerBasaltRock
suffix: Basalt Rock
components:
- type: Sprite
layers:
- state: green
- state: decal
- sprite: Decals/basalt.rsi
state: basalt1
- type: RandomDecalSpawner
decals:
- Basalt1
- Basalt2
- Basalt3
- Basalt4
- Basalt5
- Basalt6
- Basalt7
- Basalt8
- Basalt9
- type: entity
parent: DecalSpawnerFloraBase
id: DecalSpawnerBushesAC
suffix: Bushes (a-c)
components:
- type: Sprite
layers:
- state: green
- state: decal
- sprite: Decals/Flora/flora_bushes.rsi
state: busha1
- type: RandomDecalSpawner
decals:
- Busha1
- Busha2
- Busha3
- Bushb1
- Bushb2
- Bushb3
- Bushc1
- Bushc2
- Bushc3
# I really don't want to add all the rest of the bushes right now, leaving this as an exercise for someone else.
- type: entity
parent: DecalSpawnerFloraBase
id: DecalSpawnerFlowers
suffix: Flowers
components:
- type: Sprite
layers:
- state: green
- state: decal
- sprite: Decals/Flora/flora_flowers.rsi
state: flowersbr1
- type: RandomDecalSpawner
decals:
- Flowersbr1
- Flowersbr2
- Flowersbr3
- Flowerspv1
- Flowerspv2
- Flowerspv3
- Flowersy1
- Flowersy2
- Flowersy3
- Flowersy4
- type: entity
parent: DecalSpawnerFloraBase
id: DecalSpawnerGrassAB
suffix: Grass (a-b)
components:
- type: Sprite
layers:
- state: green
- state: decal
- sprite: Decals/Flora/flora_grass.rsi
state: grassa1
- type: RandomDecalSpawner
decals:
- Grassa1
- Grassa2
- Grassa3
- Grassa4
- Grassa5
- Grassb1
- Grassb2
- Grassb3
- Grassb4
- Grassb5
- type: entity
parent: DecalSpawnerFloraBase
id: DecalSpawnerGrassC
suffix: Grass (c)
components:
- type: Sprite
layers:
- state: green
- state: decal
- sprite: Decals/Flora/flora_grass.rsi
state: grassc1
- type: RandomDecalSpawner
decals:
- Grassc1
- Grassc2
- Grassc3
- Grassc4
- type: entity
parent: DecalSpawnerFloraBase
id: DecalSpawnerGrassDE
suffix: Grass (d-e)
components:
- type: Sprite
layers:
- state: green
- state: decal
- sprite: Decals/Flora/flora_grass.rsi
state: grassd1
- type: RandomDecalSpawner
decals:
- Grassd1
- Grassd2
- Grassd3
- Grasse1
- Grasse2
- Grasse3

View File

@@ -0,0 +1,102 @@
- type: entity
parent: DecalSpawnerBase
id: DecalSpawnerBloodSplatters
suffix: Blood Splatters, Footprints
components:
- type: RandomDecalSpawner
decals:
- footprint
- splatter
radius: 1
randomRotation: true
maxDecals: 3
prob: 0.5
zIndex: 1
color: '#9900007F'
cleanable: true
deleteSpawnerAfterSpawn: true
- type: entity
parent: DecalSpawnerBase
id: DecalSpawnerGraffiti
suffix: Graffiti
components:
- type: RandomDecalSpawner
decals:
- Blasto
- Clandestine
- Cyber
- Diablo
- Donk
- Gene
- Gib
- Max
- Newton
- North
- Omni
- Osiron
- Prima
- Psyke
- Sirius
- Tunnel
- Waffle
- amyjon
- beepsky
- biohazard
- blueprint
- bottle
- carp
- cat
- clown
- corgi
- credit
- cyka
- danger
- disk
- dwarf
- end
- engie
- face
- fireaxe
- firedanger
- ghost
- guy
- heart
- like
- matt
- peace
- prolizard
- radiation
- revolution
- safe
- scroll
- shotgun
- skull
- snake
- star
- stickman
- taser
- toilet
- toolbox
- uboa
radius: 0.5
randomRotation: true
maxDecals: 1
zIndex: 2
randomColorList:
- aqua
- betterviolet
- blue
- chartreuse
- cyan
- deeppink
- fuchsia
- green
- indigo
- lime
- pink
- red
- silver
- yellow
cleanable: true
deleteSpawnerAfterSpawn: true

View File

@@ -449,3 +449,11 @@
prob: 0.90
- id: SolarPanelDamageVariationPass
- id: SolarPanelEmptyVariationPass
- id: BasicDecalDirtVariationPass
- id: BasicDecalGraffitiVariationPass
- id: BasicDecalBrunsVariationPass
prob: 0.50
orGroup: monospaceDecals
- id: BasicDecalDirtMonospaceVariationPass
prob: 0.50
orGroup: monospaceDecals

View File

@@ -47,6 +47,46 @@
entities:
- id: RandomSpawner
- type: entity
id: BasicDecalDirtVariationPass
parent: BaseVariationPass
components:
- type: EntitySpawnVariationPass
tilesPerEntityAverage: 80
tilesPerEntityStdDev: 5
entities:
- id: DecalSpawnerDirtWide
- type: entity
id: BasicDecalGraffitiVariationPass
parent: BaseVariationPass
components:
- type: EntitySpawnVariationPass
tilesPerEntityAverage: 120
tilesPerEntityStdDev: 5
entities:
- id: DecalSpawnerGraffiti
- type: entity
id: BasicDecalBrunsVariationPass
parent: BaseVariationPass
components:
- type: EntitySpawnVariationPass
tilesPerEntityAverage: 120
tilesPerEntityStdDev: 10
entities:
- id: DecalSpawnerBurns
- type: entity
id: BasicDecalDirtMonospaceVariationPass
parent: BaseVariationPass
components:
- type: EntitySpawnVariationPass
tilesPerEntityAverage: 80
tilesPerEntityStdDev: 10
entities:
- id: DecalSpawnerDirtMonospace
- type: weightedRandomFillSolution
id: RandomFillTrashPuddle
fills:

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

View File

@@ -1,7 +1,7 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/e71d6c4fba5a51f99b81c295dcaec4fc2f58fb19/icons/mob/screen1.dmi - modified to add 'timed'",
"copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/e71d6c4fba5a51f99b81c295dcaec4fc2f58fb19/icons/mob/screen1.dmi - modified to add 'timed'. 'decal' by Southbridge.",
"size": {
"x": 32,
"y": 32
@@ -24,6 +24,9 @@
},
{
"name": "timed"
},
{
"name": "decal"
}
]
}