Makes the singularity engine actually work stably. (#4068)

* Add GNU Octave script for tuning singularity engine. startsingularityengine is now properly tuned & sets up radiation collectors. FTLize RadiationCollectorComponent.

* Fix bugs with radiation collectors producing infinite power.

* Ensure singularities don't instantly annihilate other singularities (causing new singularities to instantly dissolve)

Technically found by a "bug" where a singularity generator would make multiple singularities, but this renders that bug harmless.

* Tune singularity shield emitters to hopefully randomly fail less, and add an Octave script for looking into that

* Fix singularity shader

* Map in an unfinished PA into Saltern

* Correct PA particles being counted twice by singularity calculations, add singulo food component

* Hopefully stop "level 1 singulo stuck in a corner" issues by freezing it when it goes to level 1 from any other level

* Apply suggestions on 'jazz' PR
This commit is contained in:
20kdc
2021-05-28 10:44:13 +01:00
committed by GitHub
parent 3ba0c01e4f
commit a3d9562532
20 changed files with 424 additions and 153 deletions

View File

@@ -1,5 +1,6 @@
using Content.Shared.GameObjects.Components.Singularity; using Content.Shared.GameObjects.Components.Singularity;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components.Singularity namespace Content.Client.GameObjects.Components.Singularity
{ {
@@ -7,18 +8,60 @@ namespace Content.Client.GameObjects.Components.Singularity
[ComponentReference(typeof(IClientSingularityInstance))] [ComponentReference(typeof(IClientSingularityInstance))]
class ClientSingularityComponent : SharedSingularityComponent, IClientSingularityInstance class ClientSingularityComponent : SharedSingularityComponent, IClientSingularityInstance
{ {
public int Level [ViewVariables]
public int Level { get; set; }
//I am lazy
[ViewVariables]
public float Intensity
{ {
get get
{ {
return _level; switch (Level)
} {
set case 0:
{ return 0.0f;
_level = value; case 1:
return 2.7f;
case 2:
return 14.4f;
case 3:
return 47.2f;
case 4:
return 180.0f;
case 5:
return 600.0f;
case 6:
return 800.0f;
}
return -1.0f;
}
}
[ViewVariables]
public float Falloff
{
get
{
switch (Level)
{
case 0:
return 9999f;
case 1:
return 6.4f;
case 2:
return 7.0f;
case 3:
return 8.0f;
case 4:
return 10.0f;
case 5:
return 12.0f;
case 6:
return 12.0f;
}
return -1.0f;
} }
} }
private int _level;
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{ {
@@ -26,7 +69,7 @@ namespace Content.Client.GameObjects.Components.Singularity
{ {
return; return;
} }
_level = state.Level; Level = state.Level;
} }
} }
} }

View File

@@ -6,6 +6,7 @@ namespace Content.Client.GameObjects.Components.Singularity
{ {
interface IClientSingularityInstance interface IClientSingularityInstance
{ {
public int Level { get; set; } public float Intensity { get; }
public float Falloff { get; }
} }
} }

View File

@@ -1,4 +1,5 @@
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components.Singularity namespace Content.Client.GameObjects.Components.Singularity
{ {
@@ -8,12 +9,9 @@ namespace Content.Client.GameObjects.Components.Singularity
public class ToySingularityComponent : Component, IClientSingularityInstance public class ToySingularityComponent : Component, IClientSingularityInstance
{ {
public override string Name => "ToySingularity"; public override string Name => "ToySingularity";
public int Level { [ViewVariables(VVAccess.ReadWrite)]
get { public float Falloff { get; set; } = 2.0f;
return 1; [ViewVariables(VVAccess.ReadWrite)]
} public float Intensity { get; set; } = 0.25f;
set {
}
}
} }
} }

View File

@@ -5,6 +5,7 @@ using Robust.Shared.Prototypes;
using System.Collections.Generic; using System.Collections.Generic;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using System.Linq; using System.Linq;
using System;
using Robust.Shared.Enums; using Robust.Shared.Enums;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Content.Client.GameObjects.Components.Singularity; using Content.Client.GameObjects.Components.Singularity;
@@ -17,7 +18,6 @@ namespace Content.Client.Graphics.Overlays
[Dependency] private readonly IComponentManager _componentManager = default!; [Dependency] private readonly IComponentManager _componentManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IClyde _displayManager = default!; [Dependency] private readonly IClyde _displayManager = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace; public override OverlaySpace Space => OverlaySpace.WorldSpace;
@@ -40,22 +40,28 @@ namespace Content.Client.Graphics.Overlays
protected override void Draw(in OverlayDrawArgs args) protected override void Draw(in OverlayDrawArgs args)
{ {
SingularityQuery(); SingularityQuery(args.Viewport.Eye);
var viewportWB = args.WorldBounds;
// This is a blatant cheat.
// The correct way of doing this would be if the singularity shader performed the matrix transforms.
// I don't need to explain why I'm not doing that.
var resolution = Math.Max(0.125f, Math.Min(args.Viewport.RenderScale.X, args.Viewport.RenderScale.Y));
foreach (SingularityShaderInstance instance in _singularities.Values) foreach (SingularityShaderInstance instance in _singularities.Values)
{ {
var tempCoords = _eyeManager.WorldToScreen(instance.CurrentMapCoords); // To be clear, this needs to use "inside-viewport" pixels.
tempCoords.Y = _displayManager.ScreenSize.Y - tempCoords.Y; // In other words, specifically NOT IViewportControl.WorldToScreen (which uses outer coordinates).
var tempCoords = args.Viewport.WorldToLocal(instance.CurrentMapCoords);
tempCoords.Y = args.Viewport.Size.Y - tempCoords.Y;
_shader?.SetParameter("positionInput", tempCoords); _shader?.SetParameter("positionInput", tempCoords);
if (ScreenTexture != null) if (ScreenTexture != null)
_shader?.SetParameter("SCREEN_TEXTURE", ScreenTexture); _shader?.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_shader?.SetParameter("intensity", LevelToIntensity(instance.Level)); _shader?.SetParameter("intensity", instance.Intensity / resolution);
_shader?.SetParameter("falloff", LevelToFalloff(instance.Level)); _shader?.SetParameter("falloff", instance.Falloff / resolution);
var worldHandle = args.WorldHandle; var worldHandle = args.WorldHandle;
worldHandle.UseShader(_shader); worldHandle.UseShader(_shader);
var viewport = _eyeManager.GetWorldViewport(); worldHandle.DrawRect(viewportWB, Color.White);
worldHandle.DrawRect(viewport, Color.White);
} }
} }
@@ -64,19 +70,24 @@ namespace Content.Client.Graphics.Overlays
//Queries all singulos on the map and either adds or removes them from the list of rendered singulos based on whether they should be drawn (in range? on the same z-level/map? singulo entity still exists?) //Queries all singulos on the map and either adds or removes them from the list of rendered singulos based on whether they should be drawn (in range? on the same z-level/map? singulo entity still exists?)
private float _maxDist = 15.0f; private float _maxDist = 15.0f;
private void SingularityQuery() private void SingularityQuery(IEye? currentEye)
{ {
var currentEyeLoc = _eyeManager.CurrentEye.Position; if (currentEye == null)
var currentMap = _eyeManager.CurrentMap; //TODO: support multiple viewports once it is added {
_singularities.Clear();
return;
}
var currentEyeLoc = currentEye.Position;
var currentMap = currentEye.Position.MapId;
var singuloComponents = _componentManager.EntityQuery<IClientSingularityInstance>(); var singuloComponents = _componentManager.EntityQuery<IClientSingularityInstance>();
foreach (var singuloInterface in singuloComponents) //Add all singulos that are not added yet but qualify foreach (var singuloInterface in singuloComponents) //Add all singulos that are not added yet but qualify
{ {
var singuloComponent = (Component)singuloInterface; var singuloComponent = (Component)singuloInterface;
var singuloEntity = singuloComponent.Owner; var singuloEntity = singuloComponent.Owner;
if (!_singularities.Keys.Contains(singuloEntity.Uid) && singuloEntity.Transform.MapID == currentMap && singuloEntity.Transform.Coordinates.InRange(_entityManager, EntityCoordinates.FromMap(_entityManager, singuloEntity.Transform.ParentUid, currentEyeLoc), _maxDist)) if (!_singularities.Keys.Contains(singuloEntity.Uid) && SinguloQualifies(singuloEntity, currentEyeLoc))
{ {
_singularities.Add(singuloEntity.Uid, new SingularityShaderInstance(singuloEntity.Transform.MapPosition.Position, singuloInterface.Level)); _singularities.Add(singuloEntity.Uid, new SingularityShaderInstance(singuloEntity.Transform.MapPosition.Position, singuloInterface.Intensity, singuloInterface.Falloff));
} }
} }
@@ -85,7 +96,7 @@ namespace Content.Client.Graphics.Overlays
{ {
if (_entityManager.TryGetEntity(activeSinguloUid, out IEntity? singuloEntity)) if (_entityManager.TryGetEntity(activeSinguloUid, out IEntity? singuloEntity))
{ {
if (singuloEntity.Transform.MapID != currentMap || !singuloEntity.Transform.Coordinates.InRange(_entityManager, EntityCoordinates.FromMap(_entityManager, singuloEntity.Transform.ParentUid, currentEyeLoc), _maxDist)) if (!SinguloQualifies(singuloEntity, currentEyeLoc))
{ {
_singularities.Remove(activeSinguloUid); _singularities.Remove(activeSinguloUid);
} }
@@ -99,7 +110,8 @@ namespace Content.Client.Graphics.Overlays
{ {
var shaderInstance = _singularities[activeSinguloUid]; var shaderInstance = _singularities[activeSinguloUid];
shaderInstance.CurrentMapCoords = singuloEntity.Transform.MapPosition.Position; shaderInstance.CurrentMapCoords = singuloEntity.Transform.MapPosition.Position;
shaderInstance.Level = singuloInterface.Level; shaderInstance.Intensity = singuloInterface.Intensity;
shaderInstance.Falloff = singuloInterface.Falloff;
} }
} }
@@ -112,62 +124,21 @@ namespace Content.Client.Graphics.Overlays
} }
private bool SinguloQualifies(IEntity singuloEntity, MapCoordinates currentEyeLoc)
//I am lazy
private float LevelToIntensity(int level)
{ {
switch (level) return singuloEntity.Transform.MapID == currentEyeLoc.MapId && singuloEntity.Transform.Coordinates.InRange(_entityManager, EntityCoordinates.FromMap(_entityManager, singuloEntity.Transform.ParentUid, currentEyeLoc), _maxDist);
{
case 0:
return 0.0f;
case 1:
return 2.7f;
case 2:
return 14.4f;
case 3:
return 47.2f;
case 4:
return 180.0f;
case 5:
return 600.0f;
case 6:
return 800.0f;
}
return -1.0f;
}
private float LevelToFalloff(int level)
{
switch (level)
{
case 0:
return 9999f;
case 1:
return 6.4f;
case 2:
return 7.0f;
case 3:
return 8.0f;
case 4:
return 10.0f;
case 5:
return 12.0f;
case 6:
return 12.0f;
}
return -1.0f;
} }
private sealed class SingularityShaderInstance private sealed class SingularityShaderInstance
{ {
public Vector2 CurrentMapCoords; public Vector2 CurrentMapCoords;
public int Level; public float Intensity;
public SingularityShaderInstance(Vector2 mapCoords, int level) public float Falloff;
public SingularityShaderInstance(Vector2 mapCoords, float intensity, float falloff)
{ {
CurrentMapCoords = mapCoords; CurrentMapCoords = mapCoords;
Level = level; Intensity = intensity;
Falloff = falloff;
} }
} }
} }

