150 lines
5.8 KiB
C#
150 lines
5.8 KiB
C#
using Content.Shared.Mobs.Systems;
|
|
using Content.Shared.Physics;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Physics.Systems;
|
|
using Robust.Shared.Timing;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
|
|
namespace Content.Shared._DV.Light;
|
|
|
|
public abstract class SharedLightReactiveSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly IGameTiming _timing = default!;
|
|
[Dependency] private readonly MobStateSystem _mobState = default!;
|
|
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
|
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
|
|
|
|
|
private EntityQuery<LightReactiveComponent> _lightReactive;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
_lightReactive = GetEntityQuery<LightReactiveComponent>();
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
var query = EntityQueryEnumerator<LightReactiveComponent>();
|
|
while (query.MoveNext(out var uid, out var comp))
|
|
{
|
|
if (comp.Manual)
|
|
continue; // Don't auto update if it's manual
|
|
if (_timing.CurTime < comp.NextUpdate)
|
|
continue;
|
|
comp.NextUpdate = _timing.CurTime + TimeSpan.FromSeconds(1);
|
|
if (_mobState.IsDead(uid) && comp.OnlyWhileAlive)
|
|
continue; // Don't apply damage / healing if the mob is dead
|
|
// Get the light level at the entity's position
|
|
comp.CurrentLightLevel = GetLightLevelForPoint(uid);
|
|
}
|
|
}
|
|
|
|
public abstract HashSet<Entity<SharedPointLightComponent>> GetLights(EntityUid targetEntity);
|
|
|
|
/// <summary>
|
|
/// Gets the current light level of an entity.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is a cached value that is updated periodically.
|
|
/// I could add arguments to either: Force check if it's not a ReactiveComponent, or force update,
|
|
/// but I don't need them so you don't get them. Add it if you want it.
|
|
/// </remarks>
|
|
public float GetLightLevel(EntityUid uid, bool forceUpdate = false)
|
|
{
|
|
if (_lightReactive.TryComp(uid, out LightReactiveComponent? comp))
|
|
{
|
|
if (forceUpdate)
|
|
comp.CurrentLightLevel = GetLightLevelForPoint(uid);
|
|
return comp.CurrentLightLevel;
|
|
}
|
|
return 0.0f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the light level at a specific point in the world.
|
|
/// Avoid calling this too often, as it can be expensive.
|
|
/// </summary>
|
|
public float GetLightLevelForPoint(EntityUid uid)
|
|
{
|
|
float val = 0.0f;
|
|
// Get the current map entity so we can get a MapLightComponent from it if it has one
|
|
var map = _transform.GetMap(uid);
|
|
if (TryComp(map, out MapLightComponent? mapLight))
|
|
val += (mapLight.AmbientLightColor.R + mapLight.AmbientLightColor.G + mapLight.AmbientLightColor.B) / 3f;
|
|
var pos = _transform.GetWorldPosition(uid);
|
|
|
|
foreach (var (lightUid, lightComp) in GetLights(uid))
|
|
{
|
|
var energy = lightComp.Energy;
|
|
var radius = lightComp.Radius;
|
|
if (!lightComp.NetSyncEnabled)
|
|
{
|
|
// Try to use the GetLightEnergyEvent if we can't rely on it being network synced.
|
|
var lightEnergyEvnt = new OnGetLightEnergyEvent();
|
|
RaiseLocalEvent(lightUid, ref lightEnergyEvnt);
|
|
energy = lightEnergyEvnt.LightEnergy;
|
|
radius = lightEnergyEvnt.LightRadius;
|
|
if (MathHelper.CloseTo(energy, 0f))
|
|
continue; // No light, no problem.
|
|
}
|
|
|
|
energy = MathF.Min(energy, 2f); // Clamp energy, to normalize strange values.
|
|
|
|
// Ensure we're on the same grid as the light source
|
|
if (_transform.GetMap(lightUid) != map)
|
|
continue;
|
|
|
|
// Ensure we're within the light's radius.
|
|
var lightPos = _transform.GetWorldPosition(lightUid);
|
|
var sqrDistance = Vector2.DistanceSquared(pos, lightPos);
|
|
if (sqrDistance > radius * radius)
|
|
continue;
|
|
|
|
if (sqrDistance < 0.01f)
|
|
{
|
|
// If we're right on top of the light, just add its full energy value.
|
|
val += energy;
|
|
continue;
|
|
}
|
|
|
|
// Collision ray check from the entity to the light source
|
|
var ray = new CollisionRay(pos, (lightPos - pos).Normalized(), (int)CollisionGroup.Opaque);
|
|
var hit = _physics.IntersectRay(_transform.GetMapId(uid), ray, MathF.Sqrt(sqrDistance) - 0.5f, ignoredEnt: lightUid, returnOnFirstHit: true);
|
|
if (hit.Any() && hit.First().Distance != 0)
|
|
continue;
|
|
|
|
// Manual hack for cones.
|
|
if (lightComp.MaskPath == "/Textures/Effects/LightMasks/cone.png")
|
|
{
|
|
var forward = _transform.GetWorldRotation(lightUid).RotateVec(new Vector2(0.0f, -1.0f));
|
|
energy *= MathF.Max(0f, Vector2.Dot((pos - lightPos).Normalized(), forward));
|
|
}
|
|
else if (lightComp.MaskPath == "/Textures/Effects/LightMasks/double_cone.png")
|
|
{
|
|
var forward = _transform.GetWorldRotation(lightUid).RotateVec(new Vector2(0.0f, -1.0f));
|
|
energy *= MathF.Abs(Vector2.Dot((pos - lightPos).Normalized(), forward));
|
|
}
|
|
|
|
// If we reach here, the light is unobstructed and within range, calculate a light value to add.
|
|
val += energy * (1.0f - sqrDistance / (radius * radius));
|
|
}
|
|
|
|
|
|
return val;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Passed to unsync'd light sources to get the expected light energy.
|
|
/// ONLY called when NetSync is not enabled. Otherwise, uses the light directly.
|
|
/// </summary>
|
|
[ByRefEvent]
|
|
public record struct OnGetLightEnergyEvent()
|
|
{
|
|
public float LightEnergy = 0f;
|
|
public float LightRadius = 0f;
|
|
}
|