Add sun shadows (planet lighting stage 2) (#35145)
* Implements a Dynamic Lighting System on maps. * Edit: the night should be a little bit brighter and blue now. * Major edit: everything must be done on the client side now, with certain datafield replicated. Changes were outlined in the salvage to accommodate the new lighting system. * Edit: The offset is now serverside, this makes the time accurate in all situations. * Removing ununsed import * Minor tweaks * Tweak in time precision * Minor tweak + Unused import removed * Edit: apparently RealTime is better for what I'm looking for * Fix: Now the time is calculated correctly. * Minor tweaks * Adds condition for when the light should be updated * Add planet lighting * she * close-ish * c * bittersweat * Fixes * Revert "Merge branch '22719' into 2024-09-29-planet-lighting" This reverts commit 9f2785bb16aee47d794aa3eed8ae15004f97fc35, reversing changes made to 19649c07a5fb625423e08fc18d91c9cb101daa86. * Europa and day-night * weh * rooves working * Clean * Remove Europa * Fixes * fix * Update * Fix caves * Update for engine * Add sun shadows (planet lighting v2) For now mostly targeting walls and having the shadows change over time. Got the basic proof-of-concept working just needs a hell of a lot of polish. * Documentation * a * Fixes * Move blur to an overlay * Slughands * Fixes * Apply RoofOverlay per-grid not per-map * Fix light render scales * sangas * Juice it a bit * Better angle * Fixes * Add color support * Rounding bandaid * Wehs * Better * Remember I forgot to do this when writing docs --------- Co-authored-by: DoutorWhite <thedoctorwhite@gmail.com>
This commit is contained in:
@@ -54,6 +54,6 @@ public sealed class AfterLightTargetOverlay : Overlay
|
||||
|
||||
worldHandle.SetTransform(localMatrix);
|
||||
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
|
||||
}, null);
|
||||
}, Color.Transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ public sealed class PlanetLightSystem : EntitySystem
|
||||
_overlayMan.AddOverlay(new RoofOverlay(EntityManager));
|
||||
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
|
||||
_overlayMan.AddOverlay(new LightBlurOverlay());
|
||||
_overlayMan.AddOverlay(new SunShadowOverlay());
|
||||
_overlayMan.AddOverlay(new AfterLightTargetOverlay());
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ public sealed class PlanetLightSystem : EntitySystem
|
||||
_overlayMan.RemoveOverlay<RoofOverlay>();
|
||||
_overlayMan.RemoveOverlay<TileEmissionOverlay>();
|
||||
_overlayMan.RemoveOverlay<LightBlurOverlay>();
|
||||
_overlayMan.RemoveOverlay<SunShadowOverlay>();
|
||||
_overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
|
||||
}
|
||||
}
|
||||
|
||||
92
Content.Client/Light/EntitySystems/SunShadowSystem.cs
Normal file
92
Content.Client/Light/EntitySystems/SunShadowSystem.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Light.EntitySystems;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Light.EntitySystems;
|
||||
|
||||
public sealed class SunShadowSystem : SharedSunShadowSystem
|
||||
{
|
||||
[Dependency] private readonly ClientGameTicker _ticker = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (!_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
var mapQuery = AllEntityQuery<SunShadowCycleComponent, SunShadowComponent>();
|
||||
while (mapQuery.MoveNext(out var uid, out var cycle, out var shadow))
|
||||
{
|
||||
if (!cycle.Running || cycle.Directions.Count == 0)
|
||||
continue;
|
||||
|
||||
var pausedTime = _metadata.GetPauseTime(uid);
|
||||
|
||||
var time = (float)(_timing.CurTime
|
||||
.Add(cycle.Offset)
|
||||
.Subtract(_ticker.RoundStartTimeSpan)
|
||||
.Subtract(pausedTime)
|
||||
.TotalSeconds % cycle.Duration.TotalSeconds);
|
||||
|
||||
var (direction, alpha) = GetShadow((uid, cycle), time);
|
||||
shadow.Direction = direction;
|
||||
shadow.Alpha = alpha;
|
||||
}
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public (Vector2 Direction, float Alpha) GetShadow(Entity<SunShadowCycleComponent> entity, float time)
|
||||
{
|
||||
// So essentially the values are stored as the percentages of the total duration just so it adjusts the speed
|
||||
// dynamically and we don't have to manually handle it.
|
||||
// It will lerp from each value to the next one with angle and length handled separately
|
||||
var ratio = (float) (time / entity.Comp.Duration.TotalSeconds);
|
||||
|
||||
for (var i = entity.Comp.Directions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var dir = entity.Comp.Directions[i];
|
||||
|
||||
if (ratio > dir.Ratio)
|
||||
{
|
||||
var next = entity.Comp.Directions[(i + 1) % entity.Comp.Directions.Count];
|
||||
float nextRatio;
|
||||
|
||||
// Last entry
|
||||
if (i == entity.Comp.Directions.Count - 1)
|
||||
{
|
||||
nextRatio = next.Ratio + 1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextRatio = next.Ratio;
|
||||
}
|
||||
|
||||
var range = nextRatio - dir.Ratio;
|
||||
var diff = (ratio - dir.Ratio) / range;
|
||||
DebugTools.Assert(diff is >= 0f and <= 1f);
|
||||
|
||||
// We lerp angle + length separately as we don't want a straight-line lerp and want the rotation to be consistent.
|
||||
var currentAngle = dir.Direction.ToAngle();
|
||||
var nextAngle = next.Direction.ToAngle();
|
||||
|
||||
var angle = Angle.Lerp(currentAngle, nextAngle, diff);
|
||||
// This is to avoid getting weird issues where the angle gets pretty close but length still noticeably catches up.
|
||||
var lengthDiff = MathF.Pow(diff, 1f / 2f);
|
||||
var length = float.Lerp(dir.Direction.Length(), next.Direction.Length(), lengthDiff);
|
||||
|
||||
var vector = angle.ToVec() * length;
|
||||
var alpha = float.Lerp(dir.Alpha, next.Alpha, diff);
|
||||
return (vector, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Shared;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Light.EntitySystems;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -11,19 +12,29 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
|
||||
{
|
||||
[Dependency] private readonly ClientGameTicker _ticker = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (!_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
var mapQuery = AllEntityQuery<LightCycleComponent, MapLightComponent>();
|
||||
while (mapQuery.MoveNext(out var uid, out var cycle, out var map))
|
||||
{
|
||||
if (!cycle.Running)
|
||||
continue;
|
||||
|
||||
// We still iterate paused entities as we still want to override the lighting color and not have
|
||||
// it apply the server state
|
||||
var pausedTime = _metadata.GetPauseTime(uid);
|
||||
|
||||
var time = (float) _timing.CurTime
|
||||
.Add(cycle.Offset)
|
||||
.Subtract(_ticker.RoundStartTimeSpan)
|
||||
.Subtract(pausedTime)
|
||||
.TotalSeconds;
|
||||
|
||||
var color = GetColor((uid, cycle), cycle.OriginalColor, time);
|
||||
|
||||
@@ -94,13 +94,15 @@ public sealed class RoofOverlay : Overlay
|
||||
// Due to stencilling we essentially draw on unrooved tiles
|
||||
while (tileEnumerator.MoveNext(out var tileRef))
|
||||
{
|
||||
if (!_roof.IsRooved(roofEnt, tileRef.GridIndices))
|
||||
var color = _roof.GetColor(roofEnt, tileRef.GridIndices);
|
||||
|
||||
if (color == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var local = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize);
|
||||
worldHandle.DrawRect(local, roof.Color);
|
||||
worldHandle.DrawRect(local, color.Value);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
|
||||
154
Content.Client/Light/SunShadowOverlay.cs
Normal file
154
Content.Client/Light/SunShadowOverlay.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
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;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Light;
|
||||
|
||||
public sealed class SunShadowOverlay : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
|
||||
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
private readonly EntityLookupSystem _lookup;
|
||||
private readonly SharedTransformSystem _xformSys;
|
||||
|
||||
private readonly HashSet<Entity<SunShadowCastComponent>> _shadows = new();
|
||||
|
||||
private IRenderTexture? _blurTarget;
|
||||
private IRenderTexture? _target;
|
||||
|
||||
public SunShadowOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_xformSys = _entManager.System<SharedTransformSystem>();
|
||||
_lookup = _entManager.System<EntityLookupSystem>();
|
||||
ZIndex = AfterLightTargetOverlay.ContentZIndex + 1;
|
||||
}
|
||||
|
||||
private List<Entity<MapGridComponent>> _grids = new();
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var viewport = args.Viewport;
|
||||
var eye = viewport.Eye;
|
||||
|
||||
if (eye == null)
|
||||
return;
|
||||
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(args.MapId,
|
||||
args.WorldBounds.Enlarged(SunShadowComponent.MaxLength),
|
||||
ref _grids);
|
||||
|
||||
var worldHandle = args.WorldHandle;
|
||||
var mapId = args.MapId;
|
||||
var worldBounds = args.WorldBounds;
|
||||
var targetSize = viewport.LightRenderTarget.Size;
|
||||
|
||||
if (_target?.Size != targetSize)
|
||||
{
|
||||
_target = _clyde
|
||||
.CreateRenderTarget(targetSize,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
|
||||
name: "sun-shadow-target");
|
||||
|
||||
if (_blurTarget?.Size != targetSize)
|
||||
{
|
||||
_blurTarget = _clyde
|
||||
.CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
|
||||
}
|
||||
}
|
||||
|
||||
var lightScale = viewport.LightRenderTarget.Size / (Vector2)viewport.Size;
|
||||
var scale = viewport.RenderScale / (Vector2.One / lightScale);
|
||||
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
if (!_entManager.TryGetComponent(grid.Owner, out SunShadowComponent? sun))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var direction = sun.Direction;
|
||||
var alpha = Math.Clamp(sun.Alpha, 0f, 1f);
|
||||
|
||||
// Nowhere to cast to so ignore it.
|
||||
if (direction.Equals(Vector2.Zero) || alpha == 0f)
|
||||
continue;
|
||||
|
||||
// Feature todo: dynamic shadows for mobs and trees. Also ideally remove the fake tree shadows.
|
||||
// TODO: Jittering still not quite perfect
|
||||
|
||||
var expandedBounds = worldBounds.Enlarged(direction.Length() + 0.01f);
|
||||
_shadows.Clear();
|
||||
|
||||
// Draw shadow polys to stencil
|
||||
args.WorldHandle.RenderInRenderTarget(_target,
|
||||
() =>
|
||||
{
|
||||
var invMatrix =
|
||||
_target.GetWorldToLocalMatrix(eye, scale);
|
||||
var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
|
||||
|
||||
// Go through shadows in range.
|
||||
|
||||
// For each one we:
|
||||
// - Get the original vertices.
|
||||
// - Extrapolate these along the sun direction.
|
||||
// - Combine the above into 1 single polygon to draw.
|
||||
|
||||
// Note that this is range-limited for accuracy; if you set it too high it will clip through walls or other undesirable entities.
|
||||
// This is probably not noticeable most of the time but if you want something "accurate" you'll want to code a solution.
|
||||
// Ideally the CPU would have its own shadow-map copy that we could just ray-cast each vert into though
|
||||
// You might need to batch verts or the likes as this could get expensive.
|
||||
_lookup.GetEntitiesIntersecting(mapId, expandedBounds, _shadows);
|
||||
|
||||
foreach (var ent in _shadows)
|
||||
{
|
||||
var xform = _entManager.GetComponent<TransformComponent>(ent.Owner);
|
||||
var worldMatrix = _xformSys.GetWorldMatrix(xform);
|
||||
var renderMatrix = Matrix3x2.Multiply(worldMatrix, invMatrix);
|
||||
var pointCount = ent.Comp.Points.Length;
|
||||
|
||||
Array.Copy(ent.Comp.Points, indices, pointCount);
|
||||
|
||||
for (var i = 0; i < pointCount; i++)
|
||||
{
|
||||
indices[pointCount + i] = indices[i] + direction;
|
||||
}
|
||||
|
||||
var points = PhysicsHull.ComputePoints(indices, pointCount * 2);
|
||||
worldHandle.SetTransform(renderMatrix);
|
||||
|
||||
worldHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, points, Color.White);
|
||||
}
|
||||
},
|
||||
Color.Transparent);
|
||||
|
||||
// Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
|
||||
_clyde.BlurRenderTarget(viewport, _target, _target, eye, 1f);
|
||||
|
||||
// Draw stencil (see roofoverlay).
|
||||
args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
|
||||
() =>
|
||||
{
|
||||
var invMatrix =
|
||||
viewport.LightRenderTarget.GetWorldToLocalMatrix(eye, scale);
|
||||
worldHandle.SetTransform(invMatrix);
|
||||
|
||||
var maskShader = _protoManager.Index<ShaderPrototype>("Mix").Instance();
|
||||
worldHandle.UseShader(maskShader);
|
||||
|
||||
worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Light.EntitySystems;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Light.EntitySystems;
|
||||
@@ -15,8 +16,7 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
|
||||
|
||||
if (ent.Comp.InitialOffset)
|
||||
{
|
||||
ent.Comp.Offset = _random.Next(ent.Comp.Duration);
|
||||
Dirty(ent);
|
||||
SetOffset(ent, _random.Next(ent.Comp.Duration));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
Content.Server/Light/EntitySystems/SunShadowSystem.cs
Normal file
8
Content.Server/Light/EntitySystems/SunShadowSystem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Light.EntitySystems;
|
||||
|
||||
namespace Content.Server.Light.EntitySystems;
|
||||
|
||||
public sealed class SunShadowSystem : SharedSunShadowSystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1033,6 +1033,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
||||
|
||||
EnsureComp<LightCycleComponent>(mapUid);
|
||||
|
||||
EnsureComp<SunShadowComponent>(mapUid);
|
||||
EnsureComp<SunShadowCycleComponent>(mapUid);
|
||||
|
||||
var moles = new float[Atmospherics.AdjustedNumberOfGases];
|
||||
moles[(int) Gas.Oxygen] = 21.824779f;
|
||||
moles[(int) Gas.Nitrogen] = 82.10312f;
|
||||
|
||||
@@ -10,4 +10,13 @@ public sealed partial class IsRoofComponent : Component
|
||||
{
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Enabled = true;
|
||||
|
||||
/// <summary>
|
||||
/// Color for this roof. If null then falls back to the grid's color.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a tile is marked as rooved then the tile color will be used over any entity's colors on the tile.
|
||||
/// </remarks>
|
||||
[DataField, AutoNetworkedField]
|
||||
public Color? Color;
|
||||
}
|
||||
|
||||
25
Content.Shared/Light/Components/SunShadowCastComponent.cs
Normal file
25
Content.Shared/Light/Components/SunShadowCastComponent.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Content.Shared.Light.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Treats this entity as a 1x1 tile and extrapolates its position along the <see cref="SunShadowComponent"/> direction.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class SunShadowCastComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Points that will be extruded to draw the shadow color.
|
||||
/// Max <see cref="PhysicsConstants.MaxPolygonVertices"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Vector2[] Points = new[]
|
||||
{
|
||||
new Vector2(-0.5f, -0.5f),
|
||||
new Vector2(0.5f, -0.5f),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
new Vector2(-0.5f, 0.5f),
|
||||
};
|
||||
}
|
||||
25
Content.Shared/Light/Components/SunShadowComponent.cs
Normal file
25
Content.Shared/Light/Components/SunShadowComponent.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Light.Components;
|
||||
|
||||
/// <summary>
|
||||
/// When added to a map will apply shadows from <see cref="SunShadowComponent"/> to the lighting render target.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class SunShadowComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum length of <see cref="Direction"/>. Mostly used in context of querying for grids off-screen.
|
||||
/// </summary>
|
||||
public const float MaxLength = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Direction for the shadows to be extrapolated in.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public Vector2 Direction;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public float Alpha;
|
||||
}
|
||||
35
Content.Shared/Light/Components/SunShadowCycleComponent.cs
Normal file
35
Content.Shared/Light/Components/SunShadowCycleComponent.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Light.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Applies <see cref="SunShadowComponent"/> direction vectors based on a time-offset. Will track <see cref="LightCycleComponent"/> on on MapInit
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class SunShadowCycleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How long an entire cycle lasts
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan Duration = TimeSpan.FromMinutes(30);
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan Offset;
|
||||
|
||||
// Originally had this as ratios but it was slightly annoying to use.
|
||||
|
||||
/// <summary>
|
||||
/// Time to have each direction applied. Will lerp from the current value to the next one.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public List<(float Ratio, Vector2 Direction, float Alpha)> Directions = new()
|
||||
{
|
||||
(0f, new Vector2(0f, 3f), 0f),
|
||||
(0.25f, new Vector2(-3f, -0.1f), 0.5f),
|
||||
(0.5f, new Vector2(0f, -3f), 0.8f),
|
||||
(0.75f, new Vector2(3f, -0.1f), 0.5f),
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Content.Shared.Light.Components;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Shared;
|
||||
namespace Content.Shared.Light.EntitySystems;
|
||||
|
||||
public abstract class SharedLightCycleSystem : EntitySystem
|
||||
{
|
||||
@@ -30,6 +30,15 @@ public abstract class SharedLightCycleSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOffset(Entity<LightCycleComponent> entity, TimeSpan offset)
|
||||
{
|
||||
entity.Comp.Offset = offset;
|
||||
var ev = new LightCycleOffsetEvent(offset);
|
||||
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
Dirty(entity);
|
||||
}
|
||||
|
||||
public static Color GetColor(Entity<LightCycleComponent> cycle, Color color, float time)
|
||||
{
|
||||
if (cycle.Comp.Enabled)
|
||||
@@ -114,3 +123,12 @@ public abstract class SharedLightCycleSystem : EntitySystem
|
||||
return (crest - shift) * sen + shift;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the offset on <see cref="LightCycleComponent"/> changes.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct LightCycleOffsetEvent(TimeSpan Offset)
|
||||
{
|
||||
public readonly TimeSpan Offset = Offset;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Map;
|
||||
@@ -18,6 +19,7 @@ public abstract class SharedRoofSystem : EntitySystem
|
||||
/// Returns whether the specified tile is roof-occupied.
|
||||
/// </summary>
|
||||
/// <returns>Returns false if no data or not rooved.</returns>
|
||||
[Pure]
|
||||
public bool IsRooved(Entity<MapGridComponent, RoofComponent> grid, Vector2i index)
|
||||
{
|
||||
var roof = grid.Comp2;
|
||||
@@ -49,6 +51,40 @@ public abstract class SharedRoofSystem : EntitySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public Color? GetColor(Entity<MapGridComponent, RoofComponent> grid, Vector2i index)
|
||||
{
|
||||
var roof = grid.Comp2;
|
||||
var chunkOrigin = SharedMapSystem.GetChunkIndices(index, RoofComponent.ChunkSize);
|
||||
|
||||
if (roof.Data.TryGetValue(chunkOrigin, out var bitMask))
|
||||
{
|
||||
var chunkRelative = SharedMapSystem.GetChunkRelative(index, RoofComponent.ChunkSize);
|
||||
var bitFlag = (ulong) 1 << (chunkRelative.X + chunkRelative.Y * RoofComponent.ChunkSize);
|
||||
|
||||
var isRoof = (bitMask & bitFlag) == bitFlag;
|
||||
|
||||
// Early out, otherwise check for components on tile.
|
||||
if (isRoof)
|
||||
{
|
||||
return roof.Color;
|
||||
}
|
||||
}
|
||||
|
||||
_roofSet.Clear();
|
||||
_lookup.GetLocalEntitiesIntersecting(grid.Owner, index, _roofSet);
|
||||
|
||||
foreach (var isRoofEnt in _roofSet)
|
||||
{
|
||||
if (!isRoofEnt.Comp.Enabled)
|
||||
continue;
|
||||
|
||||
return isRoofEnt.Comp.Color ?? roof.Color;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SetRoof(Entity<MapGridComponent?, RoofComponent?> grid, Vector2i index, bool value)
|
||||
{
|
||||
if (!Resolve(grid, ref grid.Comp1, ref grid.Comp2, false))
|
||||
|
||||
39
Content.Shared/Light/EntitySystems/SharedSunShadowSystem.cs
Normal file
39
Content.Shared/Light/EntitySystems/SharedSunShadowSystem.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Content.Shared.Light.Components;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Shared.Light.EntitySystems;
|
||||
|
||||
public abstract class SharedSunShadowSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SunShadowCycleComponent, MapInitEvent>(OnCycleMapInit);
|
||||
SubscribeLocalEvent<SunShadowCycleComponent, LightCycleOffsetEvent>(OnCycleOffset);
|
||||
}
|
||||
|
||||
private void OnCycleOffset(Entity<SunShadowCycleComponent> ent, ref LightCycleOffsetEvent args)
|
||||
{
|
||||
// Okay so we synchronise with LightCycleComponent.
|
||||
// However, the offset is only set on MapInit and we have no guarantee which one is ran first so we make sure.
|
||||
ent.Comp.Offset = args.Offset;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnCycleMapInit(Entity<SunShadowCycleComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
if (TryComp(ent.Owner, out LightCycleComponent? lightCycle))
|
||||
{
|
||||
ent.Comp.Duration = lightCycle.Duration;
|
||||
ent.Comp.Offset = lightCycle.Offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
ent.Comp.Offset = _random.Next(ent.Comp.Duration);
|
||||
}
|
||||
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@
|
||||
- type: RadiationBlocker
|
||||
resistance: 2
|
||||
- type: BlockWeather
|
||||
- type: SunShadowCast
|
||||
|
||||
- type: entity
|
||||
parent: BaseWall
|
||||
|
||||
Reference in New Issue
Block a user