diff --git a/Content.Server/Spawners/Components/RandomDecalSpawnerComponent.cs b/Content.Server/Spawners/Components/RandomDecalSpawnerComponent.cs new file mode 100644 index 0000000000..50e9137cbd --- /dev/null +++ b/Content.Server/Spawners/Components/RandomDecalSpawnerComponent.cs @@ -0,0 +1,122 @@ +using Robust.Shared.Prototypes; +using Content.Shared.Maps; +using Content.Shared.Decals; + +namespace Content.Server.Spawners.Components; + +/// +/// This component spawns decals around the entity on MapInit. +/// See doc strings for the various parameters for more information. +/// +[RegisterComponent, EntityCategory("Spawner")] +public sealed partial class RandomDecalSpawnerComponent : Component +{ + /// + /// A list of decals to randomly select from when spawning. + /// + [DataField] + public List> Decals = new(); + + /// + /// Radius (in tiles) to spawn decals in. 0 will target only the tile the entity is on. + /// + [DataField] + public float Radius = 1f; + + /// + /// Probability that a particular decal gets spawned. + /// + [DataField] + public float Prob = 1f; + + /// + /// The maximum amount of decals to spawn across the entire radius. + /// + [DataField] + public int MaxDecals = 1; + + /// + /// The maximum amount of decals to spawn within a tile. + /// + /// + /// A value <= 0 or null is considered unlimited. + /// + [DataField] + public int? MaxDecalsPerTile = null; + + /// + /// Whether decals should have a random rotation applied to them. + /// + [DataField] + public bool RandomRotation = false; + + /// + /// Whether decals should snap to 90 degree orientations, does nothing if RandomRotation is false. + /// + [DataField] + public bool SnapRotation = false; + + /// + /// Whether decals should snap to the center omf a grid space or be placed randoly. + /// + /// + /// A null value will cause this to attempt to use the default value (DefaultSnap) for the decal. + /// + [DataField] + public bool? SnapPosition = false; + + /// + /// zIndex for the generated decals + /// + [DataField] + public int ZIndex = 0; + + /// + /// Color for the generated decals. Does nothing if RandomColorList is set. + /// + [DataField] + public Color Color = Color.White; + + /// + /// A random color to select from. Overrides Color if set. + /// + [DataField] + public List? RandomColorList = new(); + + /// + /// Whether the new decals are cleanable or not + /// + /// + /// A null value will cause this to attempt to use the default value (DefaultCleanable) for the decal. + /// + [DataField] + public bool? Cleanable = null; + + /// + /// A list of tile prototype IDs to only place decals on. + /// + /// + /// 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. + /// + [DataField] + public List> TileWhitelist = new(); + + /// + /// A list of tile prototype IDs to avoid placing decals on. + /// + /// + /// 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. + /// + [DataField] + public List> TileBlacklist = new(); + + /// + /// Sets whether to delete the entity with this component after the spawner is finished. + /// + [DataField] + public bool DeleteSpawnerAfterSpawn = false; +} diff --git a/Content.Server/Spawners/EntitySystems/RandomDecalSpawnerSystem.cs b/Content.Server/Spawners/EntitySystems/RandomDecalSpawnerSystem.cs new file mode 100644 index 0000000000..1182bc131c --- /dev/null +++ b/Content.Server/Spawners/EntitySystems/RandomDecalSpawnerSystem.cs @@ -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(OnMapInit); + } + + private void OnMapInit(EntityUid uid, RandomDecalSpawnerComponent component, MapInitEvent args) + { + TrySpawn(uid); + if (component.DeleteSpawnerAfterSpawn) + QueueDel(uid); + } + + public bool TrySpawn(Entity ent) + { + if (!TryComp(ent, out var comp)) + return false; + + if (comp.Decals.Count == 0) + return false; + + var tileWhitelist = new List(); + 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(xform.GridUid, out var grid)) + return false; + + var addedDecals = new Dictionary(); + + 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; + } +} diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/base.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/base.yml new file mode 100644 index 0000000000..3c5fc97678 --- /dev/null +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/base.yml @@ -0,0 +1,10 @@ +- type: entity + parent: MarkerBase + id: DecalSpawnerBase + name: Decal Spawner + abstract: true + components: + - type: Sprite + layers: + - state: green + - state: decal diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/dirt.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/dirt.yml new file mode 100644 index 0000000000..3286abe78e --- /dev/null +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/dirt.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/flora.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/flora.yml new file mode 100644 index 0000000000..5164ca7aea --- /dev/null +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/flora.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/splatters.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/splatters.yml new file mode 100644 index 0000000000..f498616fe8 --- /dev/null +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/Decals/splatters.yml @@ -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 diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index f5730ab00c..6056ac98d6 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -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 diff --git a/Resources/Prototypes/GameRules/variation.yml b/Resources/Prototypes/GameRules/variation.yml index d080965697..de7a0d7fab 100644 --- a/Resources/Prototypes/GameRules/variation.yml +++ b/Resources/Prototypes/GameRules/variation.yml @@ -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: diff --git a/Resources/Textures/Markers/cross.rsi/decal.png b/Resources/Textures/Markers/cross.rsi/decal.png new file mode 100644 index 0000000000..0db5120e58 Binary files /dev/null and b/Resources/Textures/Markers/cross.rsi/decal.png differ diff --git a/Resources/Textures/Markers/cross.rsi/meta.json b/Resources/Textures/Markers/cross.rsi/meta.json index 8ead41b4fb..9c46093c75 100644 --- a/Resources/Textures/Markers/cross.rsi/meta.json +++ b/Resources/Textures/Markers/cross.rsi/meta.json @@ -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" } ] }