diff --git a/Content.Client/Light/AfterLightTargetOverlay.cs b/Content.Client/Light/AfterLightTargetOverlay.cs new file mode 100644 index 000000000000..5cfe1e993b03 --- /dev/null +++ b/Content.Client/Light/AfterLightTargetOverlay.cs @@ -0,0 +1,58 @@ +using System.Numerics; +using Robust.Client.Graphics; +using Robust.Shared.Enums; + +namespace Content.Client.Light; + +/// +/// This exists just to copy to the light render target +/// +public sealed class AfterLightTargetOverlay : Overlay +{ + public override OverlaySpace Space => OverlaySpace.BeforeLighting; + + [Dependency] private readonly IOverlayManager _overlay = default!; + + public const int ContentZIndex = LightBlurOverlay.ContentZIndex + 1; + + public AfterLightTargetOverlay() + { + IoCManager.InjectDependencies(this); + ZIndex = ContentZIndex; + } + + protected override void Draw(in OverlayDrawArgs args) + { + var viewport = args.Viewport; + var worldHandle = args.WorldHandle; + + if (viewport.Eye == null) + return; + + var lightOverlay = _overlay.GetOverlay(); + var bounds = args.WorldBounds; + + // at 1-1 render scale it's mostly fine but at 4x4 it's way too fkn big + var newScale = viewport.RenderScale / 2f; + + var localMatrix = + viewport.LightRenderTarget.GetWorldToLocalMatrix(viewport.Eye, newScale); + var diff = (lightOverlay.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size); + var halfDiff = diff / 2; + + // Pixels -> Metres -> Half distance. + // If we're zoomed in need to enlarge the bounds further. + args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget, + () => + { + // We essentially need to draw the cropped version onto the lightrendertarget. + var subRegion = new UIBox2i(halfDiff.X, + halfDiff.Y, + viewport.LightRenderTarget.Size.X + halfDiff.X, + viewport.LightRenderTarget.Size.Y + halfDiff.Y); + + worldHandle.SetTransform(localMatrix); + worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion); + }, null); + } +} diff --git a/Content.Client/Light/BeforeLightTargetOverlay.cs b/Content.Client/Light/BeforeLightTargetOverlay.cs new file mode 100644 index 000000000000..add172c6f3c4 --- /dev/null +++ b/Content.Client/Light/BeforeLightTargetOverlay.cs @@ -0,0 +1,51 @@ +using System.Numerics; +using Robust.Client.Graphics; +using Robust.Shared.Enums; + +namespace Content.Client.Light; + +/// +/// Handles an enlarged lighting target so content can use large blur radii. +/// +public sealed class BeforeLightTargetOverlay : Overlay +{ + public override OverlaySpace Space => OverlaySpace.BeforeLighting; + + [Dependency] private readonly IClyde _clyde = default!; + + public IRenderTexture EnlargedLightTarget = default!; + public Box2Rotated EnlargedBounds; + + /// + /// In metres + /// + private float _skirting = 1.5f; + + public const int ContentZIndex = -10; + + public BeforeLightTargetOverlay() + { + IoCManager.InjectDependencies(this); + ZIndex = ContentZIndex; + } + + protected override void Draw(in OverlayDrawArgs args) + { + // Code is weird but I don't think engine should be enlarging the lighting render target arbitrarily either, maybe via cvar? + // The problem is the blur has no knowledge of pixels outside the viewport so with a large enough blur radius you get sampling issues. + var size = args.Viewport.LightRenderTarget.Size + (int) (_skirting * EyeManager.PixelsPerMeter); + EnlargedBounds = args.WorldBounds.Enlarged(_skirting / 2f); + + // This just exists to copy the lightrendertarget and write back to it. + if (EnlargedLightTarget?.Size != size) + { + EnlargedLightTarget = _clyde + .CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-copy"); + } + + args.WorldHandle.RenderInRenderTarget(EnlargedLightTarget, + () => + { + }, _clyde.GetClearColor(args.MapUid)); + } +} diff --git a/Content.Client/Light/EntitySystems/PlanetLightSystem.cs b/Content.Client/Light/EntitySystems/PlanetLightSystem.cs new file mode 100644 index 000000000000..2da67137ed10 --- /dev/null +++ b/Content.Client/Light/EntitySystems/PlanetLightSystem.cs @@ -0,0 +1,36 @@ +using Robust.Client.Graphics; + +namespace Content.Client.Light.EntitySystems; + +public sealed class PlanetLightSystem : EntitySystem +{ + [Dependency] private readonly IOverlayManager _overlayMan = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnClearColor); + + _overlayMan.AddOverlay(new BeforeLightTargetOverlay()); + _overlayMan.AddOverlay(new RoofOverlay(EntityManager)); + _overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager)); + _overlayMan.AddOverlay(new LightBlurOverlay()); + _overlayMan.AddOverlay(new AfterLightTargetOverlay()); + } + + private void OnClearColor(ref GetClearColorEvent ev) + { + ev.Color = Color.Transparent; + } + + public override void Shutdown() + { + base.Shutdown(); + _overlayMan.RemoveOverlay(); + _overlayMan.RemoveOverlay(); + _overlayMan.RemoveOverlay(); + _overlayMan.RemoveOverlay(); + _overlayMan.RemoveOverlay(); + } +} diff --git a/Content.Client/Light/EntitySystems/RoofSystem.cs b/Content.Client/Light/EntitySystems/RoofSystem.cs new file mode 100644 index 000000000000..559eea40fc02 --- /dev/null +++ b/Content.Client/Light/EntitySystems/RoofSystem.cs @@ -0,0 +1,9 @@ +using Content.Shared.Light.EntitySystems; + +namespace Content.Client.Light.EntitySystems; + +/// +public sealed class RoofSystem : SharedRoofSystem +{ + +} diff --git a/Content.Client/Light/LightBlurOverlay.cs b/Content.Client/Light/LightBlurOverlay.cs new file mode 100644 index 000000000000..ae0684f9ffcc --- /dev/null +++ b/Content.Client/Light/LightBlurOverlay.cs @@ -0,0 +1,44 @@ +using Robust.Client.Graphics; +using Robust.Shared.Enums; + +namespace Content.Client.Light; + +/// +/// Essentially handles blurring for content-side light overlays. +/// +public sealed class LightBlurOverlay : Overlay +{ + public override OverlaySpace Space => OverlaySpace.BeforeLighting; + + [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IOverlayManager _overlay = default!; + + public const int ContentZIndex = TileEmissionOverlay.ContentZIndex + 1; + + private IRenderTarget? _blurTarget; + + public LightBlurOverlay() + { + IoCManager.InjectDependencies(this); + ZIndex = ContentZIndex; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (args.Viewport.Eye == null) + return; + + var beforeOverlay = _overlay.GetOverlay(); + var size = beforeOverlay.EnlargedLightTarget.Size; + + if (_blurTarget?.Size != size) + { + _blurTarget = _clyde + .CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-blur"); + } + + var target = beforeOverlay.EnlargedLightTarget; + // Yeah that's all this does keep walkin. + _clyde.BlurRenderTarget(args.Viewport, target, _blurTarget, args.Viewport.Eye, 14f * 2f); + } +} diff --git a/Content.Client/Light/LightCycleSystem.cs b/Content.Client/Light/LightCycleSystem.cs new file mode 100644 index 000000000000..9e19423cc33c --- /dev/null +++ b/Content.Client/Light/LightCycleSystem.cs @@ -0,0 +1,33 @@ +using Content.Client.GameTicking.Managers; +using Content.Shared; +using Content.Shared.Light.Components; +using Robust.Shared.Map.Components; +using Robust.Shared.Timing; + +namespace Content.Client.Light; + +/// +public sealed class LightCycleSystem : SharedLightCycleSystem +{ + [Dependency] private readonly ClientGameTicker _ticker = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + var mapQuery = AllEntityQuery(); + while (mapQuery.MoveNext(out var uid, out var cycle, out var map)) + { + if (!cycle.Running) + continue; + + var time = (float) _timing.CurTime + .Add(cycle.Offset) + .Subtract(_ticker.RoundStartTimeSpan) + .TotalSeconds; + + var color = GetColor((uid, cycle), cycle.OriginalColor, time); + map.AmbientLightColor = color; + } + } +} diff --git a/Content.Client/Light/RoofOverlay.cs b/Content.Client/Light/RoofOverlay.cs new file mode 100644 index 000000000000..981edf793c8e --- /dev/null +++ b/Content.Client/Light/RoofOverlay.cs @@ -0,0 +1,100 @@ +using System.Numerics; +using Content.Shared.Light.Components; +using Content.Shared.Maps; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Map.Components; + +namespace Content.Client.Light; + +public sealed class RoofOverlay : Overlay +{ + private readonly IEntityManager _entManager; + [Dependency] private readonly IOverlayManager _overlay = default!; + + private readonly EntityLookupSystem _lookup; + private readonly SharedMapSystem _mapSystem; + private readonly SharedTransformSystem _xformSystem; + + private readonly HashSet> _occluders = new(); + + public override OverlaySpace Space => OverlaySpace.BeforeLighting; + + public const int ContentZIndex = BeforeLightTargetOverlay.ContentZIndex + 1; + + public RoofOverlay(IEntityManager entManager) + { + _entManager = entManager; + IoCManager.InjectDependencies(this); + + _lookup = _entManager.System(); + _mapSystem = _entManager.System(); + _xformSystem = _entManager.System(); + + ZIndex = ContentZIndex; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (args.Viewport.Eye == null) + return; + + var mapEnt = _mapSystem.GetMap(args.MapId); + + if (!_entManager.TryGetComponent(mapEnt, out RoofComponent? roofComp) || + !_entManager.TryGetComponent(mapEnt, out MapGridComponent? grid)) + { + return; + } + + var viewport = args.Viewport; + var eye = args.Viewport.Eye; + + var worldHandle = args.WorldHandle; + var lightoverlay = _overlay.GetOverlay(); + var bounds = lightoverlay.EnlargedBounds; + var target = lightoverlay.EnlargedLightTarget; + + worldHandle.RenderInRenderTarget(target, + () => + { + var invMatrix = target.GetWorldToLocalMatrix(eye, viewport.RenderScale / 2f); + + var gridMatrix = _xformSystem.GetWorldMatrix(mapEnt); + var matty = Matrix3x2.Multiply(gridMatrix, invMatrix); + + worldHandle.SetTransform(matty); + + var tileEnumerator = _mapSystem.GetTilesEnumerator(mapEnt, grid, bounds); + + // Due to stencilling we essentially draw on unrooved tiles + while (tileEnumerator.MoveNext(out var tileRef)) + { + if ((tileRef.Tile.Flags & (byte) TileFlag.Roof) == 0x0) + { + // Check if the tile is occluded in which case hide it anyway. + // This is to avoid lit walls bleeding over to unlit tiles. + _occluders.Clear(); + _lookup.GetLocalEntitiesIntersecting(mapEnt, tileRef.GridIndices, _occluders); + var found = false; + + foreach (var occluder in _occluders) + { + if (!occluder.Comp.Enabled) + continue; + + found = true; + break; + } + + if (!found) + continue; + } + + var local = _lookup.GetLocalBounds(tileRef, grid.TileSize); + worldHandle.DrawRect(local, roofComp.Color); + } + + }, null); + } +} diff --git a/Content.Client/Light/TileEmissionOverlay.cs b/Content.Client/Light/TileEmissionOverlay.cs new file mode 100644 index 000000000000..bccc6fc21e67 --- /dev/null +++ b/Content.Client/Light/TileEmissionOverlay.cs @@ -0,0 +1,90 @@ +using System.Numerics; +using Content.Shared.Light.Components; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; + +namespace Content.Client.Light; + +public sealed class TileEmissionOverlay : Overlay +{ + public override OverlaySpace Space => OverlaySpace.BeforeLighting; + + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IOverlayManager _overlay = default!; + + private SharedMapSystem _mapSystem; + private SharedTransformSystem _xformSystem; + + private readonly EntityLookupSystem _lookup; + + private readonly EntityQuery _xformQuery; + private readonly HashSet> _entities = new(); + + private List> _grids = new(); + + public const int ContentZIndex = RoofOverlay.ContentZIndex + 1; + + public TileEmissionOverlay(IEntityManager entManager) + { + IoCManager.InjectDependencies(this); + + _lookup = entManager.System(); + _mapSystem = entManager.System(); + _xformSystem = entManager.System(); + + _xformQuery = entManager.GetEntityQuery(); + ZIndex = ContentZIndex; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (args.Viewport.Eye == null) + return; + + var mapId = args.MapId; + var worldHandle = args.WorldHandle; + var lightoverlay = _overlay.GetOverlay(); + var bounds = lightoverlay.EnlargedBounds; + var target = lightoverlay.EnlargedLightTarget; + var viewport = args.Viewport; + + args.WorldHandle.RenderInRenderTarget(target, + () => + { + var invMatrix = target.GetWorldToLocalMatrix(viewport.Eye, viewport.RenderScale / 2f); + _grids.Clear(); + _mapManager.FindGridsIntersecting(mapId, bounds, ref _grids, approx: true); + + foreach (var grid in _grids) + { + var gridInvMatrix = _xformSystem.GetInvWorldMatrix(grid); + var localBounds = gridInvMatrix.TransformBox(bounds); + _entities.Clear(); + _lookup.GetLocalEntitiesIntersecting(grid.Owner, localBounds, _entities); + + if (_entities.Count == 0) + continue; + + var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner); + + foreach (var ent in _entities) + { + var xform = _xformQuery.Comp(ent); + + var tile = _mapSystem.LocalToTile(grid.Owner, grid, xform.Coordinates); + var matty = Matrix3x2.Multiply(gridMatrix, invMatrix); + + worldHandle.SetTransform(matty); + + // Yes I am fully aware this leads to overlap. If you really want to have alpha then you'll need + // to turn the squares into polys. + // Additionally no shadows so if you make it too big it's going to go through a 1x wall. + var local = _lookup.GetLocalBounds(tile, grid.Comp.TileSize).Enlarged(ent.Comp.Range); + worldHandle.DrawRect(local, ent.Comp.Color); + } + } + }, null); + } +} diff --git a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs index 213da5d78626..1bfcd8ab6088 100644 --- a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs +++ b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs @@ -39,12 +39,12 @@ await server.WaitAssertion(() => { var mapGrid = mapManager.CreateGridEntity(mapId); xformSystem.SetWorldPosition(mapGrid, new Vector2(10, 10)); - mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(1, (TileRenderFlag) 1, 255)); + mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(typeId: 1, flags: 1, variant: 255)); } { var mapGrid = mapManager.CreateGridEntity(mapId); xformSystem.SetWorldPosition(mapGrid, new Vector2(-8, -8)); - mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(2, (TileRenderFlag) 1, 254)); + mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(typeId: 2, flags: 1, variant: 254)); } Assert.Multiple(() => mapLoader.SaveMap(mapId, mapPath)); @@ -73,7 +73,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(xformSystem.GetWorldPosition(gridXform), Is.EqualTo(new Vector2(10, 10))); - Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(1, (TileRenderFlag) 1, 255))); + Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(typeId: 1, flags: 1, variant: 255))); }); } { @@ -87,7 +87,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(xformSystem.GetWorldPosition(gridXform), Is.EqualTo(new Vector2(-8, -8))); - Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(2, (TileRenderFlag) 1, 254))); + Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(typeId: 2, flags: 1, variant: 254))); }); } }); diff --git a/Content.Server/Light/Components/SetRoofComponent.cs b/Content.Server/Light/Components/SetRoofComponent.cs new file mode 100644 index 000000000000..6bfe64aa03dd --- /dev/null +++ b/Content.Server/Light/Components/SetRoofComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.Light.Components; + +/// +/// Applies the roof flag to this tile and deletes the entity. +/// +[RegisterComponent] +public sealed partial class SetRoofComponent : Component +{ + [DataField(required: true)] + public bool Value; +} diff --git a/Content.Server/Light/EntitySystems/LightCycleSystem.cs b/Content.Server/Light/EntitySystems/LightCycleSystem.cs new file mode 100644 index 000000000000..7d2eacc8bbbe --- /dev/null +++ b/Content.Server/Light/EntitySystems/LightCycleSystem.cs @@ -0,0 +1,22 @@ +using Content.Shared; +using Content.Shared.Light.Components; +using Robust.Shared.Random; + +namespace Content.Server.Light.EntitySystems; + +/// +public sealed class LightCycleSystem : SharedLightCycleSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void OnCycleMapInit(Entity ent, ref MapInitEvent args) + { + base.OnCycleMapInit(ent, ref args); + + if (ent.Comp.InitialOffset) + { + ent.Comp.Offset = _random.Next(ent.Comp.Duration); + Dirty(ent); + } + } +} diff --git a/Content.Server/Light/EntitySystems/RoofSystem.cs b/Content.Server/Light/EntitySystems/RoofSystem.cs new file mode 100644 index 000000000000..a3b8cb183618 --- /dev/null +++ b/Content.Server/Light/EntitySystems/RoofSystem.cs @@ -0,0 +1,33 @@ +using Content.Server.Light.Components; +using Content.Shared.Light.EntitySystems; +using Robust.Shared.Map.Components; + +namespace Content.Server.Light.EntitySystems; + +/// +public sealed class RoofSystem : SharedRoofSystem +{ + [Dependency] private readonly SharedMapSystem _maps = default!; + + private EntityQuery _gridQuery; + + public override void Initialize() + { + base.Initialize(); + _gridQuery = GetEntityQuery(); + SubscribeLocalEvent(OnFlagStartup); + } + + private void OnFlagStartup(Entity ent, ref ComponentStartup args) + { + var xform = Transform(ent.Owner); + + if (_gridQuery.TryComp(xform.GridUid, out var grid)) + { + var index = _maps.LocalToTile(xform.GridUid.Value, grid, xform.Coordinates); + SetRoof((xform.GridUid.Value, grid, null), index, ent.Comp.Value); + } + + QueueDel(ent.Owner); + } +} diff --git a/Content.Server/Parallax/BiomeSystem.cs b/Content.Server/Parallax/BiomeSystem.cs index 109aa0f6e470..e419e90fd38d 100644 --- a/Content.Server/Parallax/BiomeSystem.cs +++ b/Content.Server/Parallax/BiomeSystem.cs @@ -12,6 +12,7 @@ using Content.Shared.Decals; using Content.Shared.Ghost; using Content.Shared.Gravity; +using Content.Shared.Light.Components; using Content.Shared.Parallax.Biomes; using Content.Shared.Parallax.Biomes.Layers; using Content.Shared.Parallax.Biomes.Markers; @@ -330,6 +331,9 @@ public override void Update(float frameTime) while (biomes.MoveNext(out var biome)) { + if (biome.LifeStage < ComponentLifeStage.Running) + continue; + _activeChunks.Add(biome, _tilePool.Get()); _markerChunks.GetOrNew(biome); } @@ -379,6 +383,10 @@ public override void Update(float frameTime) while (loadBiomes.MoveNext(out var gridUid, out var biome, out var grid)) { + // If not MapInit don't run it. + if (biome.LifeStage < ComponentLifeStage.Running) + continue; + if (!biome.Enabled) continue; @@ -745,7 +753,10 @@ private void LoadChunkMarkers( } if (modified.Count == 0) + { + component.ModifiedTiles.Remove(chunk); _tilePool.Return(modified); + } component.PendingMarkers.Remove(chunk); } @@ -1014,11 +1025,14 @@ public void EnsurePlanet(EntityUid mapUid, BiomeTemplatePrototype biomeTemplate, // Midday: #E6CB8B // Moonlight: #2b3143 // Lava: #A34931 - var light = EnsureComp(mapUid); light.AmbientLightColor = mapLight ?? Color.FromHex("#D8B059"); Dirty(mapUid, light, metadata); + EnsureComp(mapUid); + + EnsureComp(mapUid); + var moles = new float[Atmospherics.AdjustedNumberOfGases]; moles[(int) Gas.Oxygen] = 21.824779f; moles[(int) Gas.Nitrogen] = 82.10312f; diff --git a/Content.Shared/Light/Components/LightCycleComponent.cs b/Content.Shared/Light/Components/LightCycleComponent.cs new file mode 100644 index 000000000000..a6ce63bdbc2b --- /dev/null +++ b/Content.Shared/Light/Components/LightCycleComponent.cs @@ -0,0 +1,56 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Map.Components; + +namespace Content.Shared.Light.Components; + +/// +/// Cycles through colors AKA "Day / Night cycle" on +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class LightCycleComponent : Component +{ + [DataField, AutoNetworkedField] + public Color OriginalColor = Color.Transparent; + + /// + /// How long an entire cycle lasts + /// + [DataField, AutoNetworkedField] + public TimeSpan Duration = TimeSpan.FromMinutes(30); + + [DataField, AutoNetworkedField] + public TimeSpan Offset; + + [DataField, AutoNetworkedField] + public bool Enabled = true; + + /// + /// Should the offset be randomised upon MapInit. + /// + [DataField, AutoNetworkedField] + public bool InitialOffset = true; + + /// + /// Trench of the oscillation. + /// + [DataField, AutoNetworkedField] + public float MinLightLevel = 0f; + + /// + /// Peak of the oscillation + /// + [DataField, AutoNetworkedField] + public float MaxLightLevel = 3f; + + [DataField, AutoNetworkedField] + public float ClipLight = 1.25f; + + [DataField, AutoNetworkedField] + public Color ClipLevel = new Color(1f, 1f, 1.25f); + + [DataField, AutoNetworkedField] + public Color MinLevel = new Color(0.1f, 0.15f, 0.50f); + + [DataField, AutoNetworkedField] + public Color MaxLevel = new Color(2f, 2f, 5f); +} diff --git a/Content.Shared/Light/Components/RoofComponent.cs b/Content.Shared/Light/Components/RoofComponent.cs new file mode 100644 index 000000000000..0e2adf527cda --- /dev/null +++ b/Content.Shared/Light/Components/RoofComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Light.Components; + +/// +/// Will draw shadows over tiles flagged as roof tiles on the attached map. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class RoofComponent : Component +{ + [DataField, AutoNetworkedField] + public Color Color = Color.Black; +} diff --git a/Content.Shared/Light/Components/TileEmissionComponent.cs b/Content.Shared/Light/Components/TileEmissionComponent.cs new file mode 100644 index 000000000000..0eec19702d81 --- /dev/null +++ b/Content.Shared/Light/Components/TileEmissionComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Light.Components; + +/// +/// Will draw lighting in a range around the tile. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class TileEmissionComponent : Component +{ + [DataField, AutoNetworkedField] + public float Range = 0.25f; + + [DataField(required: true), AutoNetworkedField] + public Color Color = Color.Transparent; +} diff --git a/Content.Shared/Light/EntitySystems/SharedRoofSystem.cs b/Content.Shared/Light/EntitySystems/SharedRoofSystem.cs new file mode 100644 index 000000000000..d06b5bcb0ce0 --- /dev/null +++ b/Content.Shared/Light/EntitySystems/SharedRoofSystem.cs @@ -0,0 +1,42 @@ +using Content.Shared.Light.Components; +using Content.Shared.Maps; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; + +namespace Content.Shared.Light.EntitySystems; + +/// +/// Handles the roof flag for tiles that gets used for the RoofOverlay. +/// +public abstract class SharedRoofSystem : EntitySystem +{ + [Dependency] private readonly SharedMapSystem _maps = default!; + + public void SetRoof(Entity grid, Vector2i index, bool value) + { + if (!Resolve(grid, ref grid.Comp1, ref grid.Comp2, false)) + return; + + if (!_maps.TryGetTile(grid.Comp1, index, out var tile)) + return; + + var mask = (tile.Flags & (byte)TileFlag.Roof); + var rooved = mask != 0x0; + + if (rooved == value) + return; + + Tile newTile; + + if (value) + { + newTile = tile.WithFlag((byte)(tile.Flags | (ushort)TileFlag.Roof)); + } + else + { + newTile = tile.WithFlag((byte)(tile.Flags & ~(ushort)TileFlag.Roof)); + } + + _maps.SetTile((grid.Owner, grid.Comp1), index, newTile); + } +} diff --git a/Content.Shared/Maps/ContentTileDefinition.cs b/Content.Shared/Maps/ContentTileDefinition.cs index 839d920df94a..86ceac77be7e 100644 --- a/Content.Shared/Maps/ContentTileDefinition.cs +++ b/Content.Shared/Maps/ContentTileDefinition.cs @@ -1,9 +1,11 @@ using Content.Shared.Atmos; +using Content.Shared.Light.Components; using Content.Shared.Movement.Systems; using Content.Shared.Tools; using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; using Robust.Shared.Utility; @@ -118,4 +120,11 @@ public void AssignTileId(ushort id) TileId = id; } } + + [Flags] + public enum TileFlag : byte + { + None = 0, + Roof = 1 << 0, + } } diff --git a/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs b/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs index 0ac9f1894c74..114b6b20b92a 100644 --- a/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs +++ b/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs @@ -1,28 +1,36 @@ using Content.Shared.Maps; using Robust.Shared.Noise; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Parallax.Biomes.Layers; [Serializable, NetSerializable] public sealed partial class BiomeTileLayer : IBiomeLayer { - [DataField("noise")] public FastNoiseLite Noise { get; private set; } = new(0); + [DataField] public FastNoiseLite Noise { get; private set; } = new(0); /// - [DataField("threshold")] + [DataField] public float Threshold { get; private set; } = 0.5f; /// - [DataField("invert")] public bool Invert { get; private set; } = false; + [DataField] public bool Invert { get; private set; } = false; /// /// Which tile variants to use for this layer. Uses all of the tile's variants if none specified /// - [DataField("variants")] + [DataField] public List? Variants = null; - [DataField("tile", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Tile = string.Empty; + [DataField(required: true)] + public ProtoId Tile = string.Empty; + + // TODO: Need some good engine solution to this, see FlagSerializer for what needs changing. + /// + /// Flags to set on the tile when placed. + /// + [DataField] + public byte Flags = 0; } diff --git a/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs b/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs index 250b0f70a54e..32a7823273f2 100644 --- a/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs +++ b/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs @@ -129,7 +129,7 @@ public bool TryGetTile(Vector2i indices, List layers, int seed, Map if (layer is not BiomeTileLayer tileLayer) continue; - if (TryGetTile(indices, noiseCopy, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index(tileLayer.Tile), tileLayer.Variants, out tile)) + if (TryGetTile(indices, noiseCopy, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index(tileLayer.Tile), tileLayer.Flags, tileLayer.Variants, out tile)) { return true; } @@ -142,7 +142,7 @@ public bool TryGetTile(Vector2i indices, List layers, int seed, Map /// /// Gets the underlying biome tile, ignoring any existing tile that may be there. /// - private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, float threshold, ContentTileDefinition tileDef, List? variants, [NotNullWhen(true)] out Tile? tile) + private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, float threshold, ContentTileDefinition tileDef, byte tileFlags, List? variants, [NotNullWhen(true)] out Tile? tile) { var found = noise.GetNoise(indices.X, indices.Y); found = invert ? found * -1 : found; @@ -163,7 +163,7 @@ private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, floa variant = _tile.PickVariant(tileDef, (int) variantValue); } - tile = new Tile(tileDef.TileId, 0, variant); + tile = new Tile(tileDef.TileId, flags: tileFlags, variant); return true; } diff --git a/Content.Shared/SharedLightCycleSystem.cs b/Content.Shared/SharedLightCycleSystem.cs new file mode 100644 index 000000000000..1ba947f78ac3 --- /dev/null +++ b/Content.Shared/SharedLightCycleSystem.cs @@ -0,0 +1,116 @@ +using Content.Shared.Light.Components; +using Robust.Shared.Map.Components; + +namespace Content.Shared; + +public abstract class SharedLightCycleSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnCycleMapInit); + SubscribeLocalEvent(OnCycleShutdown); + } + + protected virtual void OnCycleMapInit(Entity ent, ref MapInitEvent args) + { + if (TryComp(ent.Owner, out MapLightComponent? mapLight)) + { + ent.Comp.OriginalColor = mapLight.AmbientLightColor; + Dirty(ent); + } + } + + private void OnCycleShutdown(Entity ent, ref ComponentShutdown args) + { + if (TryComp(ent.Owner, out MapLightComponent? mapLight)) + { + mapLight.AmbientLightColor = ent.Comp.OriginalColor; + Dirty(ent.Owner, mapLight); + } + } + + public static Color GetColor(Entity cycle, Color color, float time) + { + if (cycle.Comp.Enabled) + { + var lightLevel = CalculateLightLevel(cycle.Comp, time); + var colorLevel = CalculateColorLevel(cycle.Comp, time); + return new Color( + (byte)Math.Min(255, color.RByte * colorLevel.R * lightLevel), + (byte)Math.Min(255, color.GByte * colorLevel.G * lightLevel), + (byte)Math.Min(255, color.BByte * colorLevel.B * lightLevel) + ); + } + + return color; + } + + /// + /// Calculates light intensity as a function of time. + /// + public static double CalculateLightLevel(LightCycleComponent comp, float time) + { + var waveLength = MathF.Max(1, (float) comp.Duration.TotalSeconds); + var crest = MathF.Max(0f, comp.MaxLightLevel); + var shift = MathF.Max(0f, comp.MinLightLevel); + return Math.Min(comp.ClipLight, CalculateCurve(time, waveLength, crest, shift, 6)); + } + + /// + /// It is important to note that each color must have a different exponent, to modify how early or late one color should stand out in relation to another. + /// This "simulates" what the atmosphere does and is what generates the effect of dawn and dusk. + /// The blue component must be a cosine function with half period, so that its minimum is at dawn and dusk, generating the "warm" color corresponding to these periods. + /// As you can see in the values, the maximums of the function serve more to define the curve behavior, + /// they must be "clipped" so as not to distort the original color of the lighting. In practice, the maximum values, in fact, are the clip thresholds. + /// + public static Color CalculateColorLevel(LightCycleComponent comp, float time) + { + var waveLength = MathF.Max(1f, (float) comp.Duration.TotalSeconds); + + var red = MathF.Min(comp.ClipLevel.R, + CalculateCurve(time, + waveLength, + MathF.Max(0f, comp.MaxLevel.R), + MathF.Max(0f, comp.MinLevel.R), + 4f)); + + var green = MathF.Min(comp.ClipLevel.G, + CalculateCurve(time, + waveLength, + MathF.Max(0f, comp.MaxLevel.G), + MathF.Max(0f, comp.MinLevel.G), + 10f)); + + var blue = MathF.Min(comp.ClipLevel.B, + CalculateCurve(time, + waveLength / 2f, + MathF.Max(0f, comp.MaxLevel.B), + MathF.Max(0f, comp.MinLevel.B), + 2, + waveLength / 4f)); + + return new Color(red, green, blue); + } + + /// + /// Generates a sinusoidal curve as a function of x (time). The other parameters serve to adjust the behavior of the curve. + /// + /// It corresponds to the independent variable of the function, which in the context of this algorithm is the current time. + /// It's the wavelength of the function, it can be said to be the total duration of the light cycle. + /// It's the maximum point of the function, where it will have its greatest value. + /// It's the vertical displacement of the function, in practice it corresponds to the minimum value of the function. + /// It is the exponent of the sine, serves to "flatten" the function close to its minimum points and make it "steeper" close to its maximum. + /// It changes the phase of the wave, like a "horizontal shift". It is important to transform the sinusoidal function into cosine, when necessary. + /// The result of the function. + public static float CalculateCurve(float x, + float waveLength, + float crest, + float shift, + float exponent, + float phase = 0) + { + var sen = MathF.Pow(MathF.Sin((MathF.PI * (phase + x)) / waveLength), exponent); + return (crest - shift) * sen + shift; + } +} diff --git a/Resources/Prototypes/Entities/Markers/tile.yml b/Resources/Prototypes/Entities/Markers/tile.yml new file mode 100644 index 000000000000..2ced9e9958de --- /dev/null +++ b/Resources/Prototypes/Entities/Markers/tile.yml @@ -0,0 +1,37 @@ +- type: entity + id: BaseRoofMarker + abstract: true + placement: + mode: SnapgridCenter + components: + - type: Transform + anchored: true + - type: Sprite + drawdepth: Overdoors + sprite: Markers/cross.rsi + +- type: entity + id: RoofMarker + name: Roof + suffix: Enabled + parent: BaseRoofMarker + components: + - type: SetRoof + value: true + - type: Sprite + layers: + - state: green + shader: unshaded + +- type: entity + id: NoRoofMarker + name: Roof + suffix: Disabled + parent: BaseRoofMarker + components: + - type: SetRoof + value: false + - type: Sprite + layers: + - state: red + shader: unshaded diff --git a/Resources/Prototypes/Entities/Structures/Walls/walls.yml b/Resources/Prototypes/Entities/Structures/Walls/walls.yml index 57e4daed5af6..278517e0e6a7 100644 --- a/Resources/Prototypes/Entities/Structures/Walls/walls.yml +++ b/Resources/Prototypes/Entities/Structures/Walls/walls.yml @@ -543,7 +543,7 @@ - type: Tag tags: - Wall - - Diagonal + - Diagonal - type: Sprite drawdepth: Walls sprite: Structures/Walls/plastitanium_diagonal.rsi diff --git a/Resources/Prototypes/Entities/Tiles/lava.yml b/Resources/Prototypes/Entities/Tiles/lava.yml index 36c7b80b81bf..68dd5671a06d 100644 --- a/Resources/Prototypes/Entities/Tiles/lava.yml +++ b/Resources/Prototypes/Entities/Tiles/lava.yml @@ -7,6 +7,8 @@ snap: - Wall components: + - type: TileEmission + color: "#FF4500" - type: StepTrigger requiredTriggeredSpeed: 0 intersectRatio: 0.1 diff --git a/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml b/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml index 869db085970b..ade23b6f711f 100644 --- a/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml +++ b/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml @@ -7,6 +7,8 @@ snap: - Wall components: + - type: TileEmission + color: "#974988" - type: StepTrigger requiredTriggeredSpeed: 0 intersectRatio: 0.1 diff --git a/Resources/Prototypes/Procedural/biome_templates.yml b/Resources/Prototypes/Procedural/biome_templates.yml index 588d95f40da5..45293f582f96 100644 --- a/Resources/Prototypes/Procedural/biome_templates.yml +++ b/Resources/Prototypes/Procedural/biome_templates.yml @@ -544,6 +544,7 @@ - !type:BiomeTileLayer threshold: -1.0 tile: FloorAsteroidSand + flags: 1 # Asteroid - type: biomeTemplate