457 lines
19 KiB
C#
457 lines
19 KiB
C#
using Content.Server.Audio;
|
|
using Content.Server.Power.Components;
|
|
using Content.Server.Electrocution;
|
|
using Content.Server.Lightning;
|
|
using Content.Server.Explosion.EntitySystems;
|
|
using Content.Server.Ghost;
|
|
using Content.Server.Revenant.EntitySystems;
|
|
using Content.Shared.Audio;
|
|
using Content.Shared.Construction.EntitySystems;
|
|
using Content.Shared.GameTicking;
|
|
using Content.Shared.Psionics.Glimmer;
|
|
using Content.Shared.Verbs;
|
|
using Content.Shared.StatusEffect;
|
|
using Content.Shared.Damage.Systems;
|
|
using Content.Shared.Destructible;
|
|
using Content.Shared.Construction.Components;
|
|
using Content.Shared.Mind.Components;
|
|
using Content.Shared.Power;
|
|
using Content.Shared.Weapons.Melee.Components;
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Physics.Components;
|
|
using Robust.Shared.Utility;
|
|
using Content.Server.Research.Components;
|
|
|
|
namespace Content.Server.Psionics.Glimmer
|
|
{
|
|
public sealed class GlimmerReactiveSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
|
|
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
|
[Dependency] private readonly ElectrocutionSystem _electrocutionSystem = default!;
|
|
[Dependency] private readonly SharedAudioSystem _sharedAudioSystem = default!;
|
|
[Dependency] private readonly SharedAmbientSoundSystem _sharedAmbientSoundSystem = default!;
|
|
[Dependency] private readonly IRobustRandom _random = default!;
|
|
[Dependency] private readonly LightningSystem _lightning = default!;
|
|
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
|
|
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
|
|
[Dependency] private readonly AnchorableSystem _anchorableSystem = default!;
|
|
[Dependency] private readonly SharedDestructibleSystem _destructibleSystem = default!;
|
|
[Dependency] private readonly GhostSystem _ghostSystem = default!;
|
|
[Dependency] private readonly RevenantSystem _revenantSystem = default!;
|
|
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
|
[Dependency] private readonly SharedPointLightSystem _pointLightSystem = default!;
|
|
|
|
public float Accumulator = 0;
|
|
public const float UpdateFrequency = 15f;
|
|
public float BeamCooldown = 3;
|
|
public GlimmerTier LastGlimmerTier = GlimmerTier.Minimal;
|
|
public bool GhostsVisible = false;
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
|
|
|
SubscribeLocalEvent<SharedGlimmerReactiveComponent, MapInitEvent>(OnMapInit);
|
|
SubscribeLocalEvent<SharedGlimmerReactiveComponent, ComponentRemove>(OnComponentRemove);
|
|
SubscribeLocalEvent<SharedGlimmerReactiveComponent, PowerChangedEvent>(OnPowerChanged);
|
|
SubscribeLocalEvent<SharedGlimmerReactiveComponent, GetVerbsEvent<AlternativeVerb>>(AddShockVerb);
|
|
SubscribeLocalEvent<SharedGlimmerReactiveComponent, DamageChangedEvent>(OnDamageChanged);
|
|
SubscribeLocalEvent<SharedGlimmerReactiveComponent, DestructionEventArgs>(OnDestroyed);
|
|
SubscribeLocalEvent<SharedGlimmerReactiveComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
|
|
SubscribeLocalEvent<SharedGlimmerReactiveComponent, AnchorStateChangedEvent>(OnAnchorStateChanged);
|
|
SubscribeLocalEvent<SharedGlimmerReactiveComponent, AttemptMeleeThrowOnHitEvent>(OnMeleeThrowOnHitAttempt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the 'maximum' glimmer value for a given tier.
|
|
/// </summary>
|
|
/// <param name="tier">The tier to get the maximum glimmer value for.</param>
|
|
private int GetMaxGlimmerByTier(GlimmerTier tier)
|
|
{
|
|
return tier switch
|
|
{
|
|
GlimmerTier.Minimal => 49,
|
|
GlimmerTier.Low => 99,
|
|
GlimmerTier.Moderate => 299,
|
|
GlimmerTier.High => 499,
|
|
GlimmerTier.Dangerous => 899,
|
|
GlimmerTier.Critical => 1000,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(tier), tier, null)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update relevant state on an Entity.
|
|
/// </summary>
|
|
/// <param name="glimmerTierDelta">The number of steps in tier
|
|
/// difference since last update. This can be zero for the sake of
|
|
/// toggling the enabled states.</param>
|
|
private void UpdateEntityState(EntityUid uid, SharedGlimmerReactiveComponent component, GlimmerTier currentGlimmerTier, int glimmerTierDelta)
|
|
{
|
|
var isEnabled = true;
|
|
|
|
if (component.RequiresApcPower)
|
|
if (TryComp(uid, out ApcPowerReceiverComponent? apcPower))
|
|
isEnabled = apcPower.Powered;
|
|
|
|
_appearanceSystem.SetData(uid, GlimmerReactiveVisuals.GlimmerTier, isEnabled ? currentGlimmerTier : GlimmerTier.Minimal);
|
|
|
|
// update ambient sound
|
|
if (TryComp(uid, out GlimmerSoundComponent? glimmerSound)
|
|
&& TryComp(uid, out AmbientSoundComponent? ambientSoundComponent)
|
|
&& glimmerSound.GetSound(currentGlimmerTier, out SoundSpecifier? spec))
|
|
{
|
|
if (spec != null)
|
|
_sharedAmbientSoundSystem.SetSound(uid, spec, ambientSoundComponent);
|
|
}
|
|
|
|
// Improve research generation but increase glimmer generation based on tier, if component.ScaleResearchGeneration is true.
|
|
if (component.ScaleResearchGeneration)
|
|
{
|
|
int maxGlimmerByTier = GetMaxGlimmerByTier(currentGlimmerTier);
|
|
if (TryComp<ResearchPointSourceComponent>(uid, out var researchGenerator))
|
|
researchGenerator.PointsPerSecond = (int)(maxGlimmerByTier * component.ResearchGenerationFactor);
|
|
if (TryComp<GlimmerSourceComponent>(uid, out var glimmerSource))
|
|
glimmerSource.SecondsPerGlimmer = 1f / (maxGlimmerByTier * component.GlimmerGenerationFactor);
|
|
}
|
|
|
|
if (component.ModulatesPointLight) //SharedPointLightComponent is now being fetched via TryGetLight.
|
|
if (_pointLightSystem.TryGetLight(uid, out var pointLight))
|
|
{
|
|
_pointLightSystem.SetEnabled(uid, isEnabled ? currentGlimmerTier != GlimmerTier.Minimal : false, pointLight);
|
|
// The light energy and radius are kept updated even when off
|
|
// to prevent the need to store additional state.
|
|
//
|
|
// Note that this doesn't handle edge cases where the
|
|
// PointLightComponent is removed while the
|
|
// GlimmerReactiveComponent is still present.
|
|
_pointLightSystem.SetEnergy(uid, pointLight.Energy + glimmerTierDelta * component.GlimmerToLightEnergyFactor, pointLight);
|
|
_pointLightSystem.SetRadius(uid, pointLight.Radius + glimmerTierDelta * component.GlimmerToLightRadiusFactor, pointLight);
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Track when the component comes online so it can be given the
|
|
/// current status of the glimmer tier, if it wasn't around when an
|
|
/// update went out.
|
|
/// </summary>
|
|
private void OnMapInit(EntityUid uid, SharedGlimmerReactiveComponent component, MapInitEvent args)
|
|
{
|
|
if (component.RequiresApcPower && !HasComp<ApcPowerReceiverComponent>(uid))
|
|
Logger.Warning($"{ToPrettyString(uid)} had RequiresApcPower set to true but no ApcPowerReceiverComponent was found on init.");
|
|
|
|
UpdateEntityState(uid, component, LastGlimmerTier, (int) LastGlimmerTier);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset the glimmer tier appearance data if the component's removed,
|
|
/// just in case some objects can temporarily become reactive to the
|
|
/// glimmer.
|
|
/// </summary>
|
|
private void OnComponentRemove(EntityUid uid, SharedGlimmerReactiveComponent component, ComponentRemove args)
|
|
{
|
|
UpdateEntityState(uid, component, GlimmerTier.Minimal, -1 * (int) LastGlimmerTier);
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the Entity has RequiresApcPower set to true, this will force an
|
|
/// update to the entity's state.
|
|
/// </summary>
|
|
private void OnPowerChanged(EntityUid uid, SharedGlimmerReactiveComponent component, ref PowerChangedEvent args)
|
|
{
|
|
if (component.RequiresApcPower)
|
|
UpdateEntityState(uid, component, LastGlimmerTier, 0);
|
|
}
|
|
|
|
private void AddShockVerb(EntityUid uid, SharedGlimmerReactiveComponent component, GetVerbsEvent<AlternativeVerb> args)
|
|
{
|
|
if(!args.CanAccess || !args.CanInteract)
|
|
return;
|
|
|
|
if (!TryComp<ApcPowerReceiverComponent>(uid, out var receiver))
|
|
return;
|
|
|
|
if (receiver.NeedsPower)
|
|
return;
|
|
|
|
AlternativeVerb verb = new()
|
|
{
|
|
Act = () =>
|
|
{
|
|
_sharedAudioSystem.PlayPvs(component.ShockNoises, args.User);
|
|
_electrocutionSystem.TryDoElectrocution(args.User, null, _glimmerSystem.Glimmer / 200, TimeSpan.FromSeconds((float) _glimmerSystem.Glimmer / 100), false);
|
|
},
|
|
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/Spare/poweronoff.svg.192dpi.png")),
|
|
Text = Loc.GetString("power-switch-component-toggle-verb"),
|
|
Priority = -3
|
|
};
|
|
args.Verbs.Add(verb);
|
|
}
|
|
|
|
private void OnDamageChanged(EntityUid uid, SharedGlimmerReactiveComponent component, DamageChangedEvent args)
|
|
{
|
|
if (args.Origin == null)
|
|
return;
|
|
|
|
if (!_random.Prob((float) _glimmerSystem.Glimmer / 1000))
|
|
return;
|
|
|
|
var tier = _glimmerSystem.GetGlimmerTier();
|
|
if (tier < GlimmerTier.High)
|
|
return;
|
|
Beam(uid, args.Origin.Value, tier);
|
|
}
|
|
|
|
private void OnDestroyed(EntityUid uid, SharedGlimmerReactiveComponent component, DestructionEventArgs args)
|
|
{
|
|
Spawn("MaterialBluespace1", Transform(uid).Coordinates);
|
|
|
|
var tier = _glimmerSystem.GetGlimmerTier();
|
|
if (tier < GlimmerTier.High)
|
|
return;
|
|
|
|
var totalIntensity = (float) (_glimmerSystem.Glimmer * 2);
|
|
var slope = (float) (11 - _glimmerSystem.Glimmer / 100);
|
|
var maxIntensity = 20;
|
|
|
|
var removed = (float) _glimmerSystem.Glimmer * _random.NextFloat(0.06f, 0.08f);
|
|
_glimmerSystem.Glimmer -= (int) removed;
|
|
BeamRandomNearProber(uid, _glimmerSystem.Glimmer / 350, _glimmerSystem.Glimmer / 50);
|
|
_explosionSystem.QueueExplosion(uid, "Default", totalIntensity, slope, maxIntensity);
|
|
}
|
|
|
|
private void OnUnanchorAttempt(EntityUid uid, SharedGlimmerReactiveComponent component, UnanchorAttemptEvent args)
|
|
{
|
|
if (component.Locked)
|
|
{
|
|
_sharedAudioSystem.PlayPvs(component.ShockNoises, args.User);
|
|
_electrocutionSystem.TryDoElectrocution(args.User, null, _glimmerSystem.Glimmer / 200, TimeSpan.FromSeconds((float) _glimmerSystem.Glimmer / 100), false);
|
|
args.Cancel();
|
|
}
|
|
}
|
|
|
|
private void OnAnchorStateChanged(EntityUid uid, SharedGlimmerReactiveComponent component, AnchorStateChangedEvent args)
|
|
{
|
|
if (!args.Anchored && component.Locked)
|
|
{
|
|
AnchorOrExplode(uid);
|
|
}
|
|
}
|
|
|
|
public void BeamRandomNearProber(EntityUid prober, int targets, float range = 10f)
|
|
{
|
|
List<EntityUid> targetList = new();
|
|
var coords = _transform.GetMapCoordinates(prober);
|
|
foreach (var target in _entityLookupSystem.GetEntitiesInRange<StatusEffectsComponent>(coords, range))
|
|
{
|
|
if (target.Comp.AllowedEffects.Contains("Electrocution"))
|
|
targetList.Add(target);
|
|
}
|
|
|
|
foreach(var reactive in _entityLookupSystem.GetEntitiesInRange<SharedGlimmerReactiveComponent>(coords, range))
|
|
{
|
|
targetList.Add(reactive);
|
|
}
|
|
|
|
_random.Shuffle(targetList);
|
|
foreach (var target in targetList)
|
|
{
|
|
if (targets <= 0)
|
|
return;
|
|
|
|
Beam(prober, target, _glimmerSystem.GetGlimmerTier(), false);
|
|
targets--;
|
|
}
|
|
}
|
|
|
|
private void Beam(EntityUid prober, EntityUid target, GlimmerTier tier, bool obeyCD = true)
|
|
{
|
|
if (obeyCD && BeamCooldown != 0)
|
|
return;
|
|
|
|
if (Deleted(prober) || Deleted(target))
|
|
return;
|
|
|
|
var lxform = Transform(prober);
|
|
var txform = Transform(target);
|
|
|
|
if (!lxform.Coordinates.TryDistance(EntityManager, txform.Coordinates, out var distance))
|
|
return;
|
|
if (distance > (float) (_glimmerSystem.Glimmer / 100))
|
|
return;
|
|
|
|
string beamproto;
|
|
|
|
switch (tier)
|
|
{
|
|
case GlimmerTier.Dangerous:
|
|
beamproto = "SuperchargedLightning";
|
|
break;
|
|
case GlimmerTier.Critical:
|
|
beamproto = "HyperchargedLightning";
|
|
break;
|
|
default:
|
|
beamproto = "ChargedLightning";
|
|
break;
|
|
}
|
|
|
|
|
|
_lightning.ShootLightning(prober, target, beamproto);
|
|
BeamCooldown += 3f;
|
|
}
|
|
|
|
public void LockProber(EntityUid uid)
|
|
{
|
|
if (!TryComp<ApcPowerReceiverComponent>(uid, out var powerReceiver))
|
|
return;
|
|
|
|
if (!Transform(uid).Anchored)
|
|
AnchorOrExplode(uid);
|
|
|
|
powerReceiver.PowerDisabled = false;
|
|
powerReceiver.NeedsPower = false;
|
|
|
|
if (TryComp<SharedGlimmerReactiveComponent>(uid, out var glimmerReactive))
|
|
{
|
|
glimmerReactive.Locked = true;
|
|
UpdateEntityState(uid, glimmerReactive, _glimmerSystem.GetGlimmerTier(), 0);
|
|
}
|
|
}
|
|
private void AnchorOrExplode(EntityUid uid)
|
|
{
|
|
var xform = Transform(uid);
|
|
if (xform.Anchored)
|
|
return;
|
|
|
|
if (!TryComp<PhysicsComponent>(uid, out var physics))
|
|
return;
|
|
|
|
var coordinates = xform.Coordinates;
|
|
var gridUid = xform.GridUid;
|
|
|
|
if (TryComp<MapGridComponent>(gridUid, out var grid))
|
|
{
|
|
var tileIndices = grid.TileIndicesFor(coordinates);
|
|
|
|
if (_anchorableSystem.TileFree(grid, tileIndices, physics.CollisionLayer, physics.CollisionMask) &&
|
|
_transform.AnchorEntity(uid, xform))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Wasn't able to get a grid or a free tile, so explode.
|
|
_destructibleSystem.DestroyEntity(uid);
|
|
}
|
|
|
|
private void OnMeleeThrowOnHitAttempt(Entity<SharedGlimmerReactiveComponent> ent, ref AttemptMeleeThrowOnHitEvent args)
|
|
{
|
|
var (uid, _) = ent;
|
|
|
|
if (_glimmerSystem.GetGlimmerTier() < GlimmerTier.Dangerous)
|
|
return;
|
|
|
|
args.Cancelled = true;
|
|
args.Handled = true;
|
|
|
|
_lightning.ShootRandomLightnings(uid, 10, 2, "SuperchargedLightning", 2, false);
|
|
|
|
// Check if the parent of the user is alive, which will be the case if the user is an item and is being held.
|
|
if (args.User is {} user && HasComp<MindContainerComponent>(args.User))
|
|
_electrocutionSystem.TryDoElectrocution(user, uid, 5, TimeSpan.FromSeconds(3), true,
|
|
ignoreInsulation: true);
|
|
}
|
|
|
|
private void Reset(RoundRestartCleanupEvent args)
|
|
{
|
|
Accumulator = 0;
|
|
|
|
// It is necessary that the GlimmerTier is reset to the default
|
|
// tier on round restart. This system will persist through
|
|
// restarts, and an undesired event will fire as a result after the
|
|
// start of the new round, causing modulatable PointLights to have
|
|
// negative Energy if the tier was higher than Minimal on restart.
|
|
LastGlimmerTier = GlimmerTier.Minimal;
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
Accumulator += frameTime;
|
|
BeamCooldown = Math.Max(0, BeamCooldown - frameTime);
|
|
|
|
if (Accumulator > UpdateFrequency)
|
|
{
|
|
var currentGlimmerTier = _glimmerSystem.GetGlimmerTier();
|
|
|
|
var reactives = EntityQuery<SharedGlimmerReactiveComponent>();
|
|
if (currentGlimmerTier != LastGlimmerTier) {
|
|
var glimmerTierDelta = (int) currentGlimmerTier - (int) LastGlimmerTier;
|
|
var ev = new GlimmerTierChangedEvent(LastGlimmerTier, currentGlimmerTier, glimmerTierDelta);
|
|
|
|
foreach (var reactive in reactives)
|
|
{
|
|
UpdateEntityState(reactive.Owner, reactive, currentGlimmerTier, glimmerTierDelta);
|
|
RaiseLocalEvent(reactive.Owner, ev);
|
|
}
|
|
|
|
LastGlimmerTier = currentGlimmerTier;
|
|
}
|
|
if (currentGlimmerTier == GlimmerTier.Critical)
|
|
{
|
|
_ghostSystem.MakeVisible(true);
|
|
_revenantSystem.MakeVisible(true);
|
|
GhostsVisible = true;
|
|
foreach (var reactive in reactives)
|
|
{
|
|
BeamRandomNearProber(reactive.Owner, 1, 12);
|
|
}
|
|
} else if (GhostsVisible == true)
|
|
{
|
|
_ghostSystem.MakeVisible(false);
|
|
_revenantSystem.MakeVisible(false);
|
|
GhostsVisible = false;
|
|
}
|
|
Accumulator = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This event is fired when the broader glimmer tier has changed,
|
|
/// not on every single adjustment to the glimmer count.
|
|
///
|
|
/// <see cref="GlimmerSystem.GetGlimmerTier"/> has the exact
|
|
/// values corresponding to tiers.
|
|
/// </summary>
|
|
public sealed class GlimmerTierChangedEvent : EntityEventArgs
|
|
{
|
|
/// <summary>
|
|
/// What was the last glimmer tier before this event fired?
|
|
/// </summary>
|
|
public readonly GlimmerTier LastTier;
|
|
|
|
/// <summary>
|
|
/// What is the current glimmer tier?
|
|
/// </summary>
|
|
public readonly GlimmerTier CurrentTier;
|
|
|
|
/// <summary>
|
|
/// What is the change in tiers between the last and current tier?
|
|
/// </summary>
|
|
public readonly int TierDelta;
|
|
|
|
public GlimmerTierChangedEvent(GlimmerTier lastTier, GlimmerTier currentTier, int tierDelta)
|
|
{
|
|
LastTier = lastTier;
|
|
CurrentTier = currentTier;
|
|
TierDelta = tierDelta;
|
|
}
|
|
}
|
|
}
|