Anomalies (#13371)
This commit is contained in:
114
Content.Server/Anomaly/AnomalySystem.Generator.cs
Normal file
114
Content.Server/Anomaly/AnomalySystem.Generator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
169
Content.Server/Anomaly/AnomalySystem.Scanner.cs
Normal file
169
Content.Server/Anomaly/AnomalySystem.Scanner.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
129
Content.Server/Anomaly/AnomalySystem.Vessel.cs
Normal file
129
Content.Server/Anomaly/AnomalySystem.Vessel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
127
Content.Server/Anomaly/AnomalySystem.cs
Normal file
127
Content.Server/Anomaly/AnomalySystem.cs
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
49
Content.Server/Anomaly/Components/AnomalyScannerComponent.cs
Normal file
49
Content.Server/Anomaly/Components/AnomalyScannerComponent.cs
Normal 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
|
||||
{
|
||||
}
|
||||
40
Content.Server/Anomaly/Components/AnomalyVesselComponent.cs
Normal file
40
Content.Server/Anomaly/Components/AnomalyVesselComponent.cs
Normal 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;
|
||||
}
|
||||
61
Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs
Normal file
61
Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Content.Server/Anomaly/Effects/GravityAnomalySystem.cs
Normal file
39
Content.Server/Anomaly/Effects/GravityAnomalySystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
100
Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs
Normal file
100
Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user