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 _lightReactive; public override void Initialize() { base.Initialize(); _lightReactive = GetEntityQuery(); } public override void Update(float frameTime) { var query = EntityQueryEnumerator(); 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> GetLights(EntityUid targetEntity); /// /// Gets the current light level of an entity. /// /// /// 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. /// 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; } /// /// Gets the light level at a specific point in the world. /// Avoid calling this too often, as it can be expensive. /// 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; } } /// /// Passed to unsync'd light sources to get the expected light energy. /// ONLY called when NetSync is not enabled. Otherwise, uses the light directly. /// [ByRefEvent] public record struct OnGetLightEnergyEvent() { public float LightEnergy = 0f; public float LightRadius = 0f; }