This commit is contained in:
Nemanja
2023-01-17 00:05:20 -05:00
committed by GitHub
parent 06f19dafc9
commit 9cd0c11870
85 changed files with 3109 additions and 54 deletions

View File

@@ -0,0 +1,114 @@
using Content.Server.Anomaly.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Anomaly;
using Content.Shared.Materials;
using Robust.Shared.Map.Components;
namespace Content.Server.Anomaly;
/// <summary>
/// This handles anomalous vessel as well as
/// the calculations for how many points they
/// should produce.
/// </summary>
public sealed partial class AnomalySystem
{
/// <summary>
/// A multiplier applied to the grid bounds
/// to make the likelihood of it spawning outside
/// of the main station less likely.
///
/// tl;dr anomalies only generate on the inner __% of the station.
/// </summary>
public const float GridBoundsMultiplier = 0.6f;
private void InitializeGenerator()
{
SubscribeLocalEvent<AnomalyGeneratorComponent, BoundUIOpenedEvent>(OnGeneratorBUIOpened);
SubscribeLocalEvent<AnomalyGeneratorComponent, MaterialAmountChangedEvent>(OnGeneratorMaterialAmountChanged);
SubscribeLocalEvent<AnomalyGeneratorComponent, AnomalyGeneratorGenerateButtonPressedEvent>(OnGenerateButtonPressed);
SubscribeLocalEvent<AnomalyGeneratorComponent, PowerChangedEvent>(OnGeneratorPowerChanged);
}
private void OnGeneratorPowerChanged(EntityUid uid, AnomalyGeneratorComponent component, ref PowerChangedEvent args)
{
_ambient.SetAmbience(uid, args.Powered);
}
private void OnGeneratorBUIOpened(EntityUid uid, AnomalyGeneratorComponent component, BoundUIOpenedEvent args)
{
UpdateGeneratorUi(uid, component);
}
private void OnGeneratorMaterialAmountChanged(EntityUid uid, AnomalyGeneratorComponent component, ref MaterialAmountChangedEvent args)
{
UpdateGeneratorUi(uid, component);
}
private void OnGenerateButtonPressed(EntityUid uid, AnomalyGeneratorComponent component, AnomalyGeneratorGenerateButtonPressedEvent args)
{
TryGeneratorCreateAnomaly(uid, component);
}
public void UpdateGeneratorUi(EntityUid uid, AnomalyGeneratorComponent component)
{
var materialAmount = _material.GetMaterialAmount(uid, component.RequiredMaterial);
var state = new AnomalyGeneratorUserInterfaceState(component.CooldownEndTime, materialAmount, component.MaterialPerAnomaly);
_ui.TrySetUiState(uid, AnomalyGeneratorUiKey.Key, state);
}
public void TryGeneratorCreateAnomaly(EntityUid uid, AnomalyGeneratorComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (!this.IsPowered(uid, EntityManager))
return;
if (Timing.CurTime < component.CooldownEndTime)
return;
var grid = Transform(uid).GridUid;
if (grid == null)
return;
if (!_material.TryChangeMaterialAmount(uid, component.RequiredMaterial, -component.MaterialPerAnomaly))
return;
SpawnOnRandomGridLocation(grid.Value, component.SpawnerPrototype);
component.CooldownEndTime = Timing.CurTime + component.CooldownLength;
UpdateGeneratorUi(uid, component);
}
private void SpawnOnRandomGridLocation(EntityUid grid, string toSpawn)
{
if (!TryComp<MapGridComponent>(grid, out var gridComp))
return;
var xform = Transform(grid);
var targetCoords = xform.Coordinates;
var (gridPos, _, gridMatrix) = _transform.GetWorldPositionRotationMatrix(xform);
var gridBounds = gridMatrix.TransformBox(gridComp.LocalAABB);
for (var i = 0; i < 25; i++)
{
var randomX = Random.Next((int) (gridBounds.Left * GridBoundsMultiplier), (int) (gridBounds.Right * GridBoundsMultiplier));
var randomY = Random.Next((int) (gridBounds.Bottom * GridBoundsMultiplier), (int) (gridBounds.Top * GridBoundsMultiplier));
var tile = new Vector2i(randomX - (int) gridPos.X, randomY - (int) gridPos.Y);
if (_atmosphere.IsTileSpace(grid, Transform(grid).MapUid, tile,
mapGridComp: gridComp) || _atmosphere.IsTileAirBlocked(grid, tile, mapGridComp: gridComp))
{
continue;
}
targetCoords = gridComp.GridTileToLocal(tile);
break;
}
Spawn(toSpawn, targetCoords);
}
}

