PowerSolarSystem.cs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. using System.Linq;
  2. using Content.Server.Power.Components;
  3. using Content.Server.Solar.Components;
  4. using Content.Shared.GameTicking;
  5. using Content.Shared.Physics;
  6. using JetBrains.Annotations;
  7. using Robust.Shared.Physics;
  8. using Robust.Shared.Physics.Systems;
  9. using Robust.Shared.Random;
  10. namespace Content.Server.Solar.EntitySystems
  11. {
  12. /// <summary>
  13. /// Responsible for maintaining the solar-panel sun angle and updating <see cref='SolarPanelComponent'/> coverage.
  14. /// </summary>
  15. [UsedImplicitly]
  16. internal sealed class PowerSolarSystem : EntitySystem
  17. {
  18. [Dependency] private readonly IRobustRandom _robustRandom = default!;
  19. [Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!;
  20. [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
  21. /// <summary>
  22. /// Maximum panel angular velocity range - used to stop people rotating panels fast enough that the lag prevention becomes noticable
  23. /// </summary>
  24. public const float MaxPanelVelocityDegrees = 1f;
  25. /// <summary>
  26. /// The current sun angle.
  27. /// </summary>
  28. public Angle TowardsSun = Angle.Zero;
  29. /// <summary>
  30. /// The current sun angular velocity. (This is changed in Initialize)
  31. /// </summary>
  32. public Angle SunAngularVelocity = Angle.Zero;
  33. /// <summary>
  34. /// The distance before the sun is considered to have been 'visible anyway'.
  35. /// This value, like the occlusion semantics, is borrowed from all the other SS13 stations with solars.
  36. /// </summary>
  37. public float SunOcclusionCheckDistance = 20;
  38. /// <summary>
  39. /// TODO: *Should be moved into the solar tracker when powernet allows for it.*
  40. /// The current target panel rotation.
  41. /// </summary>
  42. public Angle TargetPanelRotation = Angle.Zero;
  43. /// <summary>
  44. /// TODO: *Should be moved into the solar tracker when powernet allows for it.*
  45. /// The current target panel velocity.
  46. /// </summary>
  47. public Angle TargetPanelVelocity = Angle.Zero;
  48. /// <summary>
  49. /// TODO: *Should be moved into the solar tracker when powernet allows for it.*
  50. /// Last update of total panel power.
  51. /// </summary>
  52. public float TotalPanelPower = 0;
  53. /// <summary>
  54. /// Queue of panels to update each cycle.
  55. /// </summary>
  56. private readonly Queue<Entity<SolarPanelComponent>> _updateQueue = new();
  57. public override void Initialize()
  58. {
  59. SubscribeLocalEvent<SolarPanelComponent, MapInitEvent>(OnMapInit);
  60. SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
  61. RandomizeSun();
  62. }
  63. public void Reset(RoundRestartCleanupEvent ev)
  64. {
  65. RandomizeSun();
  66. TargetPanelRotation = Angle.Zero;
  67. TargetPanelVelocity = Angle.Zero;
  68. TotalPanelPower = 0;
  69. }
  70. private void RandomizeSun()
  71. {
  72. // Initialize the sun to something random
  73. TowardsSun = MathHelper.TwoPi * _robustRandom.NextDouble();
  74. SunAngularVelocity = Angle.FromDegrees(0.1 + ((_robustRandom.NextDouble() - 0.5) * 0.05));
  75. }
  76. private void OnMapInit(EntityUid uid, SolarPanelComponent component, MapInitEvent args)
  77. {
  78. UpdateSupply(uid, component);
  79. }
  80. public override void Update(float frameTime)
  81. {
  82. TowardsSun += SunAngularVelocity * frameTime;
  83. TowardsSun = TowardsSun.Reduced();
  84. TargetPanelRotation += TargetPanelVelocity * frameTime;
  85. TargetPanelRotation = TargetPanelRotation.Reduced();
  86. if (_updateQueue.Count > 0)
  87. {
  88. var panel = _updateQueue.Dequeue();
  89. if (panel.Comp.Running)
  90. UpdatePanelCoverage(panel);
  91. }
  92. else
  93. {
  94. TotalPanelPower = 0;
  95. var query = EntityQueryEnumerator<SolarPanelComponent, TransformComponent>();
  96. while (query.MoveNext(out var uid, out var panel, out var xform))
  97. {
  98. TotalPanelPower += panel.MaxSupply * panel.Coverage;
  99. _transformSystem.SetWorldRotation(xform, TargetPanelRotation);
  100. _updateQueue.Enqueue((uid, panel));
  101. }
  102. }
  103. }
  104. private void UpdatePanelCoverage(Entity<SolarPanelComponent> panel)
  105. {
  106. var entity = panel.Owner;
  107. var xform = EntityManager.GetComponent<TransformComponent>(entity);
  108. // So apparently, and yes, I *did* only find this out later,
  109. // this is just a really fancy way of saying "Lambert's law of cosines".
  110. // ...I still think this explaination makes more sense.
  111. // In the 'sunRelative' coordinate system:
  112. // the sun is considered to be an infinite distance directly up.
  113. // this is the rotation of the panel relative to that.
  114. // directly upwards (theta = 0) = coverage 1
  115. // left/right 90 degrees (abs(theta) = (pi / 2)) = coverage 0
  116. // directly downwards (abs(theta) = pi) = coverage -1
  117. // as TowardsSun + = CCW,
  118. // panelRelativeToSun should - = CW
  119. var panelRelativeToSun = _transformSystem.GetWorldRotation(xform) - TowardsSun;
  120. // essentially, given cos = X & sin = Y & Y is 'downwards',
  121. // then for the first 90 degrees of rotation in either direction,
  122. // this plots the lower-right quadrant of a circle.
  123. // now basically assume a line going from the negated X/Y to there,
  124. // and that's the hypothetical solar panel.
  125. //
  126. // since, again, the sun is considered to be an infinite distance upwards,
  127. // this essentially means Cos(panelRelativeToSun) is half of the cross-section,
  128. // and since the full cross-section has a max of 2, effectively-halving it is fine.
  129. //
  130. // as for when it goes negative, it only does that when (abs(theta) > pi)
  131. // and that's expected behavior.
  132. float coverage = (float)Math.Max(0, Math.Cos(panelRelativeToSun));
  133. if (coverage > 0)
  134. {
  135. // Determine if the solar panel is occluded, and zero out coverage if so.
  136. var ray = new CollisionRay(_transformSystem.GetWorldPosition(xform), TowardsSun.ToWorldVec(), (int) CollisionGroup.Opaque);
  137. var rayCastResults = _physicsSystem.IntersectRayWithPredicate(
  138. xform.MapID,
  139. ray,
  140. SunOcclusionCheckDistance,
  141. e => !xform.Anchored || e == entity);
  142. if (rayCastResults.Any())
  143. coverage = 0;
  144. }
  145. // Total coverage calculated; apply it to the panel.
  146. panel.Comp.Coverage = coverage;
  147. UpdateSupply(panel, panel);
  148. }
  149. public void UpdateSupply(
  150. EntityUid uid,
  151. SolarPanelComponent? solar = null,
  152. PowerSupplierComponent? supplier = null)
  153. {
  154. if (!Resolve(uid, ref solar, ref supplier, false))
  155. return;
  156. supplier.MaxSupply = (int) (solar.MaxSupply * solar.Coverage);
  157. }
  158. }
  159. }