--- /dev/null
+using Content.Server.Anomaly.Components;
+using Content.Server.DeviceLinking.Systems;
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Shared.Anomaly.Components;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+
+namespace Content.Server.Anomaly;
+
+/// <summary>
+/// a device that allows you to translate anomaly activity into multitool signals.
+/// </summary>
+public sealed partial class AnomalySynchronizerSystem : EntitySystem
+{
+ [Dependency] private readonly AnomalySystem _anomaly = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+ [Dependency] private readonly DeviceLinkSystem _signal = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly PowerReceiverSystem _power = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<AnomalySynchronizerComponent, InteractHandEvent>(OnInteractHand);
+ SubscribeLocalEvent<AnomalySynchronizerComponent, PowerChangedEvent>(OnPowerChanged);
+
+ SubscribeLocalEvent<AnomalyPulseEvent>(OnAnomalyPulse);
+ SubscribeLocalEvent<AnomalySeverityChangedEvent>(OnAnomalySeverityChanged);
+ SubscribeLocalEvent<AnomalyStabilityChangedEvent>(OnAnomalyStabilityChanged);
+ }
+
+ private void OnPowerChanged(EntityUid uid, AnomalySynchronizerComponent component, ref PowerChangedEvent args)
+ {
+ if (args.Powered)
+ return;
+
+ if (!TryComp<AnomalyComponent>(component.ConnectedAnomaly, out var anomaly))
+ return;
+
+ _anomaly.DoAnomalyPulse(component.ConnectedAnomaly.Value, anomaly);
+ DisconneсtFromAnomaly(uid, component, anomaly);
+ }
+
+ private void OnInteractHand(EntityUid uid, AnomalySynchronizerComponent component, InteractHandEvent args)
+ {
+ if (!_power.IsPowered(uid))
+ return;
+
+ foreach (var entity in _entityLookup.GetEntitiesInRange(uid, 0.15f)) //is the radius of one tile. It must not be set higher, otherwise the anomaly can be moved from tile to tile
+ {
+ if (!TryComp<AnomalyComponent>(entity, out var anomaly))
+ continue;
+
+
+ ConnectToAnomaly(uid, component, entity, anomaly);
+ break;
+ }
+ }
+
+ private void ConnectToAnomaly(EntityUid uid, AnomalySynchronizerComponent component, EntityUid auid, AnomalyComponent anomaly)
+ {
+ if (component.ConnectedAnomaly == auid)
+ return;
+
+ component.ConnectedAnomaly = auid;
+ //move the anomaly to the center of the synchronizer, for aesthetics.
+ var targetXform = _transform.GetWorldPosition(uid);
+ _transform.SetWorldPosition(auid, targetXform);
+
+ _anomaly.DoAnomalyPulse(component.ConnectedAnomaly.Value, anomaly);
+ _popup.PopupEntity(Loc.GetString("anomaly-sync-connected"), uid, PopupType.Medium);
+ _audio.PlayPvs(component.ConnectedSound, uid);
+ }
+
+ //TO DO: disconnection from the anomaly should also be triggered if the anomaly is far away from the synchronizer.
+ //Currently only bluespace anomaly can do this, but for some reason it is the only one that cannot be connected to the synchronizer.
+ private void DisconneсtFromAnomaly(EntityUid uid, AnomalySynchronizerComponent component, AnomalyComponent anomaly)
+ {
+ if (component.ConnectedAnomaly == null)
+ return;
+
+ _anomaly.DoAnomalyPulse(component.ConnectedAnomaly.Value, anomaly);
+ _popup.PopupEntity(Loc.GetString("anomaly-sync-disconnected"), uid, PopupType.Large);
+ _audio.PlayPvs(component.ConnectedSound, uid);
+
+ component.ConnectedAnomaly = default!;
+ }
+
+ private void OnAnomalyPulse(ref AnomalyPulseEvent args)
+ {
+ var query = EntityQueryEnumerator<AnomalySynchronizerComponent>();
+ while (query.MoveNext(out var ent, out var component))
+ {
+ if (args.Anomaly != component.ConnectedAnomaly)
+ continue;
+ if (!_power.IsPowered(ent))
+ continue;
+
+ _signal.InvokePort(ent, component.PulsePort);
+ }
+ }
+
+ private void OnAnomalySeverityChanged(ref AnomalySeverityChangedEvent args)
+ {
+ var query = EntityQueryEnumerator<AnomalySynchronizerComponent>();
+ while (query.MoveNext(out var ent, out var component))
+ {
+ if (args.Anomaly != component.ConnectedAnomaly)
+ continue;
+ if (!_power.IsPowered(ent))
+ continue;
+ //The superscritical port is invoked not at the AnomalySupercriticalEvent,
+ //but at the moment the growth animation starts. Otherwise, there is no point in this port.
+ //ATTENTION! the console command supercriticalanomaly does not work here,
+ //as it forcefully causes growth to start without increasing severity.
+ if (args.Severity >= 1)
+ _signal.InvokePort(ent, component.SupercritPort);
+ }
+ }
+ private void OnAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args)
+ {
+ var query = EntityQueryEnumerator<AnomalySynchronizerComponent>();
+ while (query.MoveNext(out var ent, out var component))
+ {
+ if (args.Anomaly != component.ConnectedAnomaly)
+ continue;
+ if (TryComp<ApcPowerReceiverComponent>(ent, out var apcPower) && !apcPower.Powered)
+ continue;
+
+ if (args.Stability < 0.25f) //I couldn't find where these values are stored, so I hardcoded them. Tell me where these variables are stored and I'll fix it
+ {
+ _signal.InvokePort(ent, component.DecayingPort);
+ }
+ else if (args.Stability > 0.5f) //I couldn't find where these values are stored, so I hardcoded them. Tell me where these variables are stored and I'll fix it
+ {
+ _signal.InvokePort(ent, component.GrowingPort);
+ }
+ else
+ {
+ _signal.InvokePort(ent, component.StabilizePort);
+ }
+ }
+ }
+}
--- /dev/null
+using Content.Shared.Anomaly;
+using Content.Shared.Anomaly.Components;
+using Content.Shared.DeviceLinking;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Anomaly.Components;
+
+/// <summary>
+/// a device that allows you to translate anomaly activity into multitool signals.
+/// </summary>
+[RegisterComponent, Access(typeof(AnomalySynchronizerSystem))]
+public sealed partial class AnomalySynchronizerComponent : Component
+{
+ /// <summary>
+ /// The uid of the anomaly to which the synchronizer is connected.
+ /// </summary>
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public EntityUid? ConnectedAnomaly;
+
+
+ [DataField]
+ public ProtoId<SourcePortPrototype> DecayingPort = "Decaying";
+
+ [DataField]
+ public ProtoId<SourcePortPrototype> StabilizePort = "Stabilize";
+
+ [DataField]
+ public ProtoId<SourcePortPrototype> GrowingPort = "Growing";
+
+ [DataField]
+ public ProtoId<SourcePortPrototype> PulsePort = "Pulse";
+
+ [DataField]
+ public ProtoId<SourcePortPrototype> SupercritPort = "Supercritical";
+
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public SoundSpecifier ConnectedSound = new SoundPathSpecifier("/Audio/Machines/anomaly_sync_connect.ogg");
+
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public SoundSpecifier DisconnectedSound = new SoundPathSpecifier("/Audio/Machines/anomaly_sync_connect.ogg");
+}
/// <summary>
/// Event raised at regular intervals on an anomaly to do whatever its effect is.
/// </summary>
+/// <param name="Anomaly">The anomaly pulsing</param>
/// <param name="Stability"></param>
/// <param name="Severity"></param>
[ByRefEvent]
-public readonly record struct AnomalyPulseEvent(float Stability, float Severity);
+public readonly record struct AnomalyPulseEvent(EntityUid Anomaly, float Stability, float Severity);
/// <summary>
/// Event raised on an anomaly when it reaches a supercritical point.
/// </summary>
[ByRefEvent]
-public readonly record struct AnomalySupercriticalEvent;
+public readonly record struct AnomalySupercriticalEvent(EntityUid Anomaly);
/// <summary>
/// Event broadcast after an anomaly goes supercritical
var pulse = EnsureComp<AnomalyPulsingComponent>(uid);
pulse.EndTime = Timing.CurTime + pulse.PulseDuration;
Appearance.SetData(uid, AnomalyVisuals.IsPulsing, true);
-
- var ev = new AnomalyPulseEvent(component.Stability, component.Severity);
- RaiseLocalEvent(uid, ref ev);
+
+ var ev = new AnomalyPulseEvent(uid, component.Stability, component.Severity);
+ RaiseLocalEvent(uid, ref ev, true);
}
/// <summary>
if (_net.IsServer)
_sawmill.Info($"Raising supercritical event. Entity: {ToPrettyString(uid)}");
- var ev = new AnomalySupercriticalEvent();
- RaiseLocalEvent(uid, ref ev);
+ var ev = new AnomalySupercriticalEvent(uid);
+ RaiseLocalEvent(uid, ref ev, true);
EndAnomaly(uid, component, true);
}
- files: ["warning_buzzer.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from TG station."
- source: "https://github.com/tgstation/tgstation/blob/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0/sound/machines/warning-buzzer.ogg"
\ No newline at end of file
+ source: "https://github.com/tgstation/tgstation/blob/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0/sound/machines/warning-buzzer.ogg"
+
+- files: ["anomaly_sync_connect.ogg"]
+ license: "CC0-1.0"
+ copyright: Created by newagesoup, convert to ogg mono by TheShuEd"
+ source: "https://freesound.org/people/newagesoup/sounds/341172/"
\ No newline at end of file
anomaly-scanner-particle-containment = - [color=goldenrod]Containment type:[/color] {$type}
anomaly-scanner-pulse-timer = Time until next pulse: [color=gray]{$time}[/color]
+anomaly-sync-connected = Anomaly successfully attached
+anomaly-sync-disconnected = The connection to the anomaly has been lost!
+
anomaly-generator-ui-title = Anomaly Generator
anomaly-generator-fuel-display = Fuel:
anomaly-generator-cooldown = Cooldown: [color=gray]{$time}[/color]
signal-port-name-air-normal = Normal
signal-port-description-air-normal = This port is invoked with HIGH when in normal mode and LOW when not.
+
+signal-port-name-decaying = Decaying
+signal-port-description-decaying = This port is invoked when a bound anomaly starts to decay.
+
+signal-port-name-stabilize = Stabilize
+signal-port-description-stabilize = This port is invoked when a bound anomaly is normalized.
+
+signal-port-name-growing = Growing
+signal-port-description-growing = This port is invoked when a bound anomaly starts to grow.
+
+signal-port-name-pulse = Pulse
+signal-port-description-pulse = This port is invoked when a bound anomaly is pulsing.
+
+signal-port-name-supercrit = Supercritical
+signal-port-description-supercrit = This port is invoked when a bound anomaly explode after supercrit state.
\ No newline at end of file
research-technology-grappling = Grappling
research-technology-abnormal-artifact-manipulation = Abnormal Artifact Manipulation
research-technology-gravity-manipulation = Gravity Manipulation
-research-technology-mobile-anomaly-tech = Mobile Anomaly Tech
+research-technology-advanced-anomaly-research = Advanced Anomaly Research
research-technology-rped = Rapid Part Exchange
research-technology-super-parts = Super Parts
name: signal-port-name-air-normal
description: signal-port-description-air-normal
defaultLinks: [ DoorBolt ]
+
+- type: sourcePort
+ id: Decaying
+ name: signal-port-name-decaying
+ description: signal-port-description-decaying
+
+- type: sourcePort
+ id: Stabilize
+ name: signal-port-name-stabilize
+ description: signal-port-description-stabilize
+
+- type: sourcePort
+ id: Growing
+ name: signal-port-name-growing
+ description: signal-port-description-growing
+
+- type: sourcePort
+ id: Pulse
+ name: signal-port-name-pulse
+ description: signal-port-description-pulse
+
+- type: sourcePort
+ id: Supercritical
+ name: signal-port-name-supercrit
+ description: signal-port-description-supercrit
\ No newline at end of file
- AnomalyRock
- AnomalyLiquid
chance: 1
+ offset: 0.15 # not to put it higher. The anomaly sychnronizer looks for anomalies within this radius, and if the radius is higher, the anomaly can be attracted from a neighboring tile.
Cable: 1
PlasmaGlass: 10
+- type: entity
+ parent: BaseMachineCircuitboard
+ id: AnomalySynchronizerCircuitboard
+ name: anomaly synchronizer machine board
+ description: A machine printed circuit board for an anomaly synchronizer.
+ components:
+ - type: Sprite
+ state: science
+ - type: MachineBoard
+ prototype: MachineAnomalySynchronizer
+ requirements:
+ Manipulator: 2
+ Capacitor: 5
+ materialRequirements:
+ PlasmaGlass: 25
+
- type: entity
parent: BaseMachineCircuitboard
id: APECircuitboard
--- /dev/null
+- type: entity
+ id: MachineAnomalySynchronizer
+ parent: [ BaseMachinePowered, ConstructibleMachine ]
+ name: anomaly synchronizer
+ description: A sophisticated device that reads changes in anomalous waves, and converts them into energy signals.
+ components:
+ - type: AnomalySynchronizer
+ - type: Machine
+ board: AnomalySynchronizerCircuitboard
+ - type: DeviceNetwork
+ deviceNetId: Wireless
+ - type: WirelessNetworkConnection
+ range: 300
+ - type: DeviceNetworkRequiresPower
+ - type: DeviceLinkSource
+ ports:
+ - Decaying
+ - Stabilize
+ - Growing
+ - Pulse
+ - Supercritical
+ - type: Sprite
+ noRot: true
+ sprite: Structures/Machines/anomaly_sync.rsi
+ layers:
+ - state: base
+ - state: energy
+ shader: unshaded
+ map: ["enum.PowerDeviceVisualLayers.Powered"]
+ - type: AmbientSound
+ enabled: false
+ sound:
+ path: /Audio/Machines/scan_loop.ogg
+ range: 5
+ volume: -8
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.35,-0.35,0.35,0.35"
+ density: 190
+ mask:
+ - MachineMask
+ layer:
+ - Impassable
+ - MidImpassable
+ - LowImpassable
+ hard: False
+ - type: Transform
+ anchored: true
+ noRot: false
+ - type: ApcPowerReceiver
+ powerLoad: 15000
+ needsPower: true
+ - type: UpgradePowerDraw
+ powerDrawMultiplier: 0.80
+ scaling: Exponential
+ - type: ItemPlacer
+ whitelist:
+ components:
+ - Anomaly
+ - type: DeviceList
+ - type: PointLight
+ radius: 1.8
+ energy: 1.6
+ color: "#b53ca1"
+ - type: LitOnPowered
+ - type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.PowerDeviceVisuals.Powered:
+ enum.PowerDeviceVisualLayers.Powered:
+ True: { visible: true }
+ False: { visible: false }
- AnalysisComputerCircuitboard
- ExosuitFabricatorMachineCircuitboard
- AnomalyVesselCircuitboard
+ - AnomalySynchronizerCircuitboard
- APECircuitboard
- ArtifactAnalyzerMachineCircuitboard
- TraversalDistorterMachineCircuitboard
Steel: 100
Glass: 900
+- type: latheRecipe
+ id: AnomalySynchronizerCircuitboard
+ result: AnomalySynchronizerCircuitboard
+ completetime: 4
+ materials:
+ Steel: 500
+ Glass: 700
+ Gold: 200
+ Silver: 100
+
- type: latheRecipe
id: APECircuitboard
result: APECircuitboard
- TraversalDistorterMachineCircuitboard
- type: technology
- id: MobileAnomalyTech
- name: research-technology-mobile-anomaly-tech
+ id: AdvancedAnomalyResearch
+ name: research-technology-advanced-anomaly-research
icon:
- sprite: Objects/Weapons/Guns/Revolvers/chimp.rsi
+ sprite: Structures/Machines/anomaly_sync.rsi
state: base
discipline: Experimental
tier: 2
cost: 10000
recipeUnlocks:
- WeaponPistolCHIMP
+ - AnomalySynchronizerCircuitboard
technologyPrerequisites:
- BasicAnomalousResearch
--- /dev/null
+{
+ "version": 1,
+ "license": "CC0-1.0",
+ "copyright": "Created by ThrShuEd (github) for Space Station 14",
+ "size": {
+ "x": 48,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "base"
+ },
+ {
+ "name": "energy",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ },
+ {
+ "name": "pulse",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ }
+ ]
+}
\ No newline at end of file