using System.Linq; using Content.Shared.Charges.Components; using Content.Shared.Charges.Systems; using Content.Shared.Clothing.Components; using Content.Shared.Examine; using Content.Shared.Flash.Components; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Inventory; using Content.Shared.Light; using Content.Shared.Movement.Systems; using Content.Shared.Popups; using Content.Shared.Random.Helpers; using Content.Shared.StatusEffectNew; using Content.Shared.Stunnable; using Content.Shared.Tag; using Content.Shared.Timing.Systems; using Content.Shared.Traits.Assorted; using Content.Shared.Weapons.Melee.Events; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared.Flash; public abstract partial class SharedFlashSystem : EntitySystem { [Dependency] private SharedAppearanceSystem _appearance = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedChargesSystem _sharedCharges = default!; [Dependency] private EntityLookupSystem _entityLookup = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private ExamineSystemShared _examine = default!; [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private SharedStunSystem _stun = default!; [Dependency] private MovementModStatusSystem _movementMod = default!; [Dependency] private TagSystem _tag = default!; [Dependency] private StatusEffectsSystem _statusEffectsSystem = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private UseDelaySystem _useDelay = default!; [Dependency] private EntityQuery _damagedByFlashingQuery = default!; private readonly HashSet _entSet = []; // The tag to add when a flash has no charges left. private static readonly ProtoId TrashTag = "Trash"; // The status effect entity applied to flashed targets. public static readonly EntProtoId FlashedKey = "StatusEffectFlashed"; // Handle the flash visuals // TODO: Replace this with something like sprite flick once that exists to get rid of the update loop. public override void Update(float frameTime) { base.Update(frameTime); var curTime = _timing.CurTime; var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var active)) { // reset the visuals and remove the component if (active.ActiveUntil < curTime) { _appearance.SetData(uid, FlashVisuals.Flashing, false); RemCompDeferred(uid); } } } public override void Initialize() { base.Initialize(); Subs.SubscribeWithRelay(OnFlashImmunityFlashAttempt, held: false); } [SubscribeLocalEvent] private void OnFlashMeleeHit(Entity ent, ref MeleeHitEvent args) { if (!ent.Comp.FlashOnMelee || !args.IsHit || !args.HitEntities.Any() || !TryUseFlashItem(ent.AsNullable(), args.User)) { return; } args.Handled = true; foreach (var target in args.HitEntities) { Flash(target, args.User, ent.Owner, ent.Comp.MeleeDuration, ent.Comp.SlowTo, melee: true, stunDuration: ent.Comp.MeleeStunDuration); } EntityUid? firstTarget = args.HitEntities.Count > 0 ? args.HitEntities[0] : null; // Just pick the first hit entity. var ev = new AfterFlashActivatedEvent(firstTarget, args.User); RaiseLocalEvent(ent, ref ev); } [SubscribeLocalEvent] private void OnFlashUseInHand(Entity ent, ref UseInHandEvent args) { if (!ent.Comp.FlashOnUse || args.Handled || !TryUseFlashItem(ent.AsNullable(), args.User)) return; args.Handled = true; FlashArea(ent.Owner, args.User, ent.Comp.Range, ent.Comp.AoeFlashDuration, ent.Comp.SlowTo, true, ent.Comp.Probability); var ev = new AfterFlashActivatedEvent(null, args.User); // No direct target. RaiseLocalEvent(ent, ref ev); } // TODO: This or most of the other systems subscribing to BeforeRangedInteractEvent shouldn't be using this event as handling it stops contact interaction with the used tool, // but this will need some cleanup of how SharedInteractionSystem handles the code flow. Also the event is raised for both in-range and out of range interactions, which // is what the subscribers are using it for, but does not seem originally intended from the naming convention. [SubscribeLocalEvent] private void OnRangedInteract(Entity ent, ref BeforeRangedInteractEvent args) { if (!ent.Comp.FlashOnRangedInteract || args.Handled || !TryUseFlashItem(ent.AsNullable(), args.User)) return; args.Handled = true; FlashArea(ent.Owner, args.User, ent.Comp.Range, ent.Comp.AoeFlashDuration, ent.Comp.SlowTo, true, ent.Comp.Probability); var ev = new AfterFlashActivatedEvent(args.Target, args.User); RaiseLocalEvent(ent, ref ev); } // needed for the flash lantern and interrogator lamp // TODO: This is awful and all the different components for toggleable lights need to be unified and changed to use Itemtoggle [SubscribeLocalEvent] private void OnLightToggle(Entity ent, ref LightToggleEvent args) { if (!args.IsOn || !TryUseFlashItem(ent.AsNullable(), null)) return; FlashArea(ent.Owner, null, ent.Comp.Range, ent.Comp.AoeFlashDuration, ent.Comp.SlowTo, true, ent.Comp.Probability); var ev = new AfterFlashActivatedEvent(null, null); // TODO: Add user once someone made toggleable lights not a total mess. RaiseLocalEvent(ent, ref ev); } [SubscribeLocalEvent] private void OnPermanentBlindnessFlashAttempt(Entity ent, ref FlashAttemptEvent args) { // check for total blindness if (ent.Comp.Blindness == 0) args.Cancelled = true; } [SubscribeLocalEvent] private void OnExamine(Entity ent, ref ExaminedEvent args) { if (ent.Comp.ShowInExamine) args.PushMarkup(Loc.GetString("flash-protection")); } private void OnFlashImmunityFlashAttempt(Entity ent, ref FlashAttemptEvent args) { if (TryComp(ent, out var mask) && mask.IsToggled) return; if (ent.Comp.Enabled) args.Cancelled = true; } /// /// Try to use charges, play the sound and set the visuals of a flash item. /// This does not actually cause the flash status effect by itself, you will need to either call or as well. /// /// False if no charges are left or the flash is currently in use. public bool TryUseFlashItem(Entity ent, EntityUid? user) { if (!Resolve(ent, ref ent.Comp)) return false; if (_useDelay.IsDelayed(ent.Owner)) return false; if (TryComp(ent.Owner, out var charges) && !_sharedCharges.TryUseCharge((ent.Owner, charges))) return false; _audio.PlayPredicted(ent.Comp.Sound, ent.Owner, user); var active = EnsureComp(ent.Owner); active.ActiveUntil = _timing.CurTime + ent.Comp.FlashingTime; Dirty(ent.Owner, active); _appearance.SetData(ent.Owner, FlashVisuals.Flashing, true); if (_sharedCharges.IsEmpty((ent.Owner, charges))) { _appearance.SetData(ent.Owner, FlashVisuals.Burnt, true); // TODO: Reset if charges are refilled. _tag.AddTag(ent.Owner, TrashTag); if (user != null) _popup.PopupEntity(Loc.GetString("flash-component-becomes-empty"), user.Value, user); } return true; } /// /// Cause an entity to be flashed, obstructing their vision, slowing them down and stunning them. /// In case of a melee attack this will do a check for revolutionary conversion. /// /// The mob to be flashed. /// The mob causing the flash, if any. /// The item causing the flash, if any. /// The time target will be affected by the flash. /// Movement speed modifier applied to the flashed target. Between 0 and 1. /// Whether or not to show a popup to the target player. /// Was this flash caused by a melee attack? Used for checking for revolutionary conversion. /// The time the target will be stunned. If null the target will be slowed down instead. public void Flash( EntityUid target, EntityUid? user, EntityUid? used, TimeSpan flashDuration, float slowTo, bool displayPopup = true, bool melee = false, TimeSpan? stunDuration = null) { var attempt = new FlashAttemptEvent(target, user, used); RaiseLocalEvent(target, ref attempt, true); if (attempt.Cancelled) return; // don't paralyze, slowdown or convert to rev if the target is immune to flashes if (!_statusEffectsSystem.TryAddStatusEffectDuration(target, FlashedKey, flashDuration)) return; if (stunDuration != null) _stun.TryUpdateParalyzeDuration(target, stunDuration.Value); else _movementMod.TryUpdateMovementSpeedModDuration(target, MovementModStatusSystem.FlashSlowdown, flashDuration, slowTo); if (displayPopup && user != null && target != user && Exists(user.Value)) { _popup.PopupEntity(Loc.GetString("flash-component-user-blinds-you", ("user", Identity.Entity(user.Value, EntityManager))), target, target); } var ev = new AfterFlashedEvent(target, user, used, melee); RaiseLocalEvent(target, ref ev); if (user != null) RaiseLocalEvent(user.Value, ref ev); if (used != null) RaiseLocalEvent(used.Value, ref ev); } /// /// Cause all entities in range of a source entity to be flashed. /// /// The source of the flash, which will be at the epicenter. /// The mob causing the flash, if any. /// The time target will be affected by the flash. /// Movement speed modifier applied to the flashed target. Between 0 and 1. /// Whether or not to show a popup to the target player. /// Chance to be flashed. Rolled separately for each target in range. /// Additional sound to play at the source. public void FlashArea(EntityUid source, EntityUid? user, float range, TimeSpan flashDuration, float slowTo = 0.8f, bool displayPopup = false, float probability = 1f, SoundSpecifier? sound = null) { var transform = Transform(source); var mapPosition = _transform.GetMapCoordinates(transform); _entSet.Clear(); _entityLookup.GetEntitiesInRange(transform.Coordinates, range, _entSet); foreach (var entity in _entSet) { if (!SharedRandomExtensions.PredictedProb(_timing, probability, GetNetEntity(entity))) continue; // Is the entity affected by the flash either through status effects or by taking damage? if (!_statusEffectsSystem.CanAddStatusEffect(entity, FlashedKey) && !_damagedByFlashingQuery.HasComponent(entity)) continue; // Check for entites in view. // Put DamagedByFlashingComponent in the predicate because shadow anomalies block vision. if (!_examine.InRangeUnOccluded(entity, mapPosition, range, predicate: (e) => _damagedByFlashingQuery.HasComponent(e))) continue; Flash(entity, user, source, flashDuration, slowTo, displayPopup); } _audio.PlayPredicted(sound, source, user, AudioParams.Default.WithVolume(1f).WithMaxDistance(3f)); } }