View File

@@ -0,0 +1,169 @@
using Content.Server.Anomaly.Components;
using Content.Server.DoAfter;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.Utility;
namespace Content.Server.Anomaly;
/// <summary>
/// This handles the anomaly scanner and it's UI updates.
/// </summary>
public sealed partial class AnomalySystem
{
private void InitializeScanner()
{
SubscribeLocalEvent<AnomalyScannerComponent, BoundUIOpenedEvent>(OnScannerUiOpened);
SubscribeLocalEvent<AnomalyScannerComponent, AfterInteractEvent>(OnScannerAfterInteract);
SubscribeLocalEvent<AnomalyScannerComponent, AnomalyScanFinishedEvent>(OnScannerDoAfterFinished);
SubscribeLocalEvent<AnomalyScannerComponent, AnomalyScanCancelledEvent>(OnScannerDoAfterCancelled);
SubscribeLocalEvent<AnomalyShutdownEvent>(OnScannerAnomalyShutdown);
SubscribeLocalEvent<AnomalySeverityChangedEvent>(OnScannerAnomalySeverityChanged);
SubscribeLocalEvent<AnomalyStabilityChangedEvent>(OnScannerAnomalyStabilityChanged);
SubscribeLocalEvent<AnomalyHealthChangedEvent>(OnScannerAnomalyHealthChanged);
}
private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args)
{
foreach (var component in EntityQuery<AnomalyScannerComponent>())
{
if (component.ScannedAnomaly != args.Anomaly)
continue;
_ui.TryCloseAll(component.Owner, AnomalyScannerUiKey.Key);
}
}
private void OnScannerAnomalySeverityChanged(ref AnomalySeverityChangedEvent args)
{
foreach (var component in EntityQuery<AnomalyScannerComponent>())
{
if (component.ScannedAnomaly != args.Anomaly)
continue;
UpdateScannerUi(component.Owner, component);
}
}
private void OnScannerAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args)
{
foreach (var component in EntityQuery<AnomalyScannerComponent>())
{
if (component.ScannedAnomaly != args.Anomaly)
continue;
UpdateScannerUi(component.Owner, component);
}
}
private void OnScannerAnomalyHealthChanged(ref AnomalyHealthChangedEvent args)
{
foreach (var component in EntityQuery<AnomalyScannerComponent>())
{
if (component.ScannedAnomaly != args.Anomaly)
continue;
UpdateScannerUi(component.Owner, component);
}
}
private void OnScannerUiOpened(EntityUid uid, AnomalyScannerComponent component, BoundUIOpenedEvent args)
{
UpdateScannerUi(uid, component);
}
private void OnScannerAfterInteract(EntityUid uid, AnomalyScannerComponent component, AfterInteractEvent args)
{
if (component.TokenSource != null)
return;
if (args.Target is not { } target)
return;
if (!HasComp<AnomalyComponent>(target))
return;
component.TokenSource = new();
_doAfter.DoAfter(new DoAfterEventArgs(args.User, component.ScanDoAfterDuration, component.TokenSource.Token, target, uid)
{
DistanceThreshold = 2f,
UsedFinishedEvent = new AnomalyScanFinishedEvent(target, args.User),
UsedCancelledEvent = new AnomalyScanCancelledEvent()
});
}
private void OnScannerDoAfterFinished(EntityUid uid, AnomalyScannerComponent component, AnomalyScanFinishedEvent args)
{
component.TokenSource = null;
Audio.PlayPvs(component.CompleteSound, uid);
_popup.PopupEntity(Loc.GetString("anomaly-scanner-component-scan-complete"), uid);
UpdateScannerWithNewAnomaly(uid, args.Anomaly, component);
if (TryComp<ActorComponent>(args.User, out var actor))
_ui.TryOpen(uid, AnomalyScannerUiKey.Key, actor.PlayerSession);
}
private void OnScannerDoAfterCancelled(EntityUid uid, AnomalyScannerComponent component, AnomalyScanCancelledEvent args)
{
component.TokenSource = null;
}
public void UpdateScannerUi(EntityUid uid, AnomalyScannerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
TimeSpan? nextPulse = null;
if (TryComp<AnomalyComponent>(component.ScannedAnomaly, out var anomalyComponent))
nextPulse = anomalyComponent.NextPulseTime;
var state = new AnomalyScannerUserInterfaceState(GetScannerMessage(component), nextPulse);
_ui.TrySetUiState(uid, AnomalyScannerUiKey.Key, state);
}
public void UpdateScannerWithNewAnomaly(EntityUid scanner, EntityUid anomaly, AnomalyScannerComponent? scannerComp = null, AnomalyComponent? anomalyComp = null)
{
if (!Resolve(scanner, ref scannerComp) || !Resolve(anomaly, ref anomalyComp))
return;
scannerComp.ScannedAnomaly = anomaly;
UpdateScannerUi(scanner, scannerComp);
}
public FormattedMessage GetScannerMessage(AnomalyScannerComponent component)
{
var msg = new FormattedMessage();
if (component.ScannedAnomaly is not { } anomaly || !TryComp<AnomalyComponent>(anomaly, out var anomalyComp))
{
msg.AddMarkup(Loc.GetString("anomaly-scanner-no-anomaly"));
return msg;
}
msg.AddMarkup(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P"))));
msg.PushNewline();
string stateLoc;
if (anomalyComp.Stability < anomalyComp.DecayThreshold)
stateLoc = Loc.GetString("anomaly-scanner-stability-low");
else if (anomalyComp.Stability > anomalyComp.GrowthThreshold)
stateLoc = Loc.GetString("anomaly-scanner-stability-high");
else
stateLoc = Loc.GetString("anomaly-scanner-stability-medium");
msg.AddMarkup(stateLoc);
msg.PushNewline();
var points = GetAnomalyPointValue(anomaly, anomalyComp) / 10 * 10; //round to tens place
msg.AddMarkup(Loc.GetString("anomaly-scanner-point-output", ("point", points)));
msg.PushNewline();
msg.PushNewline();
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-readout"));
msg.PushNewline();
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType))));
msg.PushNewline();
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType))));
msg.PushNewline();
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType))));
//The timer at the end here is actually added in the ui itself.
return msg;
}
}

