using Content.Shared.Damage; using Robust.Shared.Timing; using Content.Shared.Shadow.Components; using Content.Shared.Damage.Prototypes; using Robust.Server.GameObjects; using Content.Shared.Physics; using Robust.Server.Containers; using Content.Shared.Inventory; using Robust.Shared.Physics; using Robust.Shared.Physics.Systems; using Robust.Shared.Audio.Systems; using Robust.Shared.Audio; using Content.Server.Hands.Systems; using Content.Shared.Hands.Components; using Content.Shared.Movement.Systems; using Robust.Shared.Random; using Robust.Shared.Prototypes; namespace Content.Server.Shadow; public sealed class PhotophobiaSystem : EntitySystem { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly ContainerSystem _container = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly HandsSystem _handsSystem = default!; [Dependency] private readonly MovementSpeedModifierSystem _speed = default!; [Dependency] private readonly IRobustRandom _random = default!; private static readonly ProtoId Damage = "Heat"; // private readonly List _scratchLights = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnUnpaused); SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnRefreshMovementSpeedModifiers); } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { if (_gameTiming.CurTime < comp.NextTickTime) continue; comp.NextTickTime = _gameTiming.CurTime + TimeSpan.FromSeconds(comp.Interval * _random.NextFloat(0.75f, 1.25f)); var lights = CheckNearbyLights(uid); if (!lights) { if (comp.ApplyShadowWeakness) { _speed.RefreshMovementSpeedModifiers(uid); comp.ShadowWeakness = false; } continue; } if (comp.ApplyShadowWeakness) { _speed.RefreshMovementSpeedModifiers(uid); comp.ShadowWeakness = true; } if (comp.DamagePerLight != 0) { var damage = new DamageSpecifier { DamageDict = { { Damage, comp.DamagePerLight } } }; _audio.PlayPvs(comp.DamageSound, uid, AudioParams.Default.WithVolume(-10f).WithPitchScale(1.15f).WithMaxDistance(2f)); _damageable.TryChangeDamage(uid, damage, false, false); } } } private void OnUnpaused(EntityUid uid, PhotophobiaComponent comp, ref EntityUnpausedEvent args) { comp.NextTickTime += args.PausedTime; } private bool CheckNearbyLights(EntityUid uid) { var xform = Transform(uid); var worldPos = _transform.GetWorldPosition(xform); foreach (var (lightUid, light) in _lookup.GetEntitiesInRange(xform.Coordinates, 10f)) { if (!light.Enabled) continue; if (light.Radius < 2f) continue; if (TryComp(lightUid, out var visibility) && visibility.Layer != 1) continue; if (TryCheckContainingLight(lightUid)) { continue; } if (IsTargetInsideCone(uid, lightUid)) { continue; } float lightFactor = light.Energy * 0.5f + light.Radius - 2f; if (lightFactor > light.Radius) lightFactor = light.Radius - 0.5f; var lightXform = Transform(lightUid); var lightPos = _transform.GetWorldPosition(lightXform); var distanceSq = (worldPos - lightPos).LengthSquared(); if (distanceSq > lightFactor * lightFactor) { continue; } if (GetOccluded(uid, lightUid)) { return true; } } return false; } private bool TryCheckContainingLight(EntityUid uid) { if (!_container.TryGetContainingContainer(uid, out var container)) return false; if (_inventorySystem.TryGetContainingSlot(uid, out _)) return false; if (TryCheckHandLight(container.Owner, uid)) return false; return true; } private bool TryCheckHandLight(Entity user, EntityUid uid) { if (Resolve(user.Owner, ref user.Comp, false)) { foreach (var held in _handsSystem.EnumerateHeld(user)) { if (held == uid) return true; } } return false; } public bool GetOccluded(EntityUid sourceUid, EntityUid lightUid) { if (sourceUid == lightUid) return true; if (_container.TryGetContainingContainer(lightUid, out var container) && container.Owner == sourceUid) return true; var sourceXform = Transform(sourceUid); var lightXform = Transform(lightUid); if (sourceXform.MapID != lightXform.MapID) return false; var sourcePos = _transform.GetWorldPosition(sourceXform); var lightPos = _transform.GetWorldPosition(lightXform); var direction = lightPos - sourcePos; var distance = direction.Length(); if (distance <= 0.1f) return false; var normalizedDir = direction.Normalized(); var ray = new CollisionRay(sourcePos, normalizedDir, (int)(CollisionGroup.WallLayer | CollisionGroup.AirlockLayer)); var rayCastResults = _physics.IntersectRay(sourceXform.MapID, ray, distance, sourceUid, false); foreach (var result in rayCastResults) { if (TryComp(result.HitEntity, out var occluder) && occluder.Enabled) { return false; } } return true; } public bool IsTargetInsideCone(EntityUid targetUid, EntityUid lightUid) { if (!TryComp(lightUid, out var lightComp)) return true; // TODO: Избавится от этого ужаса. if (lightComp.MaskPath == "/Textures/Effects/LightMasks/double_cone.png") return true; if (lightComp.MaskPath == null) return false; var tXform = Transform(targetUid); var lXform = Transform(lightUid); var tPos = _transform.GetWorldPosition(tXform); var lPos = _transform.GetWorldPosition(lXform); float toX = tPos.X - lPos.X; float toY = tPos.Y - lPos.Y; float dist = MathF.Sqrt(toX * toX + toY * toY); float invDist = 1f / dist; float toNx = toX * invDist; float toNy = toY * invDist; float ang = (float)_transform.GetWorldRotation(lXform).Theta; ang -= MathF.PI / 2f; float fX = MathF.Cos(ang); float fY = MathF.Sin(ang); float dot = fX * toNx + fY * toNy; const float coneAngleDeg = 90f; float cosHalf = MathF.Cos(MathF.PI * coneAngleDeg / 360f); return dot <= cosHalf; } private void OnRefreshMovementSpeedModifiers(Entity ent, ref RefreshMovementSpeedModifiersEvent args) { if (ent.Comp.ShadowWeakness) args.ModifySpeed(ent.Comp.SpeedModifier, ent.Comp.SpeedModifier); } private void OnDamageChanged(Entity ent, ref DamageChangedEvent args) { if (!ent.Comp.ShadowWeakness || args.DamageDelta is null || IsNegativeDamage(args.DamageDelta)) return; var bonusDamage = args.DamageDelta * ent.Comp.DamageModfier; _damageable.TryChangeDamage(ent, bonusDamage, true); } private bool IsNegativeDamage(DamageSpecifier damage) { foreach (var type in damage.DamageDict) { if (type.Value > 0) return false; } return true; } }