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:
metalgearsloth
2025-03-08 16:07:42 +11:00
committed by GitHub
parent db96cc7881
commit f51b9bc86e
17 changed files with 466 additions and 6 deletions

View File

@@ -54,6 +54,6 @@ public sealed class AfterLightTargetOverlay : Overlay
worldHandle.SetTransform(localMatrix); worldHandle.SetTransform(localMatrix);
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion); worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
}, null); }, Color.Transparent);
} }
} }

View File

@@ -16,6 +16,7 @@ public sealed class PlanetLightSystem : EntitySystem
_overlayMan.AddOverlay(new RoofOverlay(EntityManager)); _overlayMan.AddOverlay(new RoofOverlay(EntityManager));
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager)); _overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
_overlayMan.AddOverlay(new LightBlurOverlay()); _overlayMan.AddOverlay(new LightBlurOverlay());
_overlayMan.AddOverlay(new SunShadowOverlay());
_overlayMan.AddOverlay(new AfterLightTargetOverlay()); _overlayMan.AddOverlay(new AfterLightTargetOverlay());
} }
@@ -31,6 +32,7 @@ public sealed class PlanetLightSystem : EntitySystem
_overlayMan.RemoveOverlay<RoofOverlay>(); _overlayMan.RemoveOverlay<RoofOverlay>();
_overlayMan.RemoveOverlay<TileEmissionOverlay>(); _overlayMan.RemoveOverlay<TileEmissionOverlay>();
_overlayMan.RemoveOverlay<LightBlurOverlay>(); _overlayMan.RemoveOverlay<LightBlurOverlay>();
_overlayMan.RemoveOverlay<SunShadowOverlay>();
_overlayMan.RemoveOverlay<AfterLightTargetOverlay>(); _overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
} }
} }

View 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();
}
}

View File

@@ -1,6 +1,7 @@
using Content.Client.GameTicking.Managers; using Content.Client.GameTicking.Managers;
using Content.Shared; using Content.Shared;
using Content.Shared.Light.Components; using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -11,19 +12,29 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
{ {
[Dependency] private readonly ClientGameTicker _ticker = default!; [Dependency] private readonly ClientGameTicker _ticker = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
return;
var mapQuery = AllEntityQuery<LightCycleComponent, MapLightComponent>(); var mapQuery = AllEntityQuery<LightCycleComponent, MapLightComponent>();
while (mapQuery.MoveNext(out var uid, out var cycle, out var map)) while (mapQuery.MoveNext(out var uid, out var cycle, out var map))
{ {
if (!cycle.Running) if (!cycle.Running)
continue; 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 var time = (float) _timing.CurTime
.Add(cycle.Offset) .Add(cycle.Offset)
.Subtract(_ticker.RoundStartTimeSpan) .Subtract(_ticker.RoundStartTimeSpan)
.Subtract(pausedTime)
.TotalSeconds; .TotalSeconds;
var color = GetColor((uid, cycle), cycle.OriginalColor, time); var color = GetColor((uid, cycle), cycle.OriginalColor, time);

View File

@@ -94,13 +94,15 @@ public sealed class RoofOverlay : Overlay
// Due to stencilling we essentially draw on unrooved tiles // Due to stencilling we essentially draw on unrooved tiles
while (tileEnumerator.MoveNext(out var tileRef)) while (tileEnumerator.MoveNext(out var tileRef))
{ {
if (!_roof.IsRooved(roofEnt, tileRef.GridIndices)) var color = _roof.GetColor(roofEnt, tileRef.GridIndices);
if (color == null)
{ {
continue; continue;
} }
var local = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize); var local = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize);
worldHandle.DrawRect(local, roof.Color); worldHandle.DrawRect(local, color.Value);
} }
} }
}, null); }, null);

View 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);
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Shared; using Content.Shared;
using Content.Shared.Light.Components; using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Robust.Shared.Random; using Robust.Shared.Random;
namespace Content.Server.Light.EntitySystems; namespace Content.Server.Light.EntitySystems;
@@ -15,8 +16,7 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
if (ent.Comp.InitialOffset) if (ent.Comp.InitialOffset)
{ {
ent.Comp.Offset = _random.Next(ent.Comp.Duration); SetOffset(ent, _random.Next(ent.Comp.Duration));
Dirty(ent);
} }
} }
} }

View File

@@ -0,0 +1,8 @@
using Content.Shared.Light.EntitySystems;
namespace Content.Server.Light.EntitySystems;
public sealed class SunShadowSystem : SharedSunShadowSystem
{
}

View File

@@ -1033,6 +1033,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
EnsureComp<LightCycleComponent>(mapUid); EnsureComp<LightCycleComponent>(mapUid);
EnsureComp<SunShadowComponent>(mapUid);
EnsureComp<SunShadowCycleComponent>(mapUid);
var moles = new float[Atmospherics.AdjustedNumberOfGases]; var moles = new float[Atmospherics.AdjustedNumberOfGases];
moles[(int) Gas.Oxygen] = 21.824779f; moles[(int) Gas.Oxygen] = 21.824779f;
moles[(int) Gas.Nitrogen] = 82.10312f; moles[(int) Gas.Nitrogen] = 82.10312f;

View File

@@ -10,4 +10,13 @@ public sealed partial class IsRoofComponent : Component
{ {
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public bool Enabled = true; 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;
} }

View 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),
};
}

View 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;
}

View 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),
};
}

View File

@@ -1,7 +1,7 @@
using Content.Shared.Light.Components; using Content.Shared.Light.Components;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
namespace Content.Shared; namespace Content.Shared.Light.EntitySystems;
public abstract class SharedLightCycleSystem : EntitySystem 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) public static Color GetColor(Entity<LightCycleComponent> cycle, Color color, float time)
{ {
if (cycle.Comp.Enabled) if (cycle.Comp.Enabled)
@@ -114,3 +123,12 @@ public abstract class SharedLightCycleSystem : EntitySystem
return (crest - shift) * sen + shift; 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;
}

View File

@@ -1,3 +1,4 @@
using System.Diagnostics.Contracts;
using Content.Shared.Light.Components; using Content.Shared.Light.Components;
using Content.Shared.Maps; using Content.Shared.Maps;
using Robust.Shared.Map; using Robust.Shared.Map;
@@ -18,6 +19,7 @@ public abstract class SharedRoofSystem : EntitySystem
/// Returns whether the specified tile is roof-occupied. /// Returns whether the specified tile is roof-occupied.
/// </summary> /// </summary>
/// <returns>Returns false if no data or not rooved.</returns> /// <returns>Returns false if no data or not rooved.</returns>
[Pure]
public bool IsRooved(Entity<MapGridComponent, RoofComponent> grid, Vector2i index) public bool IsRooved(Entity<MapGridComponent, RoofComponent> grid, Vector2i index)
{ {
var roof = grid.Comp2; var roof = grid.Comp2;
@@ -49,6 +51,40 @@ public abstract class SharedRoofSystem : EntitySystem
return false; 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) public void SetRoof(Entity<MapGridComponent?, RoofComponent?> grid, Vector2i index, bool value)
{ {
if (!Resolve(grid, ref grid.Comp1, ref grid.Comp2, false)) if (!Resolve(grid, ref grid.Comp1, ref grid.Comp2, false))

View 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);
}
}

View File

@@ -51,6 +51,7 @@
- type: RadiationBlocker - type: RadiationBlocker
resistance: 2 resistance: 2
- type: BlockWeather - type: BlockWeather
- type: SunShadowCast
- type: entity - type: entity
parent: BaseWall parent: BaseWall