using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Cargo.Systems; using Content.Server.GameTicking; using Content.Server.Power.EntitySystems; using Content.Server.Xenoarchaeology.Equipment.Components; using Content.Server.Xenoarchaeology.XenoArtifacts.Events; using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components; using Content.Shared.CCVar; using Content.Shared.Xenoarchaeology.XenoArtifacts; using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager; using Robust.Shared.Timing; namespace Content.Server.Xenoarchaeology.XenoArtifacts; public sealed partial class ArtifactSystem : EntitySystem { [Dependency] private readonly IComponentFactory _componentFactory = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(GetPrice); InitializeCommands(); InitializeActions(); } /// /// Calculates the price of an artifact based on /// how many nodes have been unlocked/triggered /// /// /// General balancing (for fully unlocked artifacts): /// Simple (1-2 Nodes): 1-2K /// Medium (5-8 Nodes): 6-7K /// Complex (7-12 Nodes): 10-11K /// private void GetPrice(EntityUid uid, ArtifactComponent component, ref PriceCalculationEvent args) { args.Price += (GetResearchPointValue(uid, component) + component.ConsumedPoints) * component.PriceMultiplier; } /// /// Calculates how many research points the artifact is worth /// /// /// 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, bool getMaxPrice = false) { if (!Resolve(uid, ref component)) return 0; var sumValue = component.NodeTree.Sum(n => GetNodePointValue(n, component, getMaxPrice)); var fullyExploredBonus = component.NodeTree.All(x => x.Triggered) || getMaxPrice ? 1.25f : 1; return (int) (sumValue * fullyExploredBonus) - component.ConsumedPoints; } /// /// Adjusts how many points on the artifact have been consumed /// public void AdjustConsumedPoints(EntityUid uid, int amount, ArtifactComponent? component = null) { if (!Resolve(uid, ref component)) return; component.ConsumedPoints += amount; } /// /// Sets whether or not the artifact is suppressed, /// preventing it from activating /// public void SetIsSuppressed(EntityUid uid, bool suppressed, ArtifactComponent? component = null) { if (!Resolve(uid, ref component)) return; component.IsSuppressed = suppressed; } /// /// Gets the point value for an individual node /// private float GetNodePointValue(ArtifactNode node, ArtifactComponent component, bool getMaxPrice = false) { var valueDeduction = 1f; if (!getMaxPrice) { if (!node.Discovered) return 0; valueDeduction = !node.Triggered ? 0.25f : 1; } var triggerProto = _prototype.Index(node.Trigger); var effectProto = _prototype.Index(node.Effect); var nodeDanger = (node.Depth + effectProto.TargetDepth + triggerProto.TargetDepth) / 3; return component.PointsPerNode * MathF.Pow(component.PointDangerMultiplier, nodeDanger) * valueDeduction; } /// /// Randomize a given artifact. /// [PublicAPI] public void RandomizeArtifact(EntityUid uid, ArtifactComponent component) { var nodeAmount = _random.Next(component.NodesMin, component.NodesMax); GenerateArtifactNodeTree(uid, component.NodeTree, nodeAmount); var firstNode = GetRootNode(component.NodeTree); EnterNode(uid, ref firstNode, component); } /// /// Tries to activate the artifact /// /// /// /// /// Set this to false if you don't know if the entity is an artifact. /// public bool TryActivateArtifact(EntityUid uid, EntityUid? user = null, ArtifactComponent? component = null, bool logMissing = true) { if (!Resolve(uid, ref component, logMissing)) return false; // check if artifact is under suppression field if (component.IsSuppressed) return false; // check if artifact isn't under cooldown var timeDif = _gameTiming.CurTime - component.LastActivationTime; if (timeDif < component.CooldownTime) return false; ForceActivateArtifact(uid, user, component); return true; } /// /// Forces an artifact to activate /// /// /// /// public void ForceActivateArtifact(EntityUid uid, EntityUid? user = null, ArtifactComponent? component = null) { if (!Resolve(uid, ref component)) return; if (component.CurrentNodeId == null) return; _audio.PlayPvs(component.ActivationSound, uid); component.LastActivationTime = _gameTiming.CurTime; var ev = new ArtifactActivatedEvent { Activator = user }; RaiseLocalEvent(uid, ev, true); var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component); currentNode.Triggered = true; if (currentNode.Edges.Count == 0) return; var newNode = GetNewNode(uid, component); if (newNode == null) return; EnterNode(uid, ref newNode, component); } private ArtifactNode? GetNewNode(EntityUid uid, ArtifactComponent component) { if (component.CurrentNodeId == null) return null; var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component); var allNodes = currentNode.Edges; Log.Debug($"our node: {currentNode.Id}"); Log.Debug($"other nodes: {string.Join(", ", allNodes)}"); if (TryComp(uid, out var bias) && TryComp(bias.Provider, out var trav) && this.IsPowered(bias.Provider, EntityManager)) { switch (trav.BiasDirection) { case BiasDirection.Up: var upNodes = allNodes.Where(x => GetNodeFromId(x, component).Depth < currentNode.Depth).ToHashSet(); if (upNodes.Count != 0) allNodes = upNodes; break; case BiasDirection.Down: var downNodes = allNodes.Where(x => GetNodeFromId(x, component).Depth > currentNode.Depth).ToHashSet(); if (downNodes.Count != 0) allNodes = downNodes; break; } } var undiscoveredNodes = allNodes.Where(x => !GetNodeFromId(x, component).Discovered).ToList(); Log.Debug($"Undiscovered nodes: {string.Join(", ", undiscoveredNodes)}"); var newNode = _random.Pick(allNodes); if (undiscoveredNodes.Count != 0 && _random.Prob(0.75f)) { newNode = _random.Pick(undiscoveredNodes); } Log.Debug($"Going to node {newNode}"); return GetNodeFromId(newNode, component); } /// /// Try and get a data object from a node /// /// The entity you're getting the data from /// The data's key /// The data you are trying to get. /// /// /// public bool TryGetNodeData(EntityUid uid, string key, [NotNullWhen(true)] out T? data, ArtifactComponent? component = null) { data = default; if (!Resolve(uid, ref component)) return false; if (component.CurrentNodeId == null) return false; var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component); if (currentNode.NodeData.TryGetValue(key, out var dat) && dat is T value) { data = value; return true; } return false; } /// /// Sets the node data to a certain value /// /// The artifact /// The key being set /// The value it's being set to /// public void SetNodeData(EntityUid uid, string key, object value, ArtifactComponent? component = null) { if (!Resolve(uid, ref component)) return; if (component.CurrentNodeId == null) return; var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component); currentNode.NodeData[key] = value; } /// /// Gets the base node (depth 0) of an artifact's node graph /// /// /// public ArtifactNode GetRootNode(List allNodes) { return allNodes.First(n => n.Depth == 0); } }