using Content.Server.Physics.Components; using Content.Server.Singularity.Components; using Content.Server.Singularity.Events; using Content.Shared.Singularity.Components; using Content.Shared.Singularity.EntitySystems; using Content.Shared.Singularity.Events; using Robust.Server.GameStates; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.GameStates; using Robust.Shared.Player; using Robust.Shared.Timing; namespace Content.Server.Singularity.EntitySystems; /// /// The server-side version of . /// Primarily responsible for managing s. /// Handles their accumulation of energy upon consuming entities (see ) and gradual dissipation. /// Also handles synchronizing server-side components with the singuarities level. /// public sealed class SingularitySystem : SharedSingularitySystem { #region Dependencies [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly PvsOverrideSystem _pvs = default!; #endregion Dependencies /// /// The amount of energy singulos accumulate when they eat a tile. /// public const float BaseTileEnergy = 1f; /// /// The amount of energy singulos accumulate when they eat an entity. /// public const float BaseEntityEnergy = 1f; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnDistortionStartup); SubscribeLocalEvent(OnSingularityShutdown); SubscribeLocalEvent(OnConsumed); SubscribeLocalEvent(OnConsumed); SubscribeLocalEvent(OnConsumedEntity); SubscribeLocalEvent(OnConsumedTiles); SubscribeLocalEvent(UpdateEnergyDrain); SubscribeLocalEvent(HandleSingularityState); // TODO: Figure out where all this coupling should be handled. SubscribeLocalEvent(UpdateRandomWalk); SubscribeLocalEvent(UpdateGravityWell); var vvHandle = Vvm.GetTypeHandler(); vvHandle.AddPath(nameof(SingularityComponent.Energy), (_, comp) => comp.Energy, SetEnergy); } public override void Shutdown() { var vvHandle = Vvm.GetTypeHandler(); vvHandle.RemovePath(nameof(SingularityComponent.Energy)); base.Shutdown(); } /// /// Handles the gradual dissipation of all singularities. /// /// The amount of time since the last set of updates. public override void Update(float frameTime) { if(!_timing.IsFirstTimePredicted) return; var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var singularity)) { AdjustEnergy(uid, -singularity.EnergyDrain * frameTime, singularity: singularity); } } #region Getters/Setters /// /// Setter for . /// Also updates the level of the singularity accordingly. /// /// The uid of the singularity to set the energy of. /// The amount of energy for the singularity to have. /// The state of the singularity to set the energy of. public void SetEnergy(EntityUid uid, float value, SingularityComponent? singularity = null) { if(!Resolve(uid, ref singularity)) return; var oldValue = singularity.Energy; if (oldValue == value) return; singularity.Energy = value; SetLevel(uid, value switch { // Normally, a level 6 singularity requires the supermatter + 3000 energy. // The required amount of energy has been bumped up to compensate for the lack of the supermatter. >= 5000 => 6, >= 2000 => 5, >= 1000 => 4, >= 500 => 3, >= 200 => 2, > 0 => 1, _ => 0 }, singularity); } /// /// Adjusts the amount of energy the singularity has accumulated. /// /// The uid of the singularity to adjust the energy of. /// The amount to adjust the energy of the singuarity. /// The minimum amount of energy for the singularity to be adjusted to. /// The maximum amount of energy for the singularity to be adjusted to. /// Whether the amount of energy in the singularity should be forced to within the specified range if it already is below it. /// Whether the amount of energy in the singularity should be forced to within the specified range if it already is above it. /// The state of the singularity to adjust the energy of. public void AdjustEnergy(EntityUid uid, float delta, float min = float.MinValue, float max = float.MaxValue, bool snapMin = true, bool snapMax = true, SingularityComponent? singularity = null) { if(!Resolve(uid, ref singularity)) return; var newValue = singularity.Energy + delta; if((!snapMin && newValue < min) || (!snapMax && newValue > max)) return; SetEnergy(uid, MathHelper.Clamp(newValue, min, max), singularity); } #endregion Getters/Setters #region Event Handlers /// /// Handles playing the startup sounds when a singulo forms. /// Always sets up the ambient singularity rumble. /// The formation sound only plays if the singularity is being created. /// /// The entity UID of the singularity that is forming. /// The component of the singularity that is forming. /// The event arguments. protected override void OnSingularityStartup(EntityUid uid, SingularityComponent comp, ComponentStartup args) { MetaDataComponent? metaData = null; if (Resolve(uid, ref metaData) && metaData.EntityLifeStage <= EntityLifeStage.Initializing) _audio.PlayPvs(comp.FormationSound, uid); comp.AmbientSoundStream = _audio.PlayPvs(comp.AmbientSound, uid)?.Entity; UpdateSingularityLevel(uid, comp); } /// /// Makes entities that have the singularity distortion visual warping always get their state shared with the client. /// This prevents some major popin with large distortion ranges. /// /// The entity UID of the entity that is gaining the shader. /// The component of the shader that the entity is gaining. /// The event arguments. public void OnDistortionStartup(EntityUid uid, SingularityDistortionComponent comp, ComponentStartup args) { _pvs.AddGlobalOverride(uid); } /// /// Handles playing the shutdown sounds when a singulo dissipates. /// Always stops the ambient singularity rumble. /// The dissipations sound only plays if the singularity is being destroyed. /// /// The entity UID of the singularity that is dissipating. /// The component of the singularity that is dissipating. /// The event arguments. public void OnSingularityShutdown(EntityUid uid, SingularityComponent comp, ComponentShutdown args) { comp.AmbientSoundStream = _audio.Stop(comp.AmbientSoundStream); MetaDataComponent? metaData = null; if (Resolve(uid, ref metaData) && metaData.EntityLifeStage >= EntityLifeStage.Terminating) { var xform = Transform(uid); var coordinates = xform.Coordinates; // I feel like IsValid should be checking this or something idk. if (!TerminatingOrDeleted(coordinates.EntityId)) _audio.PlayPvs(comp.DissipationSound, coordinates); } } /// /// Handles wrapping the state of a singularity for server-client syncing. /// /// The uid of the singularity that is being synced. /// The state of the singularity that is being synced. /// The event arguments. private void HandleSingularityState(EntityUid uid, SingularityComponent comp, ref ComponentGetState args) { args.State = new SingularityComponentState(comp); } /// /// Adds the energy of any entities that are consumed to the singularity that consumed them. /// /// The entity UID of the singularity that is consuming the entity. /// The component of the singularity that is consuming the entity. /// The event arguments. public void OnConsumedEntity(EntityUid uid, SingularityComponent comp, ref EntityConsumedByEventHorizonEvent args) { // Don't double count singulo food if (HasComp(args.Entity)) return; AdjustEnergy(uid, BaseEntityEnergy, singularity: comp); } /// /// Adds the energy of any tiles that are consumed to the singularity that consumed them. /// /// The entity UID of the singularity that is consuming the tiles. /// The component of the singularity that is consuming the tiles. /// The event arguments. public void OnConsumedTiles(EntityUid uid, SingularityComponent comp, ref TilesConsumedByEventHorizonEvent args) { AdjustEnergy(uid, args.Tiles.Count * BaseTileEnergy, singularity: comp); } /// /// Adds the energy of this singularity to singularities that consume it. /// /// The entity UID of the singularity that is being consumed. /// The component of the singularity that is being consumed. /// The event arguments. private void OnConsumed(EntityUid uid, SingularityComponent comp, ref EventHorizonConsumedEntityEvent args) { // Should be slightly more efficient than checking literally everything we consume for a singularity component and doing the reverse. if (EntityManager.TryGetComponent(args.EventHorizonUid, out var singulo)) { AdjustEnergy(args.EventHorizonUid, comp.Energy, singularity: singulo); SetEnergy(uid, 0.0f, comp); } } /// /// Adds some bonus energy from any singularity food to the singularity that consumes it. /// /// The entity UID of the singularity food that is being consumed. /// The component of the singularity food that is being consumed. /// The event arguments. public void OnConsumed(EntityUid uid, SinguloFoodComponent comp, ref EventHorizonConsumedEntityEvent args) { if (EntityManager.TryGetComponent(args.EventHorizonUid, out var singulo)) AdjustEnergy(args.EventHorizonUid, comp.Energy, singularity: singulo); } /// /// Updates the rate at which the singularities energy drains at when its level changes. /// /// The entity UID of the singularity that changed in level. /// The component of the singularity that changed in level. /// The event arguments. public void UpdateEnergyDrain(EntityUid uid, SingularityComponent comp, SingularityLevelChangedEvent args) { comp.EnergyDrain = args.NewValue switch { 6 => 0, 5 => 0, 4 => 20, 3 => 10, 2 => 5, 1 => 1, _ => 0 }; } /// /// Updates the possible speeds of the singulos random walk when the singularities level changes. /// /// The entity UID of the singularity. /// The random walk component component sharing the entity with the singulo component. /// The event arguments. private void UpdateRandomWalk(EntityUid uid, RandomWalkComponent comp, SingularityLevelChangedEvent args) { var scale = MathF.Max(args.NewValue, 4); comp.MinSpeed = 7.5f / scale; comp.MaxSpeed = 10f / scale; } /// /// Updates the size and strength of the singularities gravity well when the singularities level changes. /// /// The entity UID of the singularity. /// The gravity well component sharing the entity with the singulo component. /// The event arguments. private void UpdateGravityWell(EntityUid uid, GravityWellComponent comp, SingularityLevelChangedEvent args) { var singulos = args.Singularity; comp.MaxRange = GravPulseRange(singulos); (comp.BaseRadialAcceleration, comp.BaseTangentialAcceleration) = GravPulseAcceleration(singulos); } #endregion Event Handlers }