View File

@@ -0,0 +1,129 @@
using Content.Server.Anomaly.Components;
using Content.Server.Construction;
using Content.Server.Power.EntitySystems;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Research.Components;
namespace Content.Server.Anomaly;
/// <summary>
/// This handles anomalous vessel as well as
/// the calculations for how many points they
/// should produce.
/// </summary>
public sealed partial class AnomalySystem
{
private void InitializeVessel()
{
SubscribeLocalEvent<AnomalyVesselComponent, ComponentShutdown>(OnVesselShutdown);
SubscribeLocalEvent<AnomalyVesselComponent, MapInitEvent>(OnVesselMapInit);
SubscribeLocalEvent<AnomalyVesselComponent, RefreshPartsEvent>(OnRefreshParts);
SubscribeLocalEvent<AnomalyVesselComponent, InteractUsingEvent>(OnVesselInteractUsing);
SubscribeLocalEvent<AnomalyVesselComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<AnomalyVesselComponent, ResearchServerGetPointsPerSecondEvent>(OnVesselGetPointsPerSecond);
SubscribeLocalEvent<AnomalyShutdownEvent>(OnVesselAnomalyShutdown);
}
private void OnExamined(EntityUid uid, AnomalyVesselComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushText(component.Anomaly == null
? Loc.GetString("anomaly-vessel-component-not-assigned")
: Loc.GetString("anomaly-vessel-component-assigned"));
}
private void OnVesselShutdown(EntityUid uid, AnomalyVesselComponent component, ComponentShutdown args)
{
if (component.Anomaly is not { } anomaly)
return;
if (!TryComp<AnomalyComponent>(anomaly, out var anomalyComp))
return;
anomalyComp.ConnectedVessel = null;
}
private void OnVesselMapInit(EntityUid uid, AnomalyVesselComponent component, MapInitEvent args)
{
UpdateVesselAppearance(uid, component);
}
private void OnRefreshParts(EntityUid uid, AnomalyVesselComponent component, RefreshPartsEvent args)
{
var modifierRating = args.PartRatings[component.MachinePartPointModifier] - 1;
component.PointMultiplier = MathF.Pow(component.PartRatingPointModifier, modifierRating);
}
private void OnVesselInteractUsing(EntityUid uid, AnomalyVesselComponent component, InteractUsingEvent args)
{
if (component.Anomaly != null ||
!TryComp<AnomalyScannerComponent>(args.Used, out var scanner) ||
scanner.ScannedAnomaly is not {} anomaly)
{
return;
}
if (!TryComp<AnomalyComponent>(anomaly, out var anomalyComponent) || anomalyComponent.ConnectedVessel != null)
return;
component.Anomaly = scanner.ScannedAnomaly;
anomalyComponent.ConnectedVessel = uid;
UpdateVesselAppearance(uid, component);
_popup.PopupEntity(Loc.GetString("anomaly-vessel-component-anomaly-assigned"), uid);
}
private void OnVesselGetPointsPerSecond(EntityUid uid, AnomalyVesselComponent component, ref ResearchServerGetPointsPerSecondEvent args)
{
if (!this.IsPowered(uid, EntityManager) || component.Anomaly is not {} anomaly)
{
args.Points = 0;
return;
}
args.Points += (int) (GetAnomalyPointValue(anomaly) * component.PointMultiplier);
}
private void OnVesselAnomalyShutdown(ref AnomalyShutdownEvent args)
{
foreach (var component in EntityQuery<AnomalyVesselComponent>())
{
var ent = component.Owner;
if (args.Anomaly != component.Anomaly)
continue;
component.Anomaly = null;
UpdateVesselAppearance(ent, component);
if (!args.Supercritical)
continue;
_explosion.TriggerExplosive(ent);
}
}
/// <summary>
/// Updates the appearance of an anomaly vessel
/// based on whether or not it has an anomaly
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
public void UpdateVesselAppearance(EntityUid uid, AnomalyVesselComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var on = component.Anomaly != null;
Appearance.SetData(uid, AnomalyVesselVisuals.HasAnomaly, on);
if (TryComp<SharedPointLightComponent>(uid, out var pointLightComponent))
{
pointLightComponent.Enabled = on;
}
_ambient.SetAmbience(uid, on);
}
}