View File

@@ -229,6 +229,7 @@ namespace Content.Client
"GlassBeaker", "GlassBeaker",
"SliceableFood", "SliceableFood",
"DamageOtherOnHit", "DamageOtherOnHit",
"SinguloFood",
"DamageOnLand", "DamageOnLand",
"SmokeSolutionAreaEffect", "SmokeSolutionAreaEffect",
"FoamSolutionAreaEffect", "FoamSolutionAreaEffect",

View File

@@ -2,6 +2,7 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.GameObjects.Components.Singularity; using Content.Server.GameObjects.Components.Singularity;
using Content.Server.GameObjects.Components.PA; using Content.Server.GameObjects.Components.PA;
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.GameObjects.Components; using Content.Shared.GameObjects.Components;
using Robust.Shared.Console; using Robust.Shared.Console;
@@ -30,11 +31,15 @@ namespace Content.Server.Commands
{ {
ent.GetComponent<EmitterComponent>().SwitchOn(); ent.GetComponent<EmitterComponent>().SwitchOn();
} }
foreach (var ent in entityManager.GetEntities(new TypeEntityQuery(typeof(RadiationCollectorComponent))))
{
ent.GetComponent<RadiationCollectorComponent>().Collecting = true;
}
foreach (var ent in entityManager.GetEntities(new TypeEntityQuery(typeof(ParticleAcceleratorControlBoxComponent)))) foreach (var ent in entityManager.GetEntities(new TypeEntityQuery(typeof(ParticleAcceleratorControlBoxComponent))))
{ {
var pacb = ent.GetComponent<ParticleAcceleratorControlBoxComponent>(); var pacb = ent.GetComponent<ParticleAcceleratorControlBoxComponent>();
pacb.RescanParts(); pacb.RescanParts();
pacb.SetStrength(ParticleAcceleratorPowerState.Level1); pacb.SetStrength(ParticleAcceleratorPowerState.Level0);
pacb.SwitchOn(); pacb.SwitchOn();
} }
shell.WriteLine("Done!"); shell.WriteLine("Done!");

