diff --git a/Content.Server/Anomaly/AnomalySystem.Generator.cs b/Content.Server/Anomaly/AnomalySystem.Generator.cs index 0af2dfe04d..2baacf6b52 100644 --- a/Content.Server/Anomaly/AnomalySystem.Generator.cs +++ b/Content.Server/Anomaly/AnomalySystem.Generator.cs @@ -74,7 +74,7 @@ public sealed partial class AnomalySystem UpdateGeneratorUi(uid, component); } - private void SpawnOnRandomGridLocation(EntityUid grid, string toSpawn) + public void SpawnOnRandomGridLocation(EntityUid grid, string toSpawn) { if (!TryComp(grid, out var gridComp)) return; diff --git a/Content.Server/Anomaly/AnomalySystem.Scanner.cs b/Content.Server/Anomaly/AnomalySystem.Scanner.cs index 8c70810682..cdb18d41b7 100644 --- a/Content.Server/Anomaly/AnomalySystem.Scanner.cs +++ b/Content.Server/Anomaly/AnomalySystem.Scanner.cs @@ -95,7 +95,7 @@ public sealed partial class AnomalySystem component.TokenSource = null; Audio.PlayPvs(component.CompleteSound, uid); - _popup.PopupEntity(Loc.GetString("anomaly-scanner-component-scan-complete"), uid); + Popup.PopupEntity(Loc.GetString("anomaly-scanner-component-scan-complete"), uid); UpdateScannerWithNewAnomaly(uid, args.Anomaly, component); if (TryComp(args.User, out var actor)) diff --git a/Content.Server/Anomaly/AnomalySystem.Vessel.cs b/Content.Server/Anomaly/AnomalySystem.Vessel.cs index 8b334ec923..ce71cf4715 100644 --- a/Content.Server/Anomaly/AnomalySystem.Vessel.cs +++ b/Content.Server/Anomaly/AnomalySystem.Vessel.cs @@ -21,6 +21,7 @@ public sealed partial class AnomalySystem SubscribeLocalEvent(OnVesselShutdown); SubscribeLocalEvent(OnVesselMapInit); SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnVesselInteractUsing); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnVesselGetPointsPerSecond); @@ -59,6 +60,11 @@ public sealed partial class AnomalySystem component.PointMultiplier = MathF.Pow(component.PartRatingPointModifier, modifierRating); } + private void OnUpgradeExamine(EntityUid uid, AnomalyVesselComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("anomaly-vessel-component-upgrade-output", component.PointMultiplier); + } + private void OnVesselInteractUsing(EntityUid uid, AnomalyVesselComponent component, InteractUsingEvent args) { if (component.Anomaly != null || @@ -74,7 +80,7 @@ public sealed partial class AnomalySystem component.Anomaly = scanner.ScannedAnomaly; anomalyComponent.ConnectedVessel = uid; UpdateVesselAppearance(uid, component); - _popup.PopupEntity(Loc.GetString("anomaly-vessel-component-anomaly-assigned"), uid); + Popup.PopupEntity(Loc.GetString("anomaly-vessel-component-anomaly-assigned"), uid); } private void OnVesselGetPointsPerSecond(EntityUid uid, AnomalyVesselComponent component, ref ResearchServerGetPointsPerSecondEvent args) diff --git a/Content.Server/Anomaly/AnomalySystem.cs b/Content.Server/Anomaly/AnomalySystem.cs index 530545b291..f85ddbe990 100644 --- a/Content.Server/Anomaly/AnomalySystem.cs +++ b/Content.Server/Anomaly/AnomalySystem.cs @@ -4,7 +4,6 @@ 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; @@ -24,7 +23,6 @@ public sealed partial class AnomalySystem : SharedAnomalySystem [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!; @@ -101,9 +99,9 @@ public sealed partial class AnomalySystem : SharedAnomalySystem var multiplier = 1f; if (component.Stability > component.GrowthThreshold) - multiplier = 1.25f; //more points for unstable + multiplier = component.GrowingPointMultiplier; //more points for unstable else if (component.Stability < component.DecayThreshold) - multiplier = 0.75f; //less points if it's dying + multiplier = component.DecayingPointMultiplier; //less points if it's dying //penalty of up to 50% based on health multiplier *= MathF.Pow(1.5f, component.Health) - 0.5f; diff --git a/Content.Server/StationEvents/Events/AnomalySpawn.cs b/Content.Server/StationEvents/Events/AnomalySpawn.cs new file mode 100644 index 0000000000..5b7d1c42f5 --- /dev/null +++ b/Content.Server/StationEvents/Events/AnomalySpawn.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Content.Server.Anomaly; +using Content.Server.Station.Components; +using Robust.Shared.Random; + +namespace Content.Server.StationEvents.Events; + +public sealed class AnomalySpawn : StationEventSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly AnomalySystem _anomaly = default!; + + public override string Prototype => "AnomalySpawn"; + + public readonly string AnomalySpawnerPrototype = "RandomAnomalySpawner"; + + public override void Added() + { + base.Added(); + + var str = Loc.GetString("anomaly-spawn-event-announcement", + ("sighting", Loc.GetString($"anomaly-spawn-sighting-{_random.Next(1, 6)}"))); + ChatSystem.DispatchGlobalAnnouncement(str, colorOverride: Color.FromHex("#18abf5")); + } + + public override void Started() + { + base.Started(); + + if (StationSystem.Stations.Count == 0) + return; // No stations + var chosenStation = RobustRandom.Pick(StationSystem.Stations.ToList()); + if (!TryComp(chosenStation, out var stationData)) + return; + + EntityUid? grid = null; + foreach (var g in stationData.Grids.Where(HasComp)) + { + grid = g; + } + + if (grid is not { }) + return; + + var amountToSpawn = Math.Max(1, (int) MathF.Round(GetSeverityModifier() / 2)); + for (var i = 0; i < amountToSpawn; i++) + { + _anomaly.SpawnOnRandomGridLocation(grid.Value, AnomalySpawnerPrototype); + } + } +} diff --git a/Content.Server/StationEvents/Events/BluespaceArtifact.cs b/Content.Server/StationEvents/Events/BluespaceArtifact.cs index fe203a9d53..407cca0f47 100644 --- a/Content.Server/StationEvents/Events/BluespaceArtifact.cs +++ b/Content.Server/StationEvents/Events/BluespaceArtifact.cs @@ -34,13 +34,16 @@ public sealed class BluespaceArtifact : StationEventSystem public override void Started() { base.Started(); + var amountToSpawn = Math.Max(1, (int) MathF.Round(GetSeverityModifier() / 1.5f)); + for (var i = 0; i < amountToSpawn; i++) + { + if (!TryFindRandomTile(out _, out _, out _, out var coords)) + return; - if (!TryFindRandomTile(out _, out _, out _, out var coords)) - return; + EntityManager.SpawnEntity(ArtifactSpawnerPrototype, coords); + EntityManager.SpawnEntity(ArtifactFlashPrototype, coords); - EntityManager.SpawnEntity(ArtifactSpawnerPrototype, coords); - EntityManager.SpawnEntity(ArtifactFlashPrototype, coords); - - Sawmill.Info($"Spawning random artifact at {coords}"); + Sawmill.Info($"Spawning random artifact at {coords}"); + } } } diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs index 46b9e067de..7a10d85532 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs @@ -51,6 +51,25 @@ public sealed class ArtifactComponent : Component /// [DataField("lastActivationTime", customTypeSerializer: typeof(TimespanSerializer))] public TimeSpan LastActivationTime; + + /// + /// The base price of each node for an artifact + /// + [DataField("pricePerNode")] + public int PricePerNode = 500; + + /// + /// The base amount of research points for each artifact node. + /// + [DataField("pointsPerNode")] + public int PointsPerNode = 5000; + + /// + /// A multiplier that is raised to the power of the average depth of a node. + /// Used for calculating the research point value of an artifact node. + /// + [DataField("pointDangerMultiplier")] + public float PointDangerMultiplier = 1.35f; } /// diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.cs index 1fd23c10ff..a8fb10f3a9 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.cs @@ -17,9 +17,6 @@ public sealed partial class ArtifactSystem : EntitySystem [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IRobustRandom _random = default!; - private const int PricePerNode = 500; - private const int PointsPerNode = 5000; - public override void Initialize() { base.Initialize(); @@ -34,7 +31,7 @@ public sealed partial class ArtifactSystem : EntitySystem private void OnInit(EntityUid uid, ArtifactComponent component, MapInitEvent args) { - RandomizeArtifact(component); + RandomizeArtifact(uid, component); } /// @@ -52,7 +49,7 @@ public sealed partial class ArtifactSystem : EntitySystem if (component.NodeTree == null) return; - var price = component.NodeTree.AllNodes.Sum(GetNodePrice); + var price = component.NodeTree.AllNodes.Sum(x => GetNodePrice(x, component)); // 25% bonus for fully exploring every node. var fullyExploredBonus = component.NodeTree.AllNodes.Any(x => !x.Triggered) ? 1 : 1.25f; @@ -60,7 +57,7 @@ public sealed partial class ArtifactSystem : EntitySystem args.Price =+ price * fullyExploredBonus; } - private float GetNodePrice(ArtifactNode node) + private float GetNodePrice(ArtifactNode node, ArtifactComponent component) { if (!node.Discovered) //no money for undiscovered nodes. return 0; @@ -70,7 +67,7 @@ public sealed partial class ArtifactSystem : EntitySystem //the danger is the average of node depth, effect danger, and trigger danger. var nodeDanger = (node.Depth + node.Effect.TargetDepth + node.Trigger.TargetDepth) / 3; - var price = MathF.Pow(2f, nodeDanger) * PricePerNode * priceMultiplier; + var price = MathF.Pow(2f, nodeDanger) * component.PricePerNode * priceMultiplier; return price; } @@ -78,43 +75,52 @@ public sealed partial class ArtifactSystem : EntitySystem /// Calculates how many research points the artifact is worht /// /// - /// Rebalance this shit at some point. Definitely OP. + /// General balancing (for fully unlocked artifacts): + /// Simple (1-2 Nodes): ~10K + /// Medium (5-8 Nodes): ~30-40K + /// Complex (7-12 Nodes): ~60-80K + /// + /// Simple artifacts should be enough to unlock a few techs. + /// Medium should get you partway through a tree. + /// Complex should get you through a full tree and then some. /// public int GetResearchPointValue(EntityUid uid, ArtifactComponent? component = null) { if (!Resolve(uid, ref component) || component.NodeTree == null) return 0; - var sumValue = component.NodeTree.AllNodes.Sum(GetNodePointValue); + var sumValue = component.NodeTree.AllNodes.Sum(n => GetNodePointValue(n, component)); var fullyExploredBonus = component.NodeTree.AllNodes.Any(x => !x.Triggered) ? 1 : 1.25f; var pointValue = (int) (sumValue * fullyExploredBonus); return pointValue; } - private float GetNodePointValue(ArtifactNode node) + /// + /// Gets the point value for an individual node + /// + private float GetNodePointValue(ArtifactNode node, ArtifactComponent component) { if (!node.Discovered) return 0; - var valueDeduction = !node.Triggered ? 0.5f : 1; + var valueDeduction = !node.Triggered ? 0.25f : 1; var nodeDanger = (node.Depth + node.Effect.TargetDepth + node.Trigger.TargetDepth) / 3; - - return (nodeDanger+1) * PointsPerNode * valueDeduction; + return component.PointsPerNode * MathF.Pow(component.PointDangerMultiplier, nodeDanger) * valueDeduction; } /// /// Randomize a given artifact. /// [PublicAPI] - public void RandomizeArtifact(ArtifactComponent component) + public void RandomizeArtifact(EntityUid uid, ArtifactComponent component) { var nodeAmount = _random.Next(component.NodesMin, component.NodesMax); component.NodeTree = new ArtifactTree(); - GenerateArtifactNodeTree(component.Owner, ref component.NodeTree, nodeAmount); - EnterNode(component.Owner, ref component.NodeTree.StartNode, component); + GenerateArtifactNodeTree(uid, ref component.NodeTree, nodeAmount); + EnterNode(uid, ref component.NodeTree.StartNode, component); } /// @@ -166,21 +172,21 @@ public sealed partial class ArtifactSystem : EntitySystem component.CurrentNode.Triggered = true; if (component.CurrentNode.Edges.Any()) { - var newNode = GetNewNode(component); + var newNode = GetNewNode(uid, component); if (newNode == null) return; EnterNode(uid, ref newNode, component); } } - private ArtifactNode? GetNewNode(ArtifactComponent component) + private ArtifactNode? GetNewNode(EntityUid uid, ArtifactComponent component) { if (component.CurrentNode == null) return null; var allNodes = component.CurrentNode.Edges; - if (TryComp(component.Owner, out var bias) && + if (TryComp(uid, out var bias) && TryComp(bias.Provider, out var trav) && _random.Prob(trav.BiasChance) && this.IsPowered(bias.Provider, EntityManager)) diff --git a/Content.Shared/Anomaly/Components/AnomalyComponent.cs b/Content.Shared/Anomaly/Components/AnomalyComponent.cs index 13dbf6f6a9..cdee844b1f 100644 --- a/Content.Shared/Anomaly/Components/AnomalyComponent.cs +++ b/Content.Shared/Anomaly/Components/AnomalyComponent.cs @@ -1,4 +1,5 @@ -using Robust.Shared.Audio; +using Content.Shared.Damage; +using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -104,6 +105,13 @@ public sealed class AnomalyComponent : Component [DataField("pulseVariation")] public float PulseVariation = .1f; + /// + /// The largest value by which the anomaly will vary in stability for each pulse. + /// In simple terms, every pulse, stability changes from a range of -this_value to this_value + /// + [DataField("pulseStabilityVariation")] + public float PulseStabilityVariation = 0.05f; + /// /// The sound played when an anomaly pulses /// @@ -200,8 +208,36 @@ public sealed class AnomalyComponent : Component /// This doesn't include the point bonus for being unstable. /// [DataField("maxPointsPerSecond")] - public int MaxPointsPerSecond = 100; + public int MaxPointsPerSecond = 65; + + /// + /// The multiplier applied to the point value for the + /// anomaly being above the + /// + [DataField("growingPointMultiplier")] + public float GrowingPointMultiplier = 1.2f; + + /// + /// The multiplier applied to the point value for the + /// anomaly being below the + /// + [DataField("decayingPointMultiplier")] + public float DecayingPointMultiplier = 0.75f; #endregion + + /// + /// The amount of damage dealt when either a player touches the anomaly + /// directly or by hitting the anomaly. + /// + [DataField("anomalyContactDamage", required: true)] + public DamageSpecifier AnomalyContactDamage = default!; + + /// + /// The sound effect played when a player + /// burns themselves on an anomaly via contact. + /// + [DataField("anomalyContactDamageSound")] + public SoundSpecifier AnomalyContactDamageSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg"); } [Serializable, NetSerializable] diff --git a/Content.Shared/Anomaly/SharedAnomalySystem.cs b/Content.Shared/Anomaly/SharedAnomalySystem.cs index 3fe4b63f16..6106112a1d 100644 --- a/Content.Shared/Anomaly/SharedAnomalySystem.cs +++ b/Content.Shared/Anomaly/SharedAnomalySystem.cs @@ -1,6 +1,10 @@ using Content.Shared.Administration.Logs; using Content.Shared.Anomaly.Components; +using Content.Shared.Damage; using Content.Shared.Database; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Weapons.Melee.Events; using Robust.Shared.GameStates; using Robust.Shared.Network; using Robust.Shared.Random; @@ -15,8 +19,10 @@ public abstract class SharedAnomalySystem : EntitySystem [Dependency] private readonly INetManager _net = default!; [Dependency] protected readonly IRobustRandom Random = default!; [Dependency] protected readonly ISharedAdminLogManager Log = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!; [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; + [Dependency] protected readonly SharedPopupSystem Popup = default!; public override void Initialize() { @@ -26,6 +32,8 @@ public abstract class SharedAnomalySystem : EntitySystem SubscribeLocalEvent(OnAnomalyHandleState); SubscribeLocalEvent(OnSupercriticalGetState); SubscribeLocalEvent(OnSupercriticalHandleState); + SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent(OnAttacked); SubscribeLocalEvent(OnAnomalyUnpause); SubscribeLocalEvent(OnPulsingUnpause); @@ -69,6 +77,26 @@ public abstract class SharedAnomalySystem : EntitySystem component.SupercriticalDuration = state.Duration; } + private void OnInteractHand(EntityUid uid, AnomalyComponent component, InteractHandEvent args) + { + DoAnomalyBurnDamage(uid, args.User, component); + args.Handled = true; + } + + private void OnAttacked(EntityUid uid, AnomalyComponent component, AttackedEvent args) + { + DoAnomalyBurnDamage(uid, args.User, component); + } + + public void DoAnomalyBurnDamage(EntityUid source, EntityUid target, AnomalyComponent component) + { + _damageable.TryChangeDamage(target, component.AnomalyContactDamage, true); + if (!Timing.IsFirstTimePredicted || _net.IsServer) + return; + Audio.PlayPvs(component.AnomalyContactDamageSound, source); + Popup.PopupEntity(Loc.GetString("anomaly-component-contact-damage"), target, target); + } + private void OnAnomalyUnpause(EntityUid uid, AnomalyComponent component, ref EntityUnpausedEvent args) { component.NextPulseTime += args.PausedTime; @@ -100,12 +128,9 @@ public abstract class SharedAnomalySystem : EntitySystem { ChangeAnomalySeverity(uid, GetSeverityIncreaseFromGrowth(component), component); } - else - { - // just doing this to update the scanner ui - // as they hook into these events - ChangeAnomalySeverity(uid, 0); - } + + var stability = Random.NextFloat(-component.PulseStabilityVariation, component.PulseStabilityVariation); + ChangeAnomalyStability(uid, stability, component); Log.Add(LogType.Anomaly, LogImpact.Medium, $"Anomaly {ToPrettyString(uid)} pulsed with severity {component.Severity}."); if (_net.IsServer) diff --git a/Resources/Locale/en-US/anomaly/anomaly.ftl b/Resources/Locale/en-US/anomaly/anomaly.ftl index 725742ece6..ae98207115 100644 --- a/Resources/Locale/en-US/anomaly/anomaly.ftl +++ b/Resources/Locale/en-US/anomaly/anomaly.ftl @@ -1,6 +1,9 @@ +anomaly-component-contact-damage = The anomaly sears off your skin! + anomaly-vessel-component-anomaly-assigned = Anomaly assigned to vessel. anomaly-vessel-component-not-assigned = This vessel is not assigned to any anomaly. Try using a scanner on it. anomaly-vessel-component-assigned = This vessel is currently assigned to an anomaly. +anomaly-vessel-component-upgrade-output = point output anomaly-particles-delta = Delta particles anomaly-particles-epsilon = Epsilon particles diff --git a/Resources/Locale/en-US/station-events/events/anomaly-spawn.ftl b/Resources/Locale/en-US/station-events/events/anomaly-spawn.ftl new file mode 100644 index 0000000000..0e4b5f6e39 --- /dev/null +++ b/Resources/Locale/en-US/station-events/events/anomaly-spawn.ftl @@ -0,0 +1,7 @@ +anomaly-spawn-event-announcement = Our readings have detected a dangerous interspacial anomaly. Please inform the research team of { $sighting }. + +anomaly-spawn-sighting-1 = low pulsating sounds heard throughout the station +anomaly-spawn-sighting-2 = strange sources of light +anomaly-spawn-sighting-3 = inexplicable shapes +anomaly-spawn-sighting-4 = forms causing severe mental distress +anomaly-spawn-sighting-5 = strange effects on the local environment \ No newline at end of file diff --git a/Resources/Locale/en-US/station-events/events/bluespace-artifact.ftl b/Resources/Locale/en-US/station-events/events/bluespace-artifact.ftl index a7c948ef3d..a2307d77b5 100644 --- a/Resources/Locale/en-US/station-events/events/bluespace-artifact.ftl +++ b/Resources/Locale/en-US/station-events/events/bluespace-artifact.ftl @@ -1,4 +1,4 @@ -bluespace-artifact-event-announcement = Our readings have detected an incoming anomalous object. Please inform the research team of { $sighting }. +bluespace-artifact-event-announcement = Our readings have detected an incoming object of alien origin. Please inform the research team of { $sighting }. bluespace-artifact-sighting-1 = bright flashes of light bluespace-artifact-sighting-2 = strange sounds coming from maintenance tunnels diff --git a/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml b/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml index 9a69002e1b..5d57d343a7 100644 --- a/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml +++ b/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @@ -9,6 +9,9 @@ collection: RadiationPulse params: volume: 5 + anomalyContactDamage: + types: + Radiation: 10 - type: AmbientSound range: 5 volume: -5 @@ -35,8 +38,6 @@ - type: InteractionOutline - type: Clickable - type: Damageable - damageContainer: Inorganic - damageModifierSet: Metallic - type: Appearance - type: GuideHelp guides: diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index dc71e8bf4a..dae169bdd3 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -1,4 +1,13 @@ - type: gameRule + id: AnomalySpawn + config: + !type:StationEventRuleConfiguration + id: AnomalySpawn + weight: 10 + startAfter: 30 + endAfter: 35 + +- type: gameRule id: BluespaceArtifact config: !type:StationEventRuleConfiguration