using Content.Shared.Charges.Components; using Content.Shared.Charges.Systems; using Content.Shared.Examine; using Content.Shared.Eye.Blinding.Components; using Content.Shared.Flash.Components; using Content.Shared.IdentityManagement; using Content.Shared.Interaction.Events; using Content.Shared.Inventory; using Content.Shared.Light; using Content.Shared.Popups; using Content.Shared.StatusEffect; using Content.Shared.Stunnable; using Content.Shared.Tag; using Content.Shared.Timing; 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.Random; using Robust.Shared.Timing; using System.Linq; using Content.Shared.Movement.Systems; using Content.Shared.Random.Helpers; using Content.Shared.Clothing.Components; namespace Content.Shared.Flash; public abstract class SharedFlashSystem : EntitySystem { [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedChargesSystem _sharedCharges = default!; [Dependency] private readonly EntityLookupSystem _entityLookup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly ExamineSystemShared _examine = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedStunSystem _stun = default!; [Dependency] private readonly MovementModStatusSystem _movementMod = default!; [Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly UseDelaySystem _useDelay = default!; private EntityQuery _statusEffectsQuery; private EntityQuery _damagedByFlashingQuery; private HashSet _entSet = new(); // The tag to add when a flash has no charges left. private static readonly ProtoId TrashTag = "Trash"; // The key string for the status effect. public ProtoId FlashedKey = "Flashed"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnFlashMeleeHit); SubscribeLocalEvent(OnFlashUseInHand); SubscribeLocalEvent(OnLightToggle); SubscribeLocalEvent(OnPermanentBlindnessFlashAttempt); SubscribeLocalEvent(OnTemporaryBlindnessFlashAttempt); Subs.SubscribeWithRelay(OnFlashImmunityFlashAttempt, held: false); SubscribeLocalEvent(OnExamine); _statusEffectsQuery = GetEntityQuery(); _damagedByFlashingQuery = GetEntityQuery(); } private void OnFlashMeleeHit(Entity ent, ref MeleeHitEvent args) { if (!ent.Comp.FlashOnMelee || !args.IsHit || !args.HitEntities.Any() || !UseFlash(ent, 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); } } private void OnFlashUseInHand(Entity ent, ref UseInHandEvent args) { if (!ent.Comp.FlashOnUse || args.Handled || !UseFlash(ent, args.User)) return; args.Handled = true; FlashArea(ent.Owner, args.User, ent.Comp.Range, ent.Comp.AoeFlashDuration, ent.Comp.SlowTo, true, ent.Comp.Probability); } // 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 private void OnLightToggle(Entity ent, ref LightToggleEvent args) { if (!args.IsOn || !UseFlash(ent, null)) return; FlashArea(ent.Owner, null, ent.Comp.Range, ent.Comp.AoeFlashDuration, ent.Comp.SlowTo, true, ent.Comp.Probability); } /// /// Use charges and set the visuals. /// /// False if no charges are left or the flash is currently in use. private bool UseFlash(Entity ent, EntityUid? user) { if (_useDelay.IsDelayed(ent.Owner)) return false; if (TryComp(ent.Owner, out var charges) && _sharedCharges.IsEmpty((ent.Owner, charges))) return false; _sharedCharges.TryUseCharge((ent.Owner, charges)); _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); _tag.AddTag(ent.Owner, TrashTag); _popup.PopupClient(Loc.GetString("flash-component-becomes-empty"), 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.TryAddStatusEffect(target, FlashedKey, flashDuration, true)) 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) { // TODO: Use RandomPredicted https://github.com/space-wizards/RobustToolbox/pull/5849 var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(entity).Id }); var rand = new System.Random(seed); if (!rand.Prob(probability)) continue; // Is the entity affected by the flash either through status effects or by taking damage? if (!_statusEffectsQuery.HasComponent(entity) && !_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)); } // 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); } } } private void OnPermanentBlindnessFlashAttempt(Entity ent, ref FlashAttemptEvent args) { // check for total blindness if (ent.Comp.Blindness == 0) args.Cancelled = true; } private void OnTemporaryBlindnessFlashAttempt(Entity ent, ref FlashAttemptEvent args) { args.Cancelled = true; } private void OnFlashImmunityFlashAttempt(Entity ent, ref FlashAttemptEvent args) { if (TryComp(ent, out var mask) && mask.IsToggled) return; if (ent.Comp.Enabled) args.Cancelled = true; } private void OnExamine(Entity ent, ref ExaminedEvent args) { if (ent.Comp.ShowInExamine) args.PushMarkup(Loc.GetString("flash-protection")); } }