View File

@@ -20,21 +20,7 @@ namespace Content.Server.GameObjects.Components.PA
private ParticleAcceleratorPowerState _state; private ParticleAcceleratorPowerState _state;
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold) void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
{ {
if (otherFixture.Body.Owner.TryGetComponent<ServerSingularityComponent>(out var singularityComponent)) if (otherFixture.Body.Owner.TryGetComponent<SingularityGeneratorComponent>(out var singularityGeneratorComponent))
{
var multiplier = _state switch
{
ParticleAcceleratorPowerState.Standby => 0,
ParticleAcceleratorPowerState.Level0 => 1,
ParticleAcceleratorPowerState.Level1 => 3,
ParticleAcceleratorPowerState.Level2 => 6,
ParticleAcceleratorPowerState.Level3 => 10,
_ => 0
};
singularityComponent.Energy += 10 * multiplier;
Owner.QueueDelete();
}
else if (otherFixture.Body.Owner.TryGetComponent<SingularityGeneratorComponent>(out var singularityGeneratorComponent))
{ {
singularityGeneratorComponent.Power += _state switch singularityGeneratorComponent.Power += _state switch
{ {
@@ -67,6 +53,22 @@ namespace Content.Server.GameObjects.Components.PA
} }
projectileComponent.IgnoreEntity(firer); projectileComponent.IgnoreEntity(firer);
if (!Owner.TryGetComponent<SinguloFoodComponent>(out var singuloFoodComponent))
{
Logger.Error("ParticleProjectile tried firing, but it was spawned without a SinguloFoodComponent");
return;
}
var multiplier = _state switch
{
ParticleAcceleratorPowerState.Standby => 0,
ParticleAcceleratorPowerState.Level0 => 1,
ParticleAcceleratorPowerState.Level1 => 3,
ParticleAcceleratorPowerState.Level2 => 6,
ParticleAcceleratorPowerState.Level3 => 10,
_ => 0
};
singuloFoodComponent.Energy = 10 * multiplier;
var suffix = state switch var suffix = state switch
{ {
ParticleAcceleratorPowerState.Level0 => "0", ParticleAcceleratorPowerState.Level0 => "0",

View File

@@ -11,11 +11,12 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Power.PowerNetComponents namespace Content.Server.GameObjects.Components.Power.PowerNetComponents
{ {
[RegisterComponent] [RegisterComponent]
public class RadiationCollectorComponent : PowerSupplierComponent, IInteractHand, IRadiationAct public class RadiationCollectorComponent : Component, IInteractHand, IRadiationAct
{ {
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -23,14 +24,20 @@ namespace Content.Server.GameObjects.Components.Power.PowerNetComponents
private bool _enabled; private bool _enabled;
private TimeSpan _coolDownEnd; private TimeSpan _coolDownEnd;
[ComponentDependency] private readonly PhysicsComponent? _collidableComponent = default!; [ViewVariables(VVAccess.ReadWrite)]
public bool Collecting {
public void OnAnchoredChanged() get => _enabled;
{ set
if(_collidableComponent != null && _collidableComponent.BodyType == BodyType.Static) {
Owner.SnapToGrid(); if (_enabled == value) return;
_enabled = value;
SetAppearance(_enabled ? RadiationCollectorVisualState.Activating : RadiationCollectorVisualState.Deactivating);
}
} }
[ComponentDependency] private readonly BatteryComponent? _batteryComponent = default!;
[ComponentDependency] private readonly BatteryDischargerComponent? _batteryDischargerComponent = default!;
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
{ {
var curTime = _gameTiming.CurTime; var curTime = _gameTiming.CurTime;
@@ -40,13 +47,13 @@ namespace Content.Server.GameObjects.Components.Power.PowerNetComponents
if (!_enabled) if (!_enabled)
{ {
Owner.PopupMessage(eventArgs.User, Loc.GetString("The collector turns on.")); Owner.PopupMessage(eventArgs.User, Loc.GetString("radiation-collector-component-use-on"));
EnableCollection(); Collecting = true;
} }
else else
{ {
Owner.PopupMessage(eventArgs.User, Loc.GetString("The collector turns off.")); Owner.PopupMessage(eventArgs.User, Loc.GetString("radiation-collector-component-use-off"));
DisableCollection(); Collecting = false;
} }
_coolDownEnd = curTime + TimeSpan.FromSeconds(0.81f); _coolDownEnd = curTime + TimeSpan.FromSeconds(0.81f);
@@ -54,23 +61,25 @@ namespace Content.Server.GameObjects.Components.Power.PowerNetComponents
return true; return true;
} }
void EnableCollection()
{
_enabled = true;
SetAppearance(RadiationCollectorVisualState.Activating);
}
void DisableCollection()
{
_enabled = false;
SetAppearance(RadiationCollectorVisualState.Deactivating);
}
void IRadiationAct.RadiationAct(float frameTime, SharedRadiationPulseComponent radiation) void IRadiationAct.RadiationAct(float frameTime, SharedRadiationPulseComponent radiation)
{ {
if (!_enabled) return; if (!_enabled) return;
SupplyRate = (int) (frameTime * radiation.RadsPerSecond * 3000f); // No idea if this is even vaguely accurate to the previous logic.
// The maths is copied from that logic even though it works differently.
// But the previous logic would also make the radiation collectors never ever stop providing energy.
// And since frameTime was used there, I'm assuming that this is what the intent was.
// This still won't stop things being potentially hilarously unbalanced though.
if (_batteryComponent != null)
{
_batteryComponent!.CurrentCharge += frameTime * radiation.RadsPerSecond * 3000f;
if (_batteryDischargerComponent != null)
{
// The battery discharger is controlled like this to ensure it won't drain the entire battery in a single tick.
// If that occurs then the battery discharger ends up shutting down.
_batteryDischargerComponent!.ActiveSupplyRate = (int) Math.Max(1, _batteryComponent!.CurrentCharge);
}
}
} }
protected void SetAppearance(RadiationCollectorVisualState state) protected void SetAppearance(RadiationCollectorVisualState state)

View File

@@ -22,7 +22,7 @@ namespace Content.Server.GameObjects.Components.Singularity
get => _sharedEnergyPool; get => _sharedEnergyPool;
set set
{ {
_sharedEnergyPool = Math.Clamp(value, 0, 10); _sharedEnergyPool = Math.Clamp(value, 0, 25);
if (_sharedEnergyPool == 0) if (_sharedEnergyPool == 0)
{ {
Dispose(); Dispose();

View File

@@ -168,8 +168,8 @@ namespace Content.Server.GameObjects.Components.Singularity
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold) void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
{ {
if(otherFixture.Body.Owner.HasTag("EmitterBolt")) { if (otherFixture.Body.Owner.HasTag("EmitterBolt")) {
ReceivePower(4); ReceivePower(6);
} }
} }

View File

@@ -7,6 +7,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision; using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Collision.Shapes;
@@ -14,12 +15,14 @@ using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Players; using Robust.Shared.Players;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Singularity namespace Content.Server.GameObjects.Components.Singularity
{ {
[RegisterComponent] [RegisterComponent]
public class ServerSingularityComponent : SharedSingularityComponent, IStartCollide public class ServerSingularityComponent : SharedSingularityComponent, IStartCollide
{ {
[ViewVariables(VVAccess.ReadWrite)]
public int Energy public int Energy
{ {
get => _energy; get => _energy;
@@ -48,6 +51,7 @@ namespace Content.Server.GameObjects.Components.Singularity
} }
private int _energy = 180; private int _energy = 180;
[ViewVariables]
public int Level public int Level
{ {
get => _level; get => _level;
@@ -58,6 +62,11 @@ namespace Content.Server.GameObjects.Components.Singularity
if (value > 6) value = 6; if (value > 6) value = 6;
_level = value; _level = value;
if ((_level > 1) && (value <= 1))
{
// Prevents it getting stuck (see SingularityController.MoveSingulo)
if (_collidableComponent != null) _collidableComponent.LinearVelocity = Vector2.Zero;
}
if(_radiationPulseComponent != null) _radiationPulseComponent.RadsPerSecond = 10 * value; if(_radiationPulseComponent != null) _radiationPulseComponent.RadsPerSecond = 10 * value;
@@ -76,6 +85,7 @@ namespace Content.Server.GameObjects.Components.Singularity
} }
private int _level; private int _level;
[ViewVariables]
public int EnergyDrain => public int EnergyDrain =>
Level switch Level switch
{ {
@@ -88,6 +98,12 @@ namespace Content.Server.GameObjects.Components.Singularity
_ => 0 _ => 0
}; };
// This is an interesting little workaround.
// See, two singularities queuing deletion of each other at the same time will annihilate.
// This is undesirable behaviour, so this flag allows the imperatively first one processed to take priority.
[ViewVariables(VVAccess.ReadWrite)]
public bool BeingDeletedByAnotherSingularity { get; set; } = false;
private PhysicsComponent _collidableComponent = default!; private PhysicsComponent _collidableComponent = default!;
private RadiationPulseComponent _radiationPulseComponent = default!; private RadiationPulseComponent _radiationPulseComponent = default!;
private SpriteComponent _spriteComponent = default!; private SpriteComponent _spriteComponent = default!;
@@ -123,6 +139,11 @@ namespace Content.Server.GameObjects.Components.Singularity
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold) void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
{ {
// If we're being deleted by another singularity, this call is probably for that singularity.
// Even if not, just don't bother.
if (BeingDeletedByAnotherSingularity)
return;
var otherEntity = otherFixture.Body.Owner; var otherEntity = otherFixture.Body.Owner;
if (otherEntity.TryGetComponent<IMapGridComponent>(out var mapGridComponent)) if (otherEntity.TryGetComponent<IMapGridComponent>(out var mapGridComponent))
@@ -143,8 +164,16 @@ namespace Content.Server.GameObjects.Components.Singularity
if (otherEntity.IsInContainer()) if (otherEntity.IsInContainer())
return; return;
// Singularity priority management / etc.
if (otherEntity.TryGetComponent<ServerSingularityComponent>(out var otherSingulo))
otherSingulo.BeingDeletedByAnotherSingularity = true;
otherEntity.QueueDelete(); otherEntity.QueueDelete();
Energy++;
if (otherEntity.TryGetComponent<SinguloFoodComponent>(out var singuloFood))
Energy += singuloFood.Energy;
else
Energy++;
} }
public override void OnRemove() public override void OnRemove()

View File

@@ -0,0 +1,23 @@
using Content.Shared.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.GameObjects.Components.Singularity
{
/// <summary>
/// Overrides exactly how much energy this object gives to a singularity.
/// </summary>
[RegisterComponent]
public class SinguloFoodComponent : Component
{
public override string Name => "SinguloFood";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("energy")]
public int Energy { get; set; } = 1;
}
}

View File

@@ -1,30 +0,0 @@
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
public sealed class RadiationCollectorSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RadiationCollectorComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<RadiationCollectorComponent, PhysicsBodyTypeChangedEvent>();
}
private static void BodyTypeChanged(
EntityUid uid,
RadiationCollectorComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.OnAnchoredChanged();
}
}
}

View File

@@ -59,6 +59,7 @@ namespace Content.Server.Physics.Controllers
private void MoveSingulo(ServerSingularityComponent singularity, PhysicsComponent physics) private void MoveSingulo(ServerSingularityComponent singularity, PhysicsComponent physics)
{ {
// To prevent getting stuck, ServerSingularityComponent will zero the velocity of a singularity when it goes to a level <= 1 (see here).
if (singularity.Level <= 1) return; if (singularity.Level <= 1) return;
// TODO: Could try gradual changes instead but for now just try to replicate // TODO: Could try gradual changes instead but for now just try to replicate

View File

@@ -0,0 +1,3 @@
radiation-collector-component-use-on = The collector turns on.
radiation-collector-component-use-off = The collector turns off.

View File

@@ -47820,4 +47820,46 @@ entities:
EntityStorageComponent: !type:Container EntityStorageComponent: !type:Container
ents: [] ents: []
type: ContainerContainer type: ContainerContainer
- uid: 4897
type: ParticleAcceleratorEndCapUnfinished
components:
- pos: 49.5,-9.5
parent: 853
type: Transform
- uid: 4898
type: ParticleAcceleratorFuelChamberUnfinished
components:
- pos: 49.5,-10.5
parent: 853
type: Transform
- uid: 4899
type: ParticleAcceleratorPowerBoxUnfinished
components:
- pos: 49.5,-11.5
parent: 853
type: Transform
- uid: 4900
type: ParticleAcceleratorControlBoxUnfinished
components:
- pos: 48.5,-10.5
parent: 853
type: Transform
- uid: 4901
type: ParticleAcceleratorEmitterCenterUnfinished
components:
- pos: 49.5,-12.5
parent: 853
type: Transform
- uid: 4902
type: ParticleAcceleratorEmitterLeftUnfinished
components:
- pos: 48.5,-12.5
parent: 853
type: Transform
- uid: 4903
type: ParticleAcceleratorEmitterRightUnfinished
components:
- pos: 50.5,-12.5
parent: 853
type: Transform
... ...

View File

@@ -26,3 +26,6 @@
- MobMask - MobMask
- Opaque - Opaque
- type: ParticleProjectile - type: ParticleProjectile
- type: SinguloFood
# Energy is setup by the PA particle fire function.

View File

@@ -39,6 +39,16 @@
!type:AdjacentNode !type:AdjacentNode
nodeGroupID: HVPower nodeGroupID: HVPower
- type: RadiationCollector - type: RadiationCollector
# Note that this doesn't matter too much (see next comment)
# However it does act as a cap on power receivable via the collector.
- type: Battery
maxCharge: 100000
startingCharge: 0
- type: BatteryDischarger
# This is JUST a default. It has to be dynamically adjusted to ensure that the battery doesn't discharge "too fast" & run out immediately, while still scaling by input power.
activeSupplyRate: 100000
- type: PowerSupplier
supplyRate: 0
- type: Anchorable - type: Anchorable
- type: Rotatable - type: Rotatable
- type: Pullable - type: Pullable

95
Tools/singulo.m Normal file
View File

@@ -0,0 +1,95 @@
# This is a script to be loaded into GNU Octave.
# - Notes -
# + Be sure to check all parameters are up to date with game before use.
# + The way things are tuned, only PA level 1 is stable on Saltern.
# A singularity timestep is one second.
# - Parameters -
# It's expected that you dynamically modify these if relevant to your scenario.
global pa_particle_energy_for_level_table pa_level pa_time_between_shots
pa_particle_energy_for_level_table = [10, 30, 60, 100]
# Note that level 0 is 1 here.
pa_level = 1
pa_time_between_shots = 6
# Horizontal size (interior tiles) of mapped singulo cage
global cage_area cage_pa1 cage_pa2 cage_pa3
# __123__
# +---+---+
cage_area = 7
cage_pa1 = 2.5
cage_pa2 = 3.5
cage_pa3 = 4.5
global energy_drain_for_level_table
energy_drain_for_level_table = [1, 2, 5, 10, 15, 20]
function retval = level_for_energy (energy)
retval = 1
if energy >= 1500 retval = 6; return; endif
if energy >= 1000 retval = 5; return; endif
if energy >= 600 retval = 4; return; endif
if energy >= 300 retval = 3; return; endif
if energy >= 200 retval = 2; return; endif
endfunction
function retval = radius_for_level (level)
retval = level - 0.5
endfunction
# - Simulator -
global singulo_shot_timer
singulo_shot_timer = 0
function retval = singulo_step (energy)
global energy_drain_for_level_table
global pa_particle_energy_for_level_table pa_level pa_time_between_shots
global cage_area cage_pa1 cage_pa2 cage_pa3
global singulo_shot_timer
level = level_for_energy(energy)
energy_drain = energy_drain_for_level_table(level)
energy -= energy_drain
singulo_shot_timer += 1
if singulo_shot_timer == pa_time_between_shots
energy_gain_per_hit = pa_particle_energy_for_level_table(pa_level)
# This is the bit that's complicated: the area and probability calculation.
# Rather than try to work it out, let's do things by simply trying it.
# This is the area of the singulo.
singulo_area = radius_for_level(level) * 2
# This is therefore the area in which it can move.
effective_area = max(0, cage_area - singulo_area)
# Assume it's at some random position within the area it can move.
# (This is the weak point of the maths. It's not as simple as this really.)
singulo_lpos = (rand() * effective_area)
singulo_rpos = singulo_lpos + singulo_area
# Check each of 3 points.
n = 0.5
if singulo_lpos < (cage_pa1 + n) && singulo_rpos > (cage_pa1 - n)
energy += energy_gain_per_hit
endif
if singulo_lpos < (cage_pa2 + n) && singulo_rpos > (cage_pa2 - n)
energy += energy_gain_per_hit
endif
if singulo_lpos < (cage_pa3 + n) && singulo_rpos > (cage_pa3 - n)
energy += energy_gain_per_hit
endif
singulo_shot_timer = 0
endif
retval = energy
endfunction
# - Scenario -
global scenario_energy
scenario_energy = 100
function retval = scenario (x)
global scenario_energy
sce = scenario_energy
scenario_energy = singulo_step(sce)
retval = scenario_energy
endfunction
# x is in seconds.
x = 0:1:960
plot(x, arrayfun(@scenario, x))

65
Tools/singulo_emitter.m Normal file
View File

@@ -0,0 +1,65 @@
# This is a script to be loaded into GNU Octave.
# - Notes -
# + Be sure to check all parameters are up to date with game before use.
# + This plots *worst-case* performance, the assumption is that it shouldn't ever randomly fail.
# + The assumption is that there is one emitter per shield point.
# + Keep in mind that to prevent the generator being destroyed, either shield must be above a limit.
# This limit is (level*2)+1.
# The timestep used for simulation is one second.
global emitter_state emitter_timer shield_energy
emitter_state = 0
emitter_timer = 0
shield_energy = 0
function shield_clamp ()
global shield_energy
# ContainmentFieldConnection.SharedEnergyPool
shield_energy = min(max(shield_energy, 0), 25)
endfunction
function shield_tick ()
global shield_energy
shield_energy -= 1
shield_clamp()
endfunction
function shield_hit ()
global shield_energy
emitter_count = 2 # one per connection side
receive_power = 6 # ContainmentFieldGeneratorComponent.IStartCollide.CollideWith
power_per_connection = receive_power / 2 # ContainmentFieldGeneratorComponent.ReceivePower
shield_energy += power_per_connection * emitter_count
shield_clamp()
endfunction
function retval = scenario (x)
global emitter_state emitter_timer shield_energy
# Tick (degrade) shield
shield_tick()
# Timer...
if emitter_timer > 0
emitter_timer -= 1
else
# Note the logic here is written to match how EmitterComponent does it.
# Fire first...
shield_hit()
# Then check if < fireBurstSize
if emitter_state < 3
# Then increment & reset
emitter_state += 1
# to fireInterval
emitter_timer = 2
else
# Reset state
emitter_state = 0
# Worst case, fireBurstDelayMax
emitter_timer = 10
endif
endif
retval = shield_energy
endfunction
# x is in seconds.
x = 0:1:960
plot(x, arrayfun(@scenario, x))