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:
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.GameObjects.Components.Singularity;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Singularity
|
||||
{
|
||||
@@ -7,18 +8,60 @@ namespace Content.Client.GameObjects.Components.Singularity
|
||||
[ComponentReference(typeof(IClientSingularityInstance))]
|
||||
class ClientSingularityComponent : SharedSingularityComponent, IClientSingularityInstance
|
||||
{
|
||||
public int Level
|
||||
[ViewVariables]
|
||||
public int Level { get; set; }
|
||||
|
||||
//I am lazy
|
||||
[ViewVariables]
|
||||
public float Intensity
|
||||
{
|
||||
get
|
||||
{
|
||||
return _level;
|
||||
}
|
||||
set
|
||||
{
|
||||
_level = value;
|
||||
switch (Level)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
[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)
|
||||
{
|
||||
@@ -26,7 +69,7 @@ namespace Content.Client.GameObjects.Components.Singularity
|
||||
{
|
||||
return;
|
||||
}
|
||||
_level = state.Level;
|
||||
Level = state.Level;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Content.Client.GameObjects.Components.Singularity
|
||||
{
|
||||
interface IClientSingularityInstance
|
||||
{
|
||||
public int Level { get; set; }
|
||||
public float Intensity { get; }
|
||||
public float Falloff { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Singularity
|
||||
{
|
||||
@@ -8,12 +9,9 @@ namespace Content.Client.GameObjects.Components.Singularity
|
||||
public class ToySingularityComponent : Component, IClientSingularityInstance
|
||||
{
|
||||
public override string Name => "ToySingularity";
|
||||
public int Level {
|
||||
get {
|
||||
return 1;
|
||||
}
|
||||
set {
|
||||
}
|
||||
}
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Falloff { get; set; } = 2.0f;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Intensity { get; set; } = 0.25f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Shared.Prototypes;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Content.Client.GameObjects.Components.Singularity;
|
||||
@@ -17,7 +18,6 @@ namespace Content.Client.Graphics.Overlays
|
||||
[Dependency] private readonly IComponentManager _componentManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IClyde _displayManager = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
@@ -40,22 +40,28 @@ namespace Content.Client.Graphics.Overlays
|
||||
|
||||
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)
|
||||
{
|
||||
var tempCoords = _eyeManager.WorldToScreen(instance.CurrentMapCoords);
|
||||
tempCoords.Y = _displayManager.ScreenSize.Y - tempCoords.Y;
|
||||
// To be clear, this needs to use "inside-viewport" pixels.
|
||||
// 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);
|
||||
if (ScreenTexture != null)
|
||||
_shader?.SetParameter("SCREEN_TEXTURE", ScreenTexture);
|
||||
_shader?.SetParameter("intensity", LevelToIntensity(instance.Level));
|
||||
_shader?.SetParameter("falloff", LevelToFalloff(instance.Level));
|
||||
_shader?.SetParameter("intensity", instance.Intensity / resolution);
|
||||
_shader?.SetParameter("falloff", instance.Falloff / resolution);
|
||||
|
||||
var worldHandle = args.WorldHandle;
|
||||
worldHandle.UseShader(_shader);
|
||||
var viewport = _eyeManager.GetWorldViewport();
|
||||
worldHandle.DrawRect(viewport, Color.White);
|
||||
worldHandle.DrawRect(viewportWB, 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?)
|
||||
private float _maxDist = 15.0f;
|
||||
private void SingularityQuery()
|
||||
private void SingularityQuery(IEye? currentEye)
|
||||
{
|
||||
var currentEyeLoc = _eyeManager.CurrentEye.Position;
|
||||
var currentMap = _eyeManager.CurrentMap; //TODO: support multiple viewports once it is added
|
||||
if (currentEye == null)
|
||||
{
|
||||
_singularities.Clear();
|
||||
return;
|
||||
}
|
||||
var currentEyeLoc = currentEye.Position;
|
||||
var currentMap = currentEye.Position.MapId;
|
||||
|
||||
var singuloComponents = _componentManager.EntityQuery<IClientSingularityInstance>();
|
||||
foreach (var singuloInterface in singuloComponents) //Add all singulos that are not added yet but qualify
|
||||
{
|
||||
var singuloComponent = (Component)singuloInterface;
|
||||
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 (singuloEntity.Transform.MapID != currentMap || !singuloEntity.Transform.Coordinates.InRange(_entityManager, EntityCoordinates.FromMap(_entityManager, singuloEntity.Transform.ParentUid, currentEyeLoc), _maxDist))
|
||||
if (!SinguloQualifies(singuloEntity, currentEyeLoc))
|
||||
{
|
||||
_singularities.Remove(activeSinguloUid);
|
||||
}
|
||||
@@ -99,7 +110,8 @@ namespace Content.Client.Graphics.Overlays
|
||||
{
|
||||
var shaderInstance = _singularities[activeSinguloUid];
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//I am lazy
|
||||
private float LevelToIntensity(int level)
|
||||
private bool SinguloQualifies(IEntity singuloEntity, MapCoordinates currentEyeLoc)
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
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;
|
||||
return singuloEntity.Transform.MapID == currentEyeLoc.MapId && singuloEntity.Transform.Coordinates.InRange(_entityManager, EntityCoordinates.FromMap(_entityManager, singuloEntity.Transform.ParentUid, currentEyeLoc), _maxDist);
|
||||
}
|
||||
|
||||
private sealed class SingularityShaderInstance
|
||||
{
|
||||
public Vector2 CurrentMapCoords;
|
||||
public int Level;
|
||||
public SingularityShaderInstance(Vector2 mapCoords, int level)
|
||||
public float Intensity;
|
||||
public float Falloff;
|
||||
public SingularityShaderInstance(Vector2 mapCoords, float intensity, float falloff)
|
||||
{
|
||||
CurrentMapCoords = mapCoords;
|
||||
Level = level;
|
||||
Intensity = intensity;
|
||||
Falloff = falloff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ namespace Content.Client
|
||||
"GlassBeaker",
|
||||
"SliceableFood",
|
||||
"DamageOtherOnHit",
|
||||
"SinguloFood",
|
||||
"DamageOnLand",
|
||||
"SmokeSolutionAreaEffect",
|
||||
"FoamSolutionAreaEffect",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.GameObjects.Components.Singularity;
|
||||
using Content.Server.GameObjects.Components.PA;
|
||||
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Robust.Shared.Console;
|
||||
@@ -30,11 +31,15 @@ namespace Content.Server.Commands
|
||||
{
|
||||
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))))
|
||||
{
|
||||
var pacb = ent.GetComponent<ParticleAcceleratorControlBoxComponent>();
|
||||
pacb.RescanParts();
|
||||
pacb.SetStrength(ParticleAcceleratorPowerState.Level1);
|
||||
pacb.SetStrength(ParticleAcceleratorPowerState.Level0);
|
||||
pacb.SwitchOn();
|
||||
}
|
||||
shell.WriteLine("Done!");
|
||||
|
||||
@@ -20,21 +20,7 @@ namespace Content.Server.GameObjects.Components.PA
|
||||
private ParticleAcceleratorPowerState _state;
|
||||
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
|
||||
{
|
||||
if (otherFixture.Body.Owner.TryGetComponent<ServerSingularityComponent>(out var singularityComponent))
|
||||
{
|
||||
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))
|
||||
if (otherFixture.Body.Owner.TryGetComponent<SingularityGeneratorComponent>(out var singularityGeneratorComponent))
|
||||
{
|
||||
singularityGeneratorComponent.Power += _state switch
|
||||
{
|
||||
@@ -67,6 +53,22 @@ namespace Content.Server.GameObjects.Components.PA
|
||||
}
|
||||
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
|
||||
{
|
||||
ParticleAcceleratorPowerState.Level0 => "0",
|
||||
|
||||
@@ -11,11 +11,12 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Power.PowerNetComponents
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class RadiationCollectorComponent : PowerSupplierComponent, IInteractHand, IRadiationAct
|
||||
public class RadiationCollectorComponent : Component, IInteractHand, IRadiationAct
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
@@ -23,14 +24,20 @@ namespace Content.Server.GameObjects.Components.Power.PowerNetComponents
|
||||
private bool _enabled;
|
||||
private TimeSpan _coolDownEnd;
|
||||
|
||||
[ComponentDependency] private readonly PhysicsComponent? _collidableComponent = default!;
|
||||
|
||||
public void OnAnchoredChanged()
|
||||
{
|
||||
if(_collidableComponent != null && _collidableComponent.BodyType == BodyType.Static)
|
||||
Owner.SnapToGrid();
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Collecting {
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
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)
|
||||
{
|
||||
var curTime = _gameTiming.CurTime;
|
||||
@@ -40,13 +47,13 @@ namespace Content.Server.GameObjects.Components.Power.PowerNetComponents
|
||||
|
||||
if (!_enabled)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("The collector turns on."));
|
||||
EnableCollection();
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("radiation-collector-component-use-on"));
|
||||
Collecting = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("The collector turns off."));
|
||||
DisableCollection();
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("radiation-collector-component-use-off"));
|
||||
Collecting = false;
|
||||
}
|
||||
|
||||
_coolDownEnd = curTime + TimeSpan.FromSeconds(0.81f);
|
||||
@@ -54,23 +61,25 @@ namespace Content.Server.GameObjects.Components.Power.PowerNetComponents
|
||||
return true;
|
||||
}
|
||||
|
||||
void EnableCollection()
|
||||
{
|
||||
_enabled = true;
|
||||
SetAppearance(RadiationCollectorVisualState.Activating);
|
||||
}
|
||||
|
||||
void DisableCollection()
|
||||
{
|
||||
_enabled = false;
|
||||
SetAppearance(RadiationCollectorVisualState.Deactivating);
|
||||
}
|
||||
|
||||
void IRadiationAct.RadiationAct(float frameTime, SharedRadiationPulseComponent radiation)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Content.Server.GameObjects.Components.Singularity
|
||||
get => _sharedEnergyPool;
|
||||
set
|
||||
{
|
||||
_sharedEnergyPool = Math.Clamp(value, 0, 10);
|
||||
_sharedEnergyPool = Math.Clamp(value, 0, 25);
|
||||
if (_sharedEnergyPool == 0)
|
||||
{
|
||||
Dispose();
|
||||
|
||||
@@ -168,8 +168,8 @@ namespace Content.Server.GameObjects.Components.Singularity
|
||||
|
||||
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
|
||||
{
|
||||
if(otherFixture.Body.Owner.HasTag("EmitterBolt")) {
|
||||
ReceivePower(4);
|
||||
if (otherFixture.Body.Owner.HasTag("EmitterBolt")) {
|
||||
ReceivePower(6);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
@@ -14,12 +15,14 @@ using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Singularity
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class ServerSingularityComponent : SharedSingularityComponent, IStartCollide
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Energy
|
||||
{
|
||||
get => _energy;
|
||||
@@ -48,6 +51,7 @@ namespace Content.Server.GameObjects.Components.Singularity
|
||||
}
|
||||
private int _energy = 180;
|
||||
|
||||
[ViewVariables]
|
||||
public int Level
|
||||
{
|
||||
get => _level;
|
||||
@@ -58,6 +62,11 @@ namespace Content.Server.GameObjects.Components.Singularity
|
||||
if (value > 6) value = 6;
|
||||
|
||||
_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;
|
||||
|
||||
@@ -76,6 +85,7 @@ namespace Content.Server.GameObjects.Components.Singularity
|
||||
}
|
||||
private int _level;
|
||||
|
||||
[ViewVariables]
|
||||
public int EnergyDrain =>
|
||||
Level switch
|
||||
{
|
||||
@@ -88,6 +98,12 @@ namespace Content.Server.GameObjects.Components.Singularity
|
||||
_ => 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 RadiationPulseComponent _radiationPulseComponent = 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)
|
||||
{
|
||||
// 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;
|
||||
|
||||
if (otherEntity.TryGetComponent<IMapGridComponent>(out var mapGridComponent))
|
||||
@@ -143,8 +164,16 @@ namespace Content.Server.GameObjects.Components.Singularity
|
||||
if (otherEntity.IsInContainer())
|
||||
return;
|
||||
|
||||
// Singularity priority management / etc.
|
||||
if (otherEntity.TryGetComponent<ServerSingularityComponent>(out var otherSingulo))
|
||||
otherSingulo.BeingDeletedByAnotherSingularity = true;
|
||||
|
||||
otherEntity.QueueDelete();
|
||||
Energy++;
|
||||
|
||||
if (otherEntity.TryGetComponent<SinguloFoodComponent>(out var singuloFood))
|
||||
Energy += singuloFood.Energy;
|
||||
else
|
||||
Energy++;
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ namespace Content.Server.Physics.Controllers
|
||||
|
||||
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;
|
||||
// TODO: Could try gradual changes instead but for now just try to replicate
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
radiation-collector-component-use-on = The collector turns on.
|
||||
radiation-collector-component-use-off = The collector turns off.
|
||||
|
||||
@@ -47820,4 +47820,46 @@ entities:
|
||||
EntityStorageComponent: !type:Container
|
||||
ents: []
|
||||
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
|
||||
...
|
||||
|
||||
@@ -26,3 +26,6 @@
|
||||
- MobMask
|
||||
- Opaque
|
||||
- type: ParticleProjectile
|
||||
- type: SinguloFood
|
||||
# Energy is setup by the PA particle fire function.
|
||||
|
||||
|
||||
@@ -39,6 +39,16 @@
|
||||
!type:AdjacentNode
|
||||
nodeGroupID: HVPower
|
||||
- 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: Rotatable
|
||||
- type: Pullable
|
||||
|
||||
95
Tools/singulo.m
Normal file
95
Tools/singulo.m
Normal 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
65
Tools/singulo_emitter.m
Normal 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))
|
||||
Reference in New Issue
Block a user