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
}