View File

@@ -0,0 +1,127 @@
using Content.Server.Anomaly.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Audio;
using Content.Server.DoAfter;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Materials;
using Content.Server.Popups;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Physics.Events;
using Robust.Shared.Random;
namespace Content.Server.Anomaly;
/// <summary>
/// This handles logic and interactions relating to <see cref="AnomalyComponent"/>
/// </summary>
public sealed partial class AnomalySystem : SharedAnomalySystem
{
[Dependency] private readonly AmbientSoundSystem _ambient = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly ExplosionSystem _explosion = default!;
[Dependency] private readonly MaterialStorageSystem _material = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
public const float MinParticleVariation = 0.8f;
public const float MaxParticleVariation = 1.2f;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnomalyComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<AnomalyComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<AnomalyComponent, StartCollideEvent>(OnStartCollide);
InitializeGenerator();
InitializeScanner();
InitializeVessel();
}
private void OnMapInit(EntityUid uid, AnomalyComponent component, MapInitEvent args)
{
component.NextPulseTime = Timing.CurTime + GetPulseLength(component) * 3; // longer the first time
ChangeAnomalyStability(uid, Random.NextFloat(component.InitialStabilityRange.Item1 , component.InitialStabilityRange.Item2), component);
ChangeAnomalySeverity(uid, Random.NextFloat(component.InitialSeverityRange.Item1, component.InitialSeverityRange.Item2), component);
var particles = new List<AnomalousParticleType>
{ AnomalousParticleType.Delta, AnomalousParticleType.Epsilon, AnomalousParticleType.Zeta };
component.SeverityParticleType = Random.PickAndTake(particles);
component.DestabilizingParticleType = Random.PickAndTake(particles);
component.WeakeningParticleType = Random.PickAndTake(particles);
}
private void OnShutdown(EntityUid uid, AnomalyComponent component, ComponentShutdown args)
{
EndAnomaly(uid, component);
}
private void OnStartCollide(EntityUid uid, AnomalyComponent component, ref StartCollideEvent args)
{
if (!TryComp<AnomalousParticleComponent>(args.OtherFixture.Body.Owner, out var particleComponent))
return;
if (args.OtherFixture.ID != particleComponent.FixtureId)
return;
// small function to randomize because it's easier to read like this
float VaryValue(float v) => v * Random.NextFloat(MinParticleVariation, MaxParticleVariation);
if (particleComponent.ParticleType == component.DestabilizingParticleType)
{
ChangeAnomalyStability(uid, VaryValue(component.StabilityPerDestabilizingHit), component);
}
else if (particleComponent.ParticleType == component.SeverityParticleType)
{
ChangeAnomalySeverity(uid, VaryValue(component.SeverityPerSeverityHit), component);
}
else if (particleComponent.ParticleType == component.WeakeningParticleType)
{
ChangeAnomalyHealth(uid, VaryValue(component.HealthPerWeakeningeHit), component);
ChangeAnomalyStability(uid, VaryValue(component.StabilityPerWeakeningeHit), component);
}
}
/// <summary>
/// Gets the amount of research points generated per second for an anomaly.
/// </summary>
/// <param name="anomaly"></param>
/// <param name="component"></param>
/// <returns>The amount of points</returns>
public int GetAnomalyPointValue(EntityUid anomaly, AnomalyComponent? component = null)
{
if (!Resolve(anomaly, ref component, false))
return 0;
var multiplier = 1f;
if (component.Stability > component.GrowthThreshold)
multiplier = 1.25f; //more points for unstable
else if (component.Stability < component.DecayThreshold)
multiplier = 0.75f; //less points if it's dying
//penalty of up to 50% based on health
multiplier *= MathF.Pow(1.5f, component.Health) - 0.5f;
return (int) ((component.MaxPointsPerSecond - component.MinPointsPerSecond) * component.Severity * multiplier);
}
/// <summary>
/// Gets the localized name of a particle.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public string GetParticleLocale(AnomalousParticleType type)
{
return type switch
{
AnomalousParticleType.Delta => Loc.GetString("anomaly-particles-delta"),
AnomalousParticleType.Epsilon => Loc.GetString("anomaly-particles-epsilon"),
AnomalousParticleType.Zeta => Loc.GetString("anomaly-particles-zeta"),
_ => throw new ArgumentOutOfRangeException()
};
}
}

View File

@@ -0,0 +1,23 @@
using Content.Shared.Anomaly;
namespace Content.Server.Anomaly.Components;
/// <summary>
/// This is used for projectiles which affect anomalies through colliding with them.
/// </summary>
[RegisterComponent]
public sealed class AnomalousParticleComponent : Component
{
/// <summary>
/// The type of particle that the projectile
/// imbues onto the anomaly on contact.
/// </summary>
[DataField("particleType", required: true)]
public AnomalousParticleType ParticleType;
/// <summary>
/// The fixture that's checked on collision.
/// </summary>
[DataField("fixtureId")]
public string FixtureId = "projectile";
}

View File

@@ -0,0 +1,44 @@
using Content.Shared.Materials;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Anomaly.Components;
/// <summary>
/// This is used for a machine that is able to generate
/// anomalies randomly on the station.
/// </summary>
[RegisterComponent]
public sealed class AnomalyGeneratorComponent : Component
{
/// <summary>
/// The time at which the cooldown for generating another anomaly will be over
/// </summary>
[DataField("cooldownEndTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan CooldownEndTime = TimeSpan.Zero;
/// <summary>
/// The cooldown between generating anomalies.
/// </summary>
[DataField("cooldownLength"), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan CooldownLength = TimeSpan.FromMinutes(5);
/// <summary>
/// The material needed to generate an anomaly
/// </summary>
[DataField("requiredMaterial", customTypeSerializer: typeof(PrototypeIdSerializer<MaterialPrototype>)), ViewVariables(VVAccess.ReadWrite)]
public string RequiredMaterial = "Plasma";
/// <summary>
/// The amount of material needed to generate a single anomaly
/// </summary>
[DataField("materialPerAnomaly"), ViewVariables(VVAccess.ReadWrite)]
public int MaterialPerAnomaly = 1500; // a bit less than a stack of plasma
/// <summary>
/// The random anomaly spawner entity
/// </summary>
[DataField("spawnerPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
public string SpawnerPrototype = "RandomAnomalySpawner";
}

View File

@@ -0,0 +1,49 @@
using System.Threading;
using Robust.Shared.Audio;
namespace Content.Server.Anomaly.Components;
/// <summary>
/// This is used for scanning anomalies and
/// displaying information about them in the ui
/// </summary>
[RegisterComponent]
public sealed class AnomalyScannerComponent : Component
{
/// <summary>
/// The anomaly that was last scanned by this scanner.
/// </summary>
[ViewVariables]
public EntityUid? ScannedAnomaly;
/// <summary>
/// How long the scan takes
/// </summary>
[DataField("scanDoAfterDuration")]
public float ScanDoAfterDuration = 5;
public CancellationTokenSource? TokenSource;
/// <summary>
/// The sound plays when the scan finished
/// </summary>
[DataField("completeSound")]
public SoundSpecifier? CompleteSound = new SoundPathSpecifier("/Audio/Items/beep.ogg");
}
public sealed class AnomalyScanFinishedEvent : EntityEventArgs
{
public EntityUid Anomaly;
public EntityUid User;
public AnomalyScanFinishedEvent(EntityUid anomaly, EntityUid user)
{
Anomaly = anomaly;
User = user;
}
}
public sealed class AnomalyScanCancelledEvent : EntityEventArgs
{
}

View File

@@ -0,0 +1,40 @@
using Content.Shared.Construction.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Anomaly.Components;
/// <summary>
/// Anomaly Vessels can have an anomaly "stored" in them
/// by interacting on them with an anomaly scanner. Then,
/// they generate points for the selected server based on
/// the anomaly's stability and severity.
/// </summary>
[RegisterComponent]
public sealed class AnomalyVesselComponent : Component
{
/// <summary>
/// The anomaly that the vessel is storing.
/// Can be null.
/// </summary>
[ViewVariables]
public EntityUid? Anomaly;
/// <summary>
/// A multiplier applied to the amount of points generated.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float PointMultiplier = 1;
/// <summary>
/// The machine part that affects the point multiplier of the vessel
/// </summary>
[DataField("machinePartPointModifier", customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>))]
public string MachinePartPointModifier = "ScanningModule";
/// <summary>
/// A value used to scale the point multiplier
/// with the corresponding part rating.
/// </summary>
[DataField("partRatingPointModifier")]
public float PartRatingPointModifier = 1.5f;
}

View File

@@ -0,0 +1,61 @@
using Content.Server.Electrocution;
using Content.Server.Lightning;
using Content.Server.Power.Components;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.StatusEffect;
using Robust.Shared.Random;
namespace Content.Server.Anomaly.Effects;
public sealed class ElectricityAnomalySystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly LightningSystem _lightning = default!;
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ElectricityAnomalyComponent, AnomalyPulseEvent>(OnPulse);
SubscribeLocalEvent<ElectricityAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical);
}
private void OnPulse(EntityUid uid, ElectricityAnomalyComponent component, ref AnomalyPulseEvent args)
{
var range = component.MaxElectrocuteRange * args.Stabiltiy;
var damage = (int) (component.MaxElectrocuteDamage * args.Severity);
var duration = component.MaxElectrocuteDuration * args.Severity;
var xform = Transform(uid);
foreach (var comp in _lookup.GetComponentsInRange<StatusEffectsComponent>(xform.MapPosition, range))
{
var ent = comp.Owner;
_electrocution.TryDoElectrocution(ent, uid, damage, duration, true, statusEffects: comp, ignoreInsulation: true);
}
}
private void OnSupercritical(EntityUid uid, ElectricityAnomalyComponent component, ref AnomalySupercriticalEvent args)
{
var poweredQuery = GetEntityQuery<ApcPowerReceiverComponent>();
var mobQuery = GetEntityQuery<MobThresholdsComponent>();
var validEnts = new HashSet<EntityUid>();
foreach (var ent in _lookup.GetEntitiesInRange(uid, component.MaxElectrocuteRange * 2))
{
if (mobQuery.HasComponent(ent))
validEnts.Add(ent);
if (_random.Prob(0.1f) && poweredQuery.HasComponent(ent))
validEnts.Add(ent);
}
// goodbye, sweet perf
foreach (var ent in validEnts)
{
_lightning.ShootLightning(uid, ent);
}
}
}

View File

@@ -0,0 +1,39 @@
using Content.Server.Singularity.Components;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects;
using Content.Shared.Anomaly.Effects.Components;
using Content.Shared.Radiation.Components;
namespace Content.Server.Anomaly.Effects;
/// <summary>
/// This handles logic and events relating to <see cref="GravityAnomalyComponent"/> and <seealso cref="AnomalySystem"/>
/// </summary>
public sealed class GravityAnomalySystem : SharedGravityAnomalySystem
{
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GravityAnomalyComponent, AnomalySeverityChangedEvent>(OnSeverityChanged);
SubscribeLocalEvent<GravityAnomalyComponent, AnomalyStabilityChangedEvent>(OnStabilityChanged);
}
private void OnSeverityChanged(EntityUid uid, GravityAnomalyComponent component, ref AnomalySeverityChangedEvent args)
{
if (TryComp<RadiationSourceComponent>(uid, out var radSource))
radSource.Intensity = component.MaxRadiationIntensity * args.Severity;
if (!TryComp<GravityWellComponent>(uid, out var gravityWell))
return;
var accel = (component.MaxAccel - component.MinAccel) * args.Severity + component.MinAccel;
gravityWell.BaseRadialAcceleration = accel;
gravityWell.BaseTangentialAcceleration = accel * 0.2f;
}
private void OnStabilityChanged(EntityUid uid, GravityAnomalyComponent component, ref AnomalyStabilityChangedEvent args)
{
if (TryComp<GravityWellComponent>(uid, out var gravityWell))
gravityWell.MaxRange = component.MaxGravityWellRange * args.Stability;
}
}

View File

@@ -0,0 +1,100 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Interaction;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.Anomaly.Effects;
/// <summary>
/// This handles <see cref="PyroclasticAnomalyComponent"/> and the events from <seealso cref="AnomalySystem"/>
/// </summary>
public sealed class PyroclasticAnomalySystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly FlammableSystem _flammable = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly TransformSystem _xform = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<PyroclasticAnomalyComponent, AnomalyPulseEvent>(OnPulse);
SubscribeLocalEvent<PyroclasticAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical);
}
private void OnPulse(EntityUid uid, PyroclasticAnomalyComponent component, ref AnomalyPulseEvent args)
{
var xform = Transform(uid);
var ignitionRadius = component.MaximumIgnitionRadius * args.Stabiltiy;
IgniteNearby(xform.Coordinates, args.Severity, ignitionRadius);
}
private void OnSupercritical(EntityUid uid, PyroclasticAnomalyComponent component, ref AnomalySupercriticalEvent args)
{
var xform = Transform(uid);
var grid = xform.GridUid;
var map = xform.MapUid;
var indices = _xform.GetGridOrMapTilePosition(uid, xform);
var mixture = _atmosphere.GetTileMixture(grid, map, indices, true);
if (mixture == null)
return;
mixture.AdjustMoles(component.SupercriticalGas, component.SupercriticalMoleAmount);
if (grid is { })
{
foreach (var ind in _atmosphere.GetAdjacentTiles(grid.Value, indices))
{
var mix = _atmosphere.GetTileMixture(grid, map, indices, true);
if (mix is not {})
continue;
mix.AdjustMoles(component.SupercriticalGas, component.SupercriticalMoleAmount);
mix.Temperature += component.HotspotExposeTemperature;
_atmosphere.HotspotExpose(grid.Value, indices, component.HotspotExposeTemperature, mix?.Volume ?? component.SupercriticalMoleAmount, true);
}
}
IgniteNearby(xform.Coordinates, 1, component.MaximumIgnitionRadius * 2);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var (pyro, anom, xform) in EntityQuery<PyroclasticAnomalyComponent, AnomalyComponent, TransformComponent>())
{
var ent = pyro.Owner;
var grid = xform.GridUid;
var map = xform.MapUid;
var indices = _xform.GetGridOrMapTilePosition(ent, xform);
var mixture = _atmosphere.GetTileMixture(grid, map, indices, true);
if (mixture is { })
{
mixture.Temperature += pyro.HeatPerSecond * anom.Severity * frameTime;
}
if (grid != null && anom.Severity > pyro.AnomalyHotspotThreshold)
{
_atmosphere.HotspotExpose(grid.Value, indices, pyro.HotspotExposeTemperature, pyro.HotspotExposeVolume, true);
}
}
}
public void IgniteNearby(EntityCoordinates coordinates, float severity, float radius)
{
foreach (var flammable in _lookup.GetComponentsInRange<FlammableComponent>(coordinates, radius))
{
var ent = flammable.Owner;
if (!_interaction.InRangeUnobstructed(coordinates.ToMap(EntityManager), ent, -1))
continue;
var stackAmount = 1 + (int) (severity / 0.25f);
_flammable.AdjustFireStacks(ent, stackAmount, flammable);
_flammable.Ignite(ent, flammable);
}
}
}