diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index 33ee0e0a34..8c9a66894a 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -25,6 +25,7 @@
+
diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index 6d1fcb28e9..687e856481 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -21,6 +21,7 @@ using Content.Client.White.JoinQueue;
using Content.Client.White.Jukebox;
using Content.Client.White.Sponsors;
using Content.Client.White.Stalin;
+using Content.Client.White.Trail.Line.Manager;
using Content.Client.White.TTS;
using Content.Shared.Administration.Managers;
@@ -57,6 +58,7 @@ namespace Content.Client.IoC
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
+ IoCManager.Register();
//WD-EDIT
}
}
diff --git a/Content.Client/White/Trail/Line/ITrailLine.cs b/Content.Client/White/Trail/Line/ITrailLine.cs
new file mode 100644
index 0000000000..91b73d8b3a
--- /dev/null
+++ b/Content.Client/White/Trail/Line/ITrailLine.cs
@@ -0,0 +1,15 @@
+using System.Numerics;
+using Content.Shared.White.Trail;
+using Robust.Client.Graphics;
+using Robust.Shared.Map;
+
+namespace Content.Client.White.Trail.Line;
+
+public interface ITrailLine
+{
+ ITrailSettings Settings { get; }
+
+ void TryCreateSegment((Vector2 WorldPosition, Angle WorldRotation) worldPosRot, MapId mapId);
+
+ void Render(DrawingHandleWorld handle, Texture? texture);
+}
diff --git a/Content.Client/White/Trail/Line/Manager/ITrailLineHolder.cs b/Content.Client/White/Trail/Line/Manager/ITrailLineHolder.cs
new file mode 100644
index 0000000000..4bb9d6c430
--- /dev/null
+++ b/Content.Client/White/Trail/Line/Manager/ITrailLineHolder.cs
@@ -0,0 +1,6 @@
+namespace Content.Client.White.Trail.Line.Manager;
+
+public interface ITrailLineHolder
+{
+ public ITrailLine? TrailLine { get; set; }
+}
diff --git a/Content.Client/White/Trail/Line/Manager/ITrailLineManager.cs b/Content.Client/White/Trail/Line/Manager/ITrailLineManager.cs
new file mode 100644
index 0000000000..663a7b4bd3
--- /dev/null
+++ b/Content.Client/White/Trail/Line/Manager/ITrailLineManager.cs
@@ -0,0 +1,15 @@
+using Content.Shared.White.Trail;
+using Robust.Shared.Map;
+
+namespace Content.Client.White.Trail.Line.Manager;
+
+public interface ITrailLineManager
+{
+ IEnumerable Lines { get; }
+
+ ITrailLine CreateTrail(ITrailSettings settings, MapId mapId);
+
+ void Detach(ITrailLineHolder holder);
+
+ void Update(float dt);
+}
diff --git a/Content.Client/White/Trail/Line/Manager/TrailSplineManager.cs b/Content.Client/White/Trail/Line/Manager/TrailSplineManager.cs
new file mode 100644
index 0000000000..f55a5a7660
--- /dev/null
+++ b/Content.Client/White/Trail/Line/Manager/TrailSplineManager.cs
@@ -0,0 +1,64 @@
+using Content.Client.White.Trail.SplineRenderer;
+using Content.Shared.White.Spline;
+using Content.Shared.White.Trail;
+using Robust.Shared.Map;
+
+namespace Content.Client.White.Trail.Line.Manager;
+
+public sealed class TrailSplineManager : ITrailLineManager
+{
+ private readonly LinkedList _lines = new();
+
+ public IEnumerable Lines => _lines;
+
+ public ITrailLine CreateTrail(ITrailSettings settings, MapId mapId)
+ {
+ var tline = new TrailSpline
+ {
+ Attached = true,
+ Settings = settings,
+ MapId = mapId,
+ SplineIterator = Spline.From2DType(settings.SplineIteratorType),
+ GradientIterator = Spline.From4DType(settings.GradientIteratorType),
+ Renderer = TrailSplineRenderer.FromType(settings.SplineRendererType)
+ };
+
+ _lines.AddLast(tline);
+ return tline;
+ }
+
+ public void Detach(ITrailLineHolder holder)
+ {
+ if (holder.TrailLine is TrailSpline trailSpline)
+ {
+ trailSpline.Attached = false;
+ var detachedSettings = new TrailSettings();
+ TrailSettings.Inject(detachedSettings, trailSpline.Settings);
+ trailSpline.Settings = detachedSettings;
+ }
+ }
+
+ public void Update(float dt)
+ {
+ var curNode = _lines.First;
+ while (curNode != null)
+ {
+ var curLine = curNode.Value;
+ curNode = curNode.Next;
+
+ if (!curLine.HasSegments())
+ {
+ if (curLine.Attached)
+ curLine.ResetLifetime();
+ else
+ _lines.Remove(curLine);
+
+ continue;
+ }
+
+ curLine.AddLifetime(dt);
+ curLine.RemoveExpiredSegments();
+ curLine.UpdateSegments(dt);
+ }
+ }
+}
diff --git a/Content.Client/White/Trail/Line/TrailSpline.cs b/Content.Client/White/Trail/Line/TrailSpline.cs
new file mode 100644
index 0000000000..a20e50d294
--- /dev/null
+++ b/Content.Client/White/Trail/Line/TrailSpline.cs
@@ -0,0 +1,183 @@
+using Content.Client.White.Trail.SplineRenderer;
+using Content.Shared.White.Spline;
+using Content.Shared.White.Spline.Linear;
+using Content.Shared.White.Trail;
+using Robust.Client.Graphics;
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using Vector4 = Robust.Shared.Maths.Vector4;
+
+namespace Content.Client.White.Trail.Line;
+
+public sealed class TrailSpline : ITrailLine
+{
+ private static readonly IRobustRandom Random = IoCManager.Resolve();
+
+ [ViewVariables]
+ private readonly LinkedList _segments = new();
+
+ [ViewVariables]
+ private Vector2 _lastCreationPos;
+
+ [ViewVariables]
+ private float _curLifetime;
+
+ [ViewVariables]
+ private Vector2? _virtualSegmentPos;
+
+ [ViewVariables]
+ public MapId MapId { get; set; }
+
+ [ViewVariables]
+ public bool Attached { get; set; }
+
+ [ViewVariables]
+ public ITrailSettings Settings { get; set; } = TrailSettings.Default;
+
+ [ViewVariables]
+ public ISpline SplineIterator { get; set; } = new SplineLinear2D();
+
+ [ViewVariables]
+ public ISpline GradientIterator { get; set; } = new SplineLinear4D();
+
+ [ViewVariables]
+ public ITrailSplineRenderer Renderer { get; set; } = new TrailSplineRendererDebug();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool HasSegments()
+ {
+ return _segments.Count > 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLifetime(float time)
+ {
+ _curLifetime += time;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void ResetLifetime()
+ {
+ _curLifetime = 0f;
+ }
+
+ public void TryCreateSegment((Vector2 WorldPosition, Angle WorldRotation) worldPosRot, MapId mapId)
+ {
+ if (!Attached)
+ return;
+
+ if (mapId != MapId)
+ return;
+
+ if (worldPosRot.WorldPosition == Vector2.Zero)
+ return;
+
+ var pos = worldPosRot.WorldPosition + worldPosRot.WorldRotation.RotateVec(Settings.CreationOffset);
+
+ _lastCreationPos = pos;
+
+ if (_virtualSegmentPos.HasValue)
+ {
+ var vPos = _virtualSegmentPos.Value;
+ if ((vPos - pos).LengthSquared() > Settings.СreationDistanceThresholdSquared)
+ {
+ _segments.AddLast(new TrailSplineSegment()
+ { Position = vPos, ExistTil = _curLifetime + Settings.Lifetime });
+
+ _virtualSegmentPos = null;
+ }
+
+ return;
+ }
+
+ var lastPos = _segments.Last?.Value.Position;
+ if (!lastPos.HasValue || (lastPos.Value - pos).LengthSquared() > Settings.СreationDistanceThresholdSquared)
+ _virtualSegmentPos = pos;
+ }
+
+ public void UpdateSegments(float dt)
+ {
+ var gravity = Settings.Gravity;
+ var maxRandomWalk = Settings.MaxRandomWalk;
+ var lifetime = Settings.Lifetime;
+
+ if (_segments.Last != null)
+ {
+ var i = 0;
+ var positions = new Vector2[_segments.Count + 1];
+ positions[_segments.Count] = _lastCreationPos;
+
+ var curNode = _segments.First;
+ while (curNode != null)
+ {
+ var offset = gravity;
+ var curValue = curNode.Value;
+ if (maxRandomWalk != Vector2.Zero)
+ {
+ positions[i] = curValue.Position;
+ if (curNode.Next != null)
+ {
+ positions[i + 1] = curNode.Next.Value.Position;
+ if (curNode.Next.Next != null)
+ positions[i + 2] = curNode.Next.Next.Value.Position;
+ }
+
+ var effectiveRandomWalk = maxRandomWalk * (curValue.ExistTil - _curLifetime) / lifetime;
+ var gradientNorm = -SplineIterator.SampleVelocity(positions, i).Normalized();
+ offset += gradientNorm * effectiveRandomWalk.Y * Random.NextFloat(-1.0f, 1.0f);
+ var rotated90Degrees = new Vector2(-gradientNorm.Y, gradientNorm.X);
+ offset += rotated90Degrees * effectiveRandomWalk.X * Random.NextFloat(-1.0f, 1.0f);
+ }
+
+ curValue.Position += offset;
+ i++;
+ curNode = curNode.Next;
+ }
+ }
+
+ if (_virtualSegmentPos.HasValue)
+ _virtualSegmentPos = _virtualSegmentPos.Value + gravity;
+
+ if (!Attached)
+ _lastCreationPos += gravity;
+ }
+
+ public void RemoveExpiredSegments()
+ {
+ while (_segments.First?.Value.ExistTil < _curLifetime)
+ {
+ _segments.RemoveFirst();
+ }
+ }
+
+ public void Render(DrawingHandleWorld handle, Texture? texture)
+ {
+ if (_segments.Last == null)
+ return;
+
+ var arrSize = _segments.Count + 1;
+ var paPositions = new Vector2[arrSize];
+ var paLifetimes = new float[arrSize];
+ paPositions[0] = _lastCreationPos;
+ paLifetimes[0] = 1f;
+
+ var reversedIndexedSegments = _segments.Reverse().Select((x, i) => (x, i + 1));
+ foreach (var (x, i) in reversedIndexedSegments)
+ {
+ paPositions[i] = x.Position;
+ paLifetimes[i] = (x.ExistTil - _curLifetime) / Settings.Lifetime;
+ }
+
+ Renderer.Render(handle, texture, SplineIterator, GradientIterator, Settings, paPositions, paLifetimes);
+ }
+
+ private sealed class TrailSplineSegment
+ {
+ public Vector2 Position { get; set; }
+
+ public float ExistTil { get; init; }
+ }
+}
diff --git a/Content.Client/White/Trail/SplineRenderer/ITrailSplineRenderer.cs b/Content.Client/White/Trail/SplineRenderer/ITrailSplineRenderer.cs
new file mode 100644
index 0000000000..460a3b43db
--- /dev/null
+++ b/Content.Client/White/Trail/SplineRenderer/ITrailSplineRenderer.cs
@@ -0,0 +1,20 @@
+using System.Numerics;
+using Content.Shared.White.Spline;
+using Content.Shared.White.Trail;
+using Robust.Client.Graphics;
+using Vector4 = Robust.Shared.Maths.Vector4;
+
+namespace Content.Client.White.Trail.SplineRenderer;
+
+public interface ITrailSplineRenderer
+{
+ void Render(
+ DrawingHandleWorld handle,
+ Texture? texture,
+ ISpline splineIterator,
+ ISpline gradientIterator,
+ ITrailSettings settings,
+ Vector2[] paPositions,
+ float[] paLifetimes
+ );
+}
diff --git a/Content.Client/White/Trail/SplineRenderer/TrailSplineRenderer.cs b/Content.Client/White/Trail/SplineRenderer/TrailSplineRenderer.cs
new file mode 100644
index 0000000000..53b0a6e3b0
--- /dev/null
+++ b/Content.Client/White/Trail/SplineRenderer/TrailSplineRenderer.cs
@@ -0,0 +1,17 @@
+using Content.Shared.White.Trail;
+
+namespace Content.Client.White.Trail.SplineRenderer;
+
+public static class TrailSplineRenderer
+{
+ public static ITrailSplineRenderer FromType(TrailSplineRendererType type)
+ {
+ return type switch
+ {
+ TrailSplineRendererType.Continuous => new TrailSplineRendererContinuous(),
+ TrailSplineRendererType.Point => new TrailSplineRendererPoint(),
+ TrailSplineRendererType.Debug => new TrailSplineRendererDebug(),
+ _ => throw new NotImplementedException()
+ };
+ }
+}
diff --git a/Content.Client/White/Trail/SplineRenderer/TrailSplineRendererContinuous.cs b/Content.Client/White/Trail/SplineRenderer/TrailSplineRendererContinuous.cs
new file mode 100644
index 0000000000..a7c74211bc
--- /dev/null
+++ b/Content.Client/White/Trail/SplineRenderer/TrailSplineRendererContinuous.cs
@@ -0,0 +1,90 @@
+using Content.Shared.White.Spline;
+using Content.Shared.White.Trail;
+using Robust.Client.Graphics;
+using System.Linq;
+using System.Numerics;
+using Vector4 = Robust.Shared.Maths.Vector4;
+
+namespace Content.Client.White.Trail.SplineRenderer;
+
+public sealed class TrailSplineRendererContinuous : ITrailSplineRenderer
+{
+ public void Render(
+ DrawingHandleWorld handle,
+ Texture? texture,
+ ISpline splineIterator,
+ ISpline gradientIterator,
+ ITrailSettings settings,
+ Vector2[] paPositions,
+ float[] paLifetimes
+ )
+ {
+ float[] splinePointParams;
+ if (settings.LengthStep == 0f)
+ {
+ splinePointParams = Enumerable.Range(0, paPositions.Length - 1).Select(x => (float) x).ToArray();
+ }
+ else
+ {
+ splinePointParams = splineIterator
+ .IteratePointParamsByLength(paPositions, Math.Max(settings.LengthStep, 0.1f)).ToArray();
+ }
+
+ var gradientControlGroups = gradientIterator.GetControlGroupAmount(settings.Gradient.Length);
+ var colorToPointMul = 0f;
+ if (gradientControlGroups > 0)
+ colorToPointMul = gradientControlGroups / splineIterator.GetControlGroupAmount(paPositions.Length);
+
+ (Vector2, Vector2)? prevPoints = null;
+ foreach (var u in splinePointParams)
+ {
+ var (position, velocity) = splineIterator.SamplePositionVelocity(paPositions, u);
+
+ var offset = new Vector2(-velocity.Y, velocity.X).Normalized() *
+ settings.Scale.X; // 90-degree anticlockwise rotation
+
+ var curPoints = (position - offset, position + offset);
+
+ if (prevPoints.HasValue)
+ {
+ var colorVec = Vector4.One;
+ if (settings.Gradient != null && settings.Gradient.Length > 0)
+ {
+ if (gradientControlGroups > 0)
+ colorVec = gradientIterator.SamplePosition(settings.Gradient, u * colorToPointMul);
+ else
+ colorVec = settings.Gradient[0];
+ }
+
+ if (texture != null)
+ {
+ var verts = new DrawVertexUV2D[]
+ {
+ new(curPoints.Item1, Vector2.Zero),
+ new(curPoints.Item2, Vector2.UnitY),
+ new(prevPoints.Value.Item2, Vector2.One),
+ new(prevPoints.Value.Item1, Vector2.UnitX),
+ };
+
+ handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, texture, verts,
+ new Color(colorVec.X, colorVec.Y, colorVec.Z, colorVec.W));
+ }
+ else
+ {
+ var verts = new[]
+ {
+ curPoints.Item1,
+ curPoints.Item2,
+ prevPoints.Value.Item2,
+ prevPoints.Value.Item1,
+ };
+
+ handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts,
+ new Color(colorVec.X, colorVec.Y, colorVec.Z, colorVec.W));
+ }
+ }
+
+ prevPoints = curPoints;
+ }
+ }
+}
diff --git a/Content.Client/White/Trail/SplineRenderer/TrailSplineRendererDebug.cs b/Content.Client/White/Trail/SplineRenderer/TrailSplineRendererDebug.cs
new file mode 100644
index 0000000000..503b3e5d12
--- /dev/null
+++ b/Content.Client/White/Trail/SplineRenderer/TrailSplineRendererDebug.cs
@@ -0,0 +1,54 @@
+using Content.Shared.White.Spline;
+using Content.Shared.White.Trail;
+using Robust.Client.Graphics;
+using System.Linq;
+using System.Numerics;
+using Vector4 = Robust.Shared.Maths.Vector4;
+
+namespace Content.Client.White.Trail.SplineRenderer;
+
+public sealed class TrailSplineRendererDebug : ITrailSplineRenderer
+{
+ public void Render(
+ DrawingHandleWorld handle,
+ Texture? texture,
+ ISpline splineIterator,
+ ISpline gradientIterator,
+ ITrailSettings settings,
+ Vector2[] paPositions,
+ float[] paLifetimes
+ )
+ {
+ float[] splinePointParams;
+ if (settings.LengthStep == 0f)
+ {
+ splinePointParams = Enumerable.Range(0, paPositions.Length - 1).Select(x => (float) x).ToArray();
+ }
+ else
+ {
+ splinePointParams = splineIterator
+ .IteratePointParamsByLength(paPositions, Math.Max(settings.LengthStep, 0.1f)).ToArray();
+ }
+
+ Vector2? prevPosControlPoint = null;
+ foreach (var item in paPositions)
+ {
+ if (prevPosControlPoint.HasValue)
+ handle.DrawLine(item, prevPosControlPoint.Value, Color.Blue);
+
+ prevPosControlPoint = item;
+ }
+
+ Vector2? prevPosSplinePoint = null;
+ foreach (var u in splinePointParams)
+ {
+ var (position, velocity) = splineIterator.SamplePositionVelocity(paPositions, u);
+ if (prevPosSplinePoint.HasValue)
+ handle.DrawLine(position, prevPosSplinePoint.Value, Color.Red);
+
+ handle.DrawLine(position, position + velocity, Color.White);
+ handle.DrawCircle(position, 0.03f, new Color(0, 255, 0));
+ prevPosSplinePoint = position;
+ }
+ }
+}
diff --git a/Content.Client/White/Trail/SplineRenderer/TrailSplineRendererPoint.cs b/Content.Client/White/Trail/SplineRenderer/TrailSplineRendererPoint.cs
new file mode 100644
index 0000000000..3d84c42239
--- /dev/null
+++ b/Content.Client/White/Trail/SplineRenderer/TrailSplineRendererPoint.cs
@@ -0,0 +1,58 @@
+using Content.Shared.White.Spline;
+using Content.Shared.White.Trail;
+using Robust.Client.Graphics;
+using System.Linq;
+using System.Numerics;
+using Vector4 = Robust.Shared.Maths.Vector4;
+
+namespace Content.Client.White.Trail.SplineRenderer;
+
+public sealed class TrailSplineRendererPoint : ITrailSplineRenderer
+{
+ public void Render(
+ DrawingHandleWorld handle,
+ Texture? texture,
+ ISpline splineIterator,
+ ISpline gradientIterator,
+ ITrailSettings settings,
+ Vector2[] paPositions,
+ float[] paLifetimes
+ )
+ {
+ if (texture == null)
+ return;
+
+ float[] splinePointParams;
+ if (settings.LengthStep == 0f)
+ {
+ splinePointParams = Enumerable.Range(0, paPositions.Length - 1).Select(x => (float) x).ToArray();
+ }
+ else
+ {
+ splinePointParams = splineIterator
+ .IteratePointParamsByLength(paPositions, Math.Max(settings.LengthStep, 0.1f)).ToArray();
+ }
+
+ var gradientControlGroups = gradientIterator.GetControlGroupAmount(settings.Gradient.Length);
+ var colorToPointMul = 0f;
+ if (gradientControlGroups > 0)
+ colorToPointMul = gradientControlGroups / gradientIterator.GetControlGroupAmount(paPositions.Length);
+
+ foreach (var u in splinePointParams)
+ {
+ var (position, velocity) = splineIterator.SamplePositionVelocity(paPositions, u);
+
+ var colorVec = Vector4.One;
+ if (settings.Gradient != null && settings.Gradient.Length > 0)
+ {
+ colorVec = gradientControlGroups > 0
+ ? gradientIterator.SamplePosition(settings.Gradient, u * colorToPointMul)
+ : settings.Gradient[0];
+ }
+
+ var quad = Box2.FromDimensions(position, texture.Size * settings.Scale / EyeManager.PixelsPerMeter);
+ handle.DrawTextureRect(texture, new Box2Rotated(quad, velocity.ToAngle(), quad.Center),
+ new Color(colorVec.X, colorVec.Y, colorVec.Z, colorVec.W));
+ }
+ }
+}
diff --git a/Content.Client/White/Trail/TrailComponent.cs b/Content.Client/White/Trail/TrailComponent.cs
new file mode 100644
index 0000000000..31ae97cbcc
--- /dev/null
+++ b/Content.Client/White/Trail/TrailComponent.cs
@@ -0,0 +1,56 @@
+using Content.Client.White.Trail.Line;
+using Content.Client.White.Trail.Line.Manager;
+using Content.Client.White.Trail.SplineRenderer;
+using Content.Shared.White.Spline;
+using Content.Shared.White.Trail;
+
+namespace Content.Client.White.Trail;
+
+[RegisterComponent]
+public sealed partial class TrailComponent : SharedTrailComponent, ITrailLineHolder
+{
+ [ViewVariables]
+ public ITrailLine? TrailLine { get; set; }
+
+ public override Spline2DType SplineIteratorType
+ {
+ get => base.SplineIteratorType;
+ set
+ {
+ if (base.SplineIteratorType == value)
+ return;
+
+ base.SplineIteratorType = value;
+ if (TrailLine is TrailSpline trailSpline)
+ trailSpline.SplineIterator = Spline.From2DType(value);
+ }
+ }
+
+ public override Spline4DType GradientIteratorType
+ {
+ get => base.GradientIteratorType;
+ set
+ {
+ if (base.GradientIteratorType == value)
+ return;
+
+ base.GradientIteratorType = value;
+ if (TrailLine is TrailSpline trailSpline)
+ trailSpline.GradientIterator = Spline.From4DType(value);
+ }
+ }
+
+ public override TrailSplineRendererType SplineRendererType
+ {
+ get => base.SplineRendererType;
+ set
+ {
+ if (base.SplineRendererType == value)
+ return;
+
+ base.SplineRendererType = value;
+ if (TrailLine is TrailSpline trailSpline)
+ trailSpline.Renderer = TrailSplineRenderer.FromType(value);
+ }
+ }
+}
diff --git a/Content.Client/White/Trail/TrailOverlay.cs b/Content.Client/White/Trail/TrailOverlay.cs
new file mode 100644
index 0000000000..07b1380d74
--- /dev/null
+++ b/Content.Client/White/Trail/TrailOverlay.cs
@@ -0,0 +1,69 @@
+using Content.Client.White.Trail.Line.Manager;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.White.Trail;
+
+public sealed class TrailOverlay : Overlay
+{
+ private readonly IPrototypeManager _protoManager;
+ private readonly IResourceCache _cache;
+ private readonly ITrailLineManager _lineManager;
+
+ private readonly Dictionary _shaderDict;
+ private readonly Dictionary _textureDict;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities;
+
+ public TrailOverlay(
+ IPrototypeManager protoManager,
+ IResourceCache cache,
+ ITrailLineManager lineManager
+ )
+ {
+ _protoManager = protoManager;
+ _cache = cache;
+ _lineManager = lineManager;
+
+ _shaderDict = new Dictionary();
+ _textureDict = new Dictionary();
+
+ ZIndex = (int) Shared.DrawDepth.DrawDepth.Effects;
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ var handle = args.WorldHandle;
+ foreach (var item in _lineManager.Lines)
+ {
+ item.Render(handle, GetCachedTexture(item.Settings.TexurePath ?? ""));
+ }
+ }
+
+ //влепить на ети два метода мемори кеш со слайдинг експирейшоном вместо дикта если проблемы будут
+ private ShaderInstance? GetCachedShader(string id)
+ {
+ if (_shaderDict.TryGetValue(id, out var shader))
+ return shader;
+
+ if (_protoManager.TryIndex(id, out var shaderRes))
+ shader = shaderRes?.InstanceUnique();
+
+ _shaderDict.Add(id, shader);
+ return shader;
+ }
+
+ private Texture? GetCachedTexture(string path)
+ {
+ if (_textureDict.TryGetValue(path, out var texture))
+ return texture;
+
+ if (_cache.TryGetResource(path, out var texRes))
+ texture = texRes;
+
+ _textureDict.Add(path, texture);
+ return texture;
+ }
+}
diff --git a/Content.Client/White/Trail/TrailSystem.cs b/Content.Client/White/Trail/TrailSystem.cs
new file mode 100644
index 0000000000..6ad6d3b07a
--- /dev/null
+++ b/Content.Client/White/Trail/TrailSystem.cs
@@ -0,0 +1,76 @@
+using Content.Client.White.Trail.Line.Manager;
+using Content.Shared.White.Trail;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Client.White.Trail;
+
+public sealed class TrailSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly ITrailLineManager _lineManager = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ IoCManager.Resolve().AddOverlay(
+ new TrailOverlay(
+ IoCManager.Resolve(),
+ IoCManager.Resolve(),
+ _lineManager
+ ));
+
+ SubscribeLocalEvent(OnTrailMove);
+ SubscribeLocalEvent(OnTrailRemove);
+ SubscribeLocalEvent(OnHandleState);
+ }
+
+ private void OnHandleState(EntityUid uid, TrailComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not TrailComponentState state)
+ return;
+
+ TrailSettings.Inject(component, state.Settings);
+ }
+
+ private void OnTrailRemove(EntityUid uid, TrailComponent comp, ComponentRemove args)
+ {
+ _lineManager.Detach(comp);
+ }
+
+ private void OnTrailMove(EntityUid uid, TrailComponent comp, ref MoveEvent args)
+ {
+ if (comp.СreationMethod != SegmentCreationMethod.OnMove || _gameTiming.InPrediction)
+ return;
+
+ TryCreateSegment(comp, args.Component);
+ }
+
+ private void TryCreateSegment(TrailComponent comp, TransformComponent xform)
+ {
+ if (xform.MapID == MapId.Nullspace)
+ return;
+
+ comp.TrailLine ??= _lineManager.CreateTrail(comp, xform.MapID);
+ comp.TrailLine.TryCreateSegment(_transformSystem.GetWorldPositionRotation(xform), xform.MapID);
+ }
+
+ public override void FrameUpdate(float frameTime)
+ {
+ base.FrameUpdate(frameTime);
+
+ _lineManager.Update(frameTime);
+
+ foreach (var (comp, xform) in EntityQuery())
+ {
+ if (comp.СreationMethod == SegmentCreationMethod.OnFrameUpdate)
+ TryCreateSegment(comp, xform);
+ }
+ }
+}
diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
index 35256ad976..5991299db0 100644
--- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
+++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
@@ -1,13 +1,10 @@
using Content.Server.Power.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
-using Content.Shared.FixedPoint;
using Content.Shared.Interaction.Events;
-using Content.Shared.Projectiles;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Server.Audio;
-using Robust.Shared.Prototypes;
namespace Content.Server.Weapons.Ranged.Systems;
@@ -41,28 +38,30 @@ public sealed partial class GunSystem
if (!TryComp(uid, out var gun))
return;
- if (component.CurrentMode == EnergyModes.Stun)
+ switch (component.CurrentMode)
{
- component.InStun = false;
- gun.SoundGunshot = component.HitscanSound;
- component.CurrentMode = EnergyModes.Laser;
- component.FireCost = component.HitscanFireCost;
- _audio.PlayPvs(component.ToggleSound, args.User);
- }
- else if (component.CurrentMode == EnergyModes.Laser)
- {
- component.InStun = true;
- gun.SoundGunshot = component.ProjSound;
- component.CurrentMode = EnergyModes.Stun;
- component.FireCost = component.ProjFireCost;
- _audio.PlayPvs(component.ToggleSound, args.User);
+ case EnergyModes.Stun:
+ component.InStun = false;
+ component.CurrentMode = EnergyModes.Laser;
+ component.FireCost = component.LaserFireCost;
+ gun.SoundGunshot = component.LaserSound;
+ gun.ProjectileSpeed = component.LaserProjectileSpeed;
+ _audio.PlayPvs(component.ToggleSound, args.User);
+ break;
+ case EnergyModes.Laser:
+ component.InStun = true;
+ component.CurrentMode = EnergyModes.Stun;
+ component.FireCost = component.StunFireCost;
+ gun.SoundGunshot = component.StunSound;
+ gun.ProjectileSpeed = component.StunProjectileSpeed;
+ _audio.PlayPvs(component.ToggleSound, args.User);
+ break;
}
+
UpdateShots(uid, component);
UpdateTwoModeAppearance(uid, component);
UpdateBatteryAppearance(uid, component);
UpdateAmmoCount(uid);
- Dirty(gun);
- Dirty(component);
}
private void OnBatteryStartup(EntityUid uid, BatteryAmmoProviderComponent component, ComponentStartup args)
@@ -70,7 +69,10 @@ public sealed partial class GunSystem
UpdateShots(uid, component);
}
- private void OnBatteryChargeChange(EntityUid uid, BatteryAmmoProviderComponent component, ref ChargeChangedEvent args)
+ private void OnBatteryChargeChange(
+ EntityUid uid,
+ BatteryAmmoProviderComponent component,
+ ref ChargeChangedEvent args)
{
UpdateShots(uid, component, args.Charge, args.MaxCharge);
}
@@ -90,7 +92,7 @@ public sealed partial class GunSystem
if (component.Shots != shots || component.Capacity != maxShots)
{
- Dirty(component);
+ Dirty(uid, component);
}
component.Shots = shots;
@@ -98,78 +100,41 @@ public sealed partial class GunSystem
UpdateBatteryAppearance(uid, component);
}
- private void OnBatteryDamageExamine(EntityUid uid, BatteryAmmoProviderComponent component, ref DamageExamineEvent args)
+ private void OnBatteryDamageExamine(
+ EntityUid uid,
+ BatteryAmmoProviderComponent component,
+ ref DamageExamineEvent args)
{
var damageSpec = GetDamage(component);
if (damageSpec == null)
return;
- string? damageType;
- switch (component)
+ var damageType = component switch
{
- case HitscanBatteryAmmoProviderComponent:
- damageType = Loc.GetString("damage-hitscan");
- break;
- case ProjectileBatteryAmmoProviderComponent:
- damageType = Loc.GetString("damage-projectile");
- break;
- case TwoModeEnergyAmmoProviderComponent twoMode:
- if (twoMode.CurrentMode == EnergyModes.Stun)
- damageType = Loc.GetString("damage-projectile");
- else
- damageType = Loc.GetString("damage-hitscan");
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
+ HitscanBatteryAmmoProviderComponent => Loc.GetString("damage-hitscan"),
+ ProjectileBatteryAmmoProviderComponent => Loc.GetString("damage-projectile"),
+ TwoModeEnergyAmmoProviderComponent twoMode => Loc.GetString(twoMode.CurrentMode == EnergyModes.Stun
+ ? "damage-projectile"
+ : "damage-hitscan"),
+ _ => throw new ArgumentOutOfRangeException()
+ };
_damageExamine.AddDamageExamine(args.Message, damageSpec, damageType);
}
private DamageSpecifier? GetDamage(BatteryAmmoProviderComponent component)
{
- if (component is ProjectileBatteryAmmoProviderComponent battery)
+ return component switch
{
- if (ProtoManager.Index(battery.Prototype).Components
- .TryGetValue(_factory.GetComponentName(typeof(ProjectileComponent)), out var projectile))
- {
- var p = (ProjectileComponent) projectile.Component;
-
- if (!p.Damage.Empty)
- {
- return p.Damage;
- }
- }
-
- return null;
- }
-
- if (component is HitscanBatteryAmmoProviderComponent hitscan)
- {
- return ProtoManager.Index(hitscan.Prototype).Damage;
- }
-
- if (component is TwoModeEnergyAmmoProviderComponent twoMode)
- {
- if (twoMode.CurrentMode == EnergyModes.Stun)
- {
- if (ProtoManager.Index(twoMode.ProjectilePrototype).Components
- .TryGetValue(_factory.GetComponentName(typeof(ProjectileComponent)), out var projectile))
- {
- var p = (ProjectileComponent) projectile.Component;
-
- if (p.Damage.Total > FixedPoint2.Zero)
- {
- return p.Damage;
- }
- }
-
- return null;
- }
- return ProtoManager.Index(twoMode.HitscanPrototype).Damage;
- }
- return null;
+ HitscanBatteryAmmoProviderComponent hitscan =>
+ ProtoManager.Index(hitscan.Prototype).Damage,
+ ProjectileBatteryAmmoProviderComponent battery => GetProjectileDamage(battery.Prototype),
+ TwoModeEnergyAmmoProviderComponent twoMode => GetProjectileDamage(twoMode.CurrentMode == EnergyModes.Laser
+ ? twoMode.LaserPrototype
+ : twoMode.StunPrototype),
+ _ => null
+ };
}
protected override void TakeCharge(EntityUid uid, BatteryAmmoProviderComponent component)
diff --git a/Content.Server/White/Trail/TrailComponent.cs b/Content.Server/White/Trail/TrailComponent.cs
new file mode 100644
index 0000000000..02dfbc2914
--- /dev/null
+++ b/Content.Server/White/Trail/TrailComponent.cs
@@ -0,0 +1,51 @@
+using System.Numerics;
+using Content.Shared.White.Spline;
+using Content.Shared.White.Trail;
+using Vector4 = Robust.Shared.Maths.Vector4;
+
+namespace Content.Server.White.Trail;
+
+[RegisterComponent]
+public sealed partial class TrailComponent : SharedTrailComponent
+{
+ public TrailComponent()
+ {
+ var defaultTrail = TrailSettings.Default;
+ Scale = defaultTrail.Scale;
+ СreationDistanceThresholdSquared = defaultTrail.СreationDistanceThresholdSquared;
+ СreationMethod = defaultTrail.СreationMethod;
+ CreationOffset = defaultTrail.CreationOffset;
+ Gravity = defaultTrail.Gravity;
+ MaxRandomWalk = defaultTrail.MaxRandomWalk;
+ Lifetime = defaultTrail.Lifetime;
+ TexurePath = defaultTrail.TexurePath;
+ Gradient = defaultTrail.Gradient;
+ GradientIteratorType = defaultTrail.GradientIteratorType;
+ }
+
+ public override Vector2 Gravity { get; set; }
+
+ public override float Lifetime { get; set; }
+
+ public override Vector2 MaxRandomWalk { get; set; }
+
+ public override Vector2 Scale { get; set; }
+
+ public override string? TexurePath { get; set; }
+
+ public override Vector2 CreationOffset { get; set; }
+
+ public override float СreationDistanceThresholdSquared { get; set; }
+
+ public override SegmentCreationMethod СreationMethod { get; set; }
+
+ public override Vector4[] Gradient { get; set; }
+
+ public override float LengthStep { get; set; }
+
+ public override Spline2DType SplineIteratorType { get; set; }
+
+ public override TrailSplineRendererType SplineRendererType { get; set; }
+
+ public override Spline4DType GradientIteratorType { get; set; }
+}
diff --git a/Content.Server/White/Trail/TrailSystem.cs b/Content.Server/White/Trail/TrailSystem.cs
new file mode 100644
index 0000000000..d6b6d04204
--- /dev/null
+++ b/Content.Server/White/Trail/TrailSystem.cs
@@ -0,0 +1,21 @@
+using Content.Shared.White.Trail;
+using Robust.Shared.GameStates;
+
+namespace Content.Server.White.Trail;
+
+public sealed class TrailSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetState);
+ }
+
+ private void OnGetState(EntityUid uid, TrailComponent component, ref ComponentGetState args)
+ {
+ var settings = new TrailSettings();
+ TrailSettings.Inject(settings, component);
+ args.State = new TrailComponentState(settings);
+ }
+}
diff --git a/Content.Shared/Weapons/Ranged/Components/TwoModeEnergyAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/TwoModeEnergyAmmoProviderComponent.cs
index 05756e2134..bc03fc4aba 100644
--- a/Content.Shared/Weapons/Ranged/Components/TwoModeEnergyAmmoProviderComponent.cs
+++ b/Content.Shared/Weapons/Ranged/Components/TwoModeEnergyAmmoProviderComponent.cs
@@ -9,27 +9,33 @@ namespace Content.Shared.Weapons.Ranged.Components;
public sealed partial class TwoModeEnergyAmmoProviderComponent : BatteryAmmoProviderComponent
{
[ViewVariables(VVAccess.ReadOnly),
- DataField("projProto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string ProjectilePrototype = default!;
+ DataField("stunPrototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string StunPrototype = default!;
[ViewVariables(VVAccess.ReadOnly),
- DataField("hitscanProto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string HitscanPrototype = default!;
+ DataField("laserPrototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string LaserPrototype = default!;
- [ViewVariables(VVAccess.ReadOnly), DataField("projFireCost")]
- public float ProjFireCost = 50;
+ [ViewVariables(VVAccess.ReadOnly), DataField("stunFireCost")]
+ public float StunFireCost = 142;
- [ViewVariables(VVAccess.ReadOnly), DataField("hitscanFireCost")]
- public float HitscanFireCost = 100;
+ [ViewVariables(VVAccess.ReadOnly), DataField("laserFireCost")]
+ public float LaserFireCost = 65;
+
+ [ViewVariables(VVAccess.ReadOnly), DataField("stunProjectileSpeed")]
+ public float StunProjectileSpeed = 12;
+
+ [ViewVariables(VVAccess.ReadOnly), DataField("laserProjectileSpeed")]
+ public float LaserProjectileSpeed = 25;
[ViewVariables(VVAccess.ReadOnly), DataField("currentMode")]
public EnergyModes CurrentMode { get; set; } = EnergyModes.Stun;
- [ViewVariables(VVAccess.ReadOnly), DataField("projSound")]
- public SoundSpecifier? ProjSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/taser2.ogg");
+ [ViewVariables(VVAccess.ReadOnly), DataField("stunSound")]
+ public SoundSpecifier? StunSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/taser2.ogg");
- [ViewVariables(VVAccess.ReadOnly), DataField("hitscanSound")]
- public SoundSpecifier? HitscanSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/laser_cannon.ogg");
+ [ViewVariables(VVAccess.ReadOnly), DataField("laserSound")]
+ public SoundSpecifier? LaserSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/laser_cannon.ogg");
public SoundSpecifier? ToggleSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/egun_toggle.ogg");
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs
index 6d9c4eaf12..7be1a36b23 100644
--- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs
@@ -38,15 +38,18 @@ public abstract partial class SharedGunSystem
SubscribeLocalEvent(OnBatteryExamine);
}
-
private void OnTwoModeInit(EntityUid uid, TwoModeEnergyAmmoProviderComponent component, ComponentInit args)
{
- if (!Timing.IsFirstTimePredicted || !TryComp(component.Owner, out var appearance)) return;
+ if (!Timing.IsFirstTimePredicted || !TryComp(component.Owner, out var appearance))
+ return;
Appearance.SetData(appearance.Owner, AmmoVisuals.InStun, component.InStun, appearance);
}
- private void OnBatteryTwoModeHandleState(EntityUid uid, TwoModeEnergyAmmoProviderComponent component, ref ComponentHandleState args)
+ private void OnBatteryTwoModeHandleState(
+ EntityUid uid,
+ TwoModeEnergyAmmoProviderComponent component,
+ ref ComponentHandleState args)
{
if (args.Current is not TwoModeComponentState state)
return;
@@ -58,7 +61,10 @@ public abstract partial class SharedGunSystem
component.InStun = state.InStun;
}
- private void OnBatteryTwoModeGetState(EntityUid uid, TwoModeEnergyAmmoProviderComponent component, ref ComponentGetState args)
+ private void OnBatteryTwoModeGetState(
+ EntityUid uid,
+ TwoModeEnergyAmmoProviderComponent component,
+ ref ComponentGetState args)
{
args.State = new TwoModeComponentState()
{
@@ -74,19 +80,20 @@ public abstract partial class SharedGunSystem
{
if (!TryComp(uid, out var appearance))
return;
+
if (!TryComp(uid, out var item))
return;
- if (component.InStun)
- _item.SetHeldPrefix(uid, null, false, item);
- else
- _item.SetHeldPrefix(uid, "laser", false, item);
-
+ _item.SetHeldPrefix(uid, component.InStun ? null : "laser", false, item);
Appearance.SetData(uid, AmmoVisuals.InStun, component.InStun, appearance);
+ Dirty(uid, component);
}
- private void OnBatteryHandleState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentHandleState args)
+ private void OnBatteryHandleState(
+ EntityUid uid,
+ BatteryAmmoProviderComponent component,
+ ref ComponentHandleState args)
{
if (args.Current is not BatteryAmmoProviderComponentState state)
return;
@@ -139,7 +146,7 @@ public abstract partial class SharedGunSystem
///
/// Update the battery (server-only) whenever fired.
///
- protected virtual void TakeCharge(EntityUid uid, BatteryAmmoProviderComponent component) {}
+ protected virtual void TakeCharge(EntityUid uid, BatteryAmmoProviderComponent component) { }
protected void UpdateBatteryAppearance(EntityUid uid, BatteryAmmoProviderComponent component)
{
@@ -151,7 +158,9 @@ public abstract partial class SharedGunSystem
Appearance.SetData(uid, AmmoVisuals.AmmoMax, component.Capacity, appearance);
}
- private (EntityUid? Entity, IShootable) GetShootable(BatteryAmmoProviderComponent component, EntityCoordinates coordinates)
+ private (EntityUid? Entity, IShootable) GetShootable(
+ BatteryAmmoProviderComponent component,
+ EntityCoordinates coordinates)
{
switch (component)
{
@@ -161,12 +170,13 @@ public abstract partial class SharedGunSystem
case HitscanBatteryAmmoProviderComponent hitscan:
return (null, ProtoManager.Index(hitscan.Prototype));
case TwoModeEnergyAmmoProviderComponent twoMode:
- if (twoMode.CurrentMode == EnergyModes.Stun)
- {
- var projEnt = Spawn(twoMode.ProjectilePrototype, coordinates);
- return (projEnt, EnsureComp(projEnt));
- }
- return (null, ProtoManager.Index(twoMode.HitscanPrototype));
+ var projEntity =
+ Spawn(twoMode.CurrentMode == EnergyModes.Stun
+ ? twoMode.StunPrototype
+ : twoMode.LaserPrototype,
+ coordinates);
+
+ return (projEntity, EnsureComp(projEntity));
default:
throw new ArgumentOutOfRangeException();
}
@@ -184,6 +194,7 @@ public abstract partial class SharedGunSystem
public sealed class TwoModeComponentState : ComponentState
{
public EnergyModes CurrentMode { get; init; }
+
public int Shots;
public int MaxShots;
public float FireCost;
diff --git a/Content.Shared/White/Spline/CatmullRom/SplineCatmullRom.cs b/Content.Shared/White/Spline/CatmullRom/SplineCatmullRom.cs
new file mode 100644
index 0000000000..2f610f6cef
--- /dev/null
+++ b/Content.Shared/White/Spline/CatmullRom/SplineCatmullRom.cs
@@ -0,0 +1,85 @@
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Content.Shared.White.Spline.CatmullRom;
+
+public abstract class SplineCatmullRom : Spline
+{
+ protected const int LookupPrecision = 100;
+
+ protected static readonly (float c0, float c1, float c2, float c3)[] PositionCoefficientLookup
+ = Enumerable.Range(0, LookupPrecision + 1)
+ .Select(x => CalculateCoefficientsPosition((float) x / LookupPrecision)).ToArray();
+
+ protected static readonly (float c0, float c1, float c2, float c3)[] GradientCoefficientLookup
+ = Enumerable.Range(0, LookupPrecision + 1)
+ .Select(x => CalculateCoefficientsTangent((float) x / LookupPrecision)).ToArray();
+
+ protected static (float c0, float c1, float c2, float c3) CalculateCoefficientsPosition(float t)
+ {
+ var tt = t * t;
+ var ttt = tt * t;
+ return (
+ -ttt + 2.0f * tt - t,
+ 3.0f * ttt - 5.0f * tt + 2.0f,
+ -3.0f * ttt + 4.0f * tt + t,
+ ttt - tt
+ );
+ }
+
+ protected static (float c0, float c1, float c2, float c3) CalculateCoefficientsTangent(float t)
+ {
+ var tt = t * t;
+ return (
+ -3.0f * tt + 4.0f * t - 1,
+ 9.0f * tt - 10.0f * t,
+ -9.0f * tt + 8.0f * t + 1.0f,
+ 3.0f * tt - 2.0f * t
+ );
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetLookupIndex(float t)
+ {
+ return (int) (t * LookupPrecision);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override T SamplePosition(ReadOnlySpan controlPoints, float u)
+ {
+ return CalculateCatmullRom(GetCurrentControlPoints(controlPoints, (int) u),
+ PositionCoefficientLookup[GetLookupIndex(u % 1)]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override T SampleVelocity(ReadOnlySpan controlPoints, float u)
+ {
+ return CalculateCatmullRom(GetCurrentControlPoints(controlPoints, (int) u),
+ GradientCoefficientLookup[GetLookupIndex(u % 1)]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override (T Position, T Velocity) SamplePositionVelocity(ReadOnlySpan controlPoints, float u)
+ {
+ var lookupIndex = GetLookupIndex(u % 1);
+ var currentControlPoints = GetCurrentControlPoints(controlPoints, (int) u);
+ return (
+ CalculateCatmullRom(currentControlPoints, PositionCoefficientLookup[lookupIndex]),
+ CalculateCatmullRom(currentControlPoints, GradientCoefficientLookup[lookupIndex])
+ );
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected virtual (T p0, T p1, T p2, T p3) GetCurrentControlPoints(ReadOnlySpan controlPoints, int u)
+ {
+ var p1 = controlPoints[u];
+ var p2 = controlPoints[u + 1];
+ var p0 = u == 0 ? Add(p1, Subtract(p1, p2)) : controlPoints[u - 1];
+ var p3 = u + 2 == controlPoints.Length ? Add(p2, Subtract(p2, p2)) : controlPoints[u + 2];
+ return (p0, p1, p2, p3);
+ }
+
+ protected abstract T CalculateCatmullRom(
+ (T p0, T p1, T p2, T p3) points,
+ (float c0, float c1, float c2, float c3) coeffs);
+}
diff --git a/Content.Shared/White/Spline/CatmullRom/SplineCatmullRom2D.cs b/Content.Shared/White/Spline/CatmullRom/SplineCatmullRom2D.cs
new file mode 100644
index 0000000000..eaaa669027
--- /dev/null
+++ b/Content.Shared/White/Spline/CatmullRom/SplineCatmullRom2D.cs
@@ -0,0 +1,38 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace Content.Shared.White.Spline.CatmullRom;
+
+public sealed class SplineCatmullRom2D : SplineCatmullRom
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector2 Add(Vector2 op1, Vector2 op2)
+ {
+ return op1 + op2;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector2 Subtract(Vector2 op1, Vector2 op2)
+ {
+ return op1 - op2;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override float Magnitude(Vector2 op1)
+ {
+ return op1.Length();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector2 CalculateCatmullRom(
+ (Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) points,
+ (float c0, float c1, float c2, float c3) coeffs)
+ {
+ return new Vector2(
+ 0.5f * (points.p0.X * coeffs.c0 + points.p1.X * coeffs.c1 + points.p2.X * coeffs.c2 +
+ points.p3.X * coeffs.c3),
+ 0.5f * (points.p0.Y * coeffs.c0 + points.p1.Y * coeffs.c1 + points.p2.Y * coeffs.c2 +
+ points.p3.Y * coeffs.c3)
+ );
+ }
+}
diff --git a/Content.Shared/White/Spline/CubicBezier/SplineCubicBezier.cs b/Content.Shared/White/Spline/CubicBezier/SplineCubicBezier.cs
new file mode 100644
index 0000000000..fb4821bbbc
--- /dev/null
+++ b/Content.Shared/White/Spline/CubicBezier/SplineCubicBezier.cs
@@ -0,0 +1,81 @@
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Content.Shared.White.Spline.CubicBezier;
+
+public abstract class SplineCubicBezier : Spline
+{
+ protected const int LookupPrecision = 100;
+
+ protected static readonly (float c0, float c1, float c2, float c3)[] PositionCoefficientLookup
+ = Enumerable.Range(0, LookupPrecision + 1).Select(x => CalculateCoefficientsPosition((float) x / LookupPrecision)).ToArray();
+
+ protected static readonly (float c0, float c1, float c2, float c3)[] GradientCoefficientLookup
+ = Enumerable.Range(0, LookupPrecision + 1).Select(x => CalculateCoefficientsTangent((float) x / LookupPrecision)).ToArray();
+
+ protected static (float c0, float c1, float c2, float c3) CalculateCoefficientsPosition(float t)
+ {
+ var tt = t * t;
+ var ttt = tt * t;
+ return (
+ -ttt + 3f * tt - 3f * t + 1f,
+ 3f * ttt - 6f * tt + 3f * t,
+ -3f * ttt + 3f * tt,
+ ttt
+ );
+ }
+
+ protected static (float c0, float c1, float c2, float c3) CalculateCoefficientsTangent(float t)
+ {
+ var tt = t * t;
+ return (
+ -3f * tt + 6f * t - 3,
+ 9f * tt - 12f * t + 3,
+ -9f * tt + 6f * t,
+ 3f * tt
+ );
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetLookupIndex(float t)
+ {
+ return (int) (t * LookupPrecision);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override T SamplePosition(ReadOnlySpan controlPoints, float u)
+ {
+ return CalculateBezier(GetCurrentControlPoints(controlPoints, (int) u), PositionCoefficientLookup[GetLookupIndex(u % 1)]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override T SampleVelocity(ReadOnlySpan controlPoints, float u)
+ {
+ return CalculateBezier(GetCurrentControlPoints(controlPoints, (int) u), GradientCoefficientLookup[GetLookupIndex(u % 1)]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override (T Position, T Velocity) SamplePositionVelocity(ReadOnlySpan controlPoints, float u)
+ {
+ var lookupIndex = GetLookupIndex(u % 1);
+ var currentControlPoints = GetCurrentControlPoints(controlPoints, (int) u);
+ return (
+ CalculateBezier(currentControlPoints, PositionCoefficientLookup[lookupIndex]),
+ CalculateBezier(currentControlPoints, GradientCoefficientLookup[lookupIndex])
+ );
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override float GetControlGroupAmount(int controlPointAmount)
+ {
+ return (controlPointAmount - 1) / 3f;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected virtual (T p0, T p1, T p2, T p3) GetCurrentControlPoints(ReadOnlySpan controlPoints, int u)
+ {
+ return (controlPoints[u], controlPoints[u + 1], controlPoints[u + 2], controlPoints[u + 3]);
+ }
+
+ protected abstract T CalculateBezier((T p0, T p1, T p2, T p3) points, (float c0, float c1, float c2, float c3) coeffs);
+}
diff --git a/Content.Shared/White/Spline/CubicBezier/SplineCubicBezier4D.cs b/Content.Shared/White/Spline/CubicBezier/SplineCubicBezier4D.cs
new file mode 100644
index 0000000000..77954c4c68
--- /dev/null
+++ b/Content.Shared/White/Spline/CubicBezier/SplineCubicBezier4D.cs
@@ -0,0 +1,37 @@
+using System.Runtime.CompilerServices;
+
+namespace Content.Shared.White.Spline.CubicBezier;
+
+public sealed class SplineCubicBezier4D : SplineCubicBezier
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector4 Add(Vector4 op1, Vector4 op2)
+ {
+ return new Vector4(op1.X + op2.X, op1.Y + op2.Y, op1.Z + op2.Z, op1.W + op2.W);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector4 Subtract(Vector4 op1, Vector4 op2)
+ {
+ return new Vector4(op1.X - op2.X, op1.Y - op2.Y, op1.Z - op2.Z, op1.W - op2.W);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override float Magnitude(Vector4 op1)
+ {
+ return MathF.Sqrt(op1.X * op1.X + op1.Y * op1.Y + op1.Z * op1.Z + op1.W * op1.W);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector4 CalculateBezier(
+ (Vector4 p0, Vector4 p1, Vector4 p2, Vector4 p3) points,
+ (float c0, float c1, float c2, float c3) coeffs)
+ {
+ return new Vector4(
+ points.p0.X * coeffs.c0 + points.p1.X * coeffs.c1 + points.p2.X * coeffs.c2 + points.p3.X * coeffs.c3,
+ points.p0.Y * coeffs.c0 + points.p1.Y * coeffs.c1 + points.p2.Y * coeffs.c2 + points.p3.Y * coeffs.c3,
+ points.p0.Z * coeffs.c0 + points.p1.Z * coeffs.c1 + points.p2.Z * coeffs.c2 + points.p3.Z * coeffs.c3,
+ points.p0.W * coeffs.c0 + points.p1.W * coeffs.c1 + points.p2.W * coeffs.c2 + points.p3.W * coeffs.c3
+ );
+ }
+}
diff --git a/Content.Shared/White/Spline/CubicBezier/SplineCubicBezierColor.cs b/Content.Shared/White/Spline/CubicBezier/SplineCubicBezierColor.cs
new file mode 100644
index 0000000000..0251a35e00
--- /dev/null
+++ b/Content.Shared/White/Spline/CubicBezier/SplineCubicBezierColor.cs
@@ -0,0 +1,37 @@
+using System.Runtime.CompilerServices;
+
+namespace Content.Shared.White.Spline.CubicBezier;
+
+public sealed class SplineCubicBezierColor : SplineCubicBezier
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Color Add(Color op1, Color op2)
+ {
+ return new Color(op1.R + op2.R, op1.G + op2.G, op1.B + op2.B, op1.A + op2.A);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Color Subtract(Color op1, Color op2)
+ {
+ return new Color(op1.R - op2.R, op1.G - op2.G, op1.B - op2.B, op1.A - op2.A);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override float Magnitude(Color op1)
+ {
+ return MathF.Sqrt(op1.R * op1.R + op1.G * op1.G + op1.B * op1.B + op1.A * op1.A);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Color CalculateBezier(
+ (Color p0, Color p1, Color p2, Color p3) points,
+ (float c0, float c1, float c2, float c3) coeffs)
+ {
+ return new Color(
+ points.p0.R * coeffs.c0 + points.p1.R * coeffs.c1 + points.p2.R * coeffs.c2 + points.p3.R * coeffs.c3,
+ points.p0.G * coeffs.c0 + points.p1.G * coeffs.c1 + points.p2.G * coeffs.c2 + points.p3.G * coeffs.c3,
+ points.p0.B * coeffs.c0 + points.p1.B * coeffs.c1 + points.p2.B * coeffs.c2 + points.p3.B * coeffs.c3,
+ points.p0.A * coeffs.c0 + points.p1.A * coeffs.c1 + points.p2.A * coeffs.c2 + points.p3.A * coeffs.c3
+ );
+ }
+}
diff --git a/Content.Shared/White/Spline/ISpline.cs b/Content.Shared/White/Spline/ISpline.cs
new file mode 100644
index 0000000000..ffec44b860
--- /dev/null
+++ b/Content.Shared/White/Spline/ISpline.cs
@@ -0,0 +1,14 @@
+namespace Content.Shared.White.Spline;
+
+public interface ISpline
+{
+ T SamplePosition(ReadOnlySpan controlPoints, float u);
+
+ T SampleVelocity(ReadOnlySpan controlPoints, float u);
+
+ (T Position, T Velocity) SamplePositionVelocity(ReadOnlySpan controlPoints, float u);
+
+ IEnumerable IteratePointParamsByLength(T[] controlPoints, float lengthStepSize);
+
+ float GetControlGroupAmount(int controlPointAmount);
+}
diff --git a/Content.Shared/White/Spline/Linear/SplineLinear.cs b/Content.Shared/White/Spline/Linear/SplineLinear.cs
new file mode 100644
index 0000000000..06d33dd182
--- /dev/null
+++ b/Content.Shared/White/Spline/Linear/SplineLinear.cs
@@ -0,0 +1,34 @@
+using System.Runtime.CompilerServices;
+
+namespace Content.Shared.White.Spline.Linear;
+
+public abstract class SplineLinear : Spline
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override T SamplePosition(ReadOnlySpan controlPoints, float u)
+ {
+ var iu = (int) u;
+ var t = u % 1;
+ return Add(Multiply(controlPoints[iu], 1 - t), Multiply(controlPoints[iu + 1], t));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override T SampleVelocity(ReadOnlySpan controlPoints, float u)
+ {
+ var iu = (int) u;
+ return iu == 0
+ ? Subtract(controlPoints[iu + 1], controlPoints[iu])
+ : Subtract(controlPoints[iu + 1], controlPoints[iu - 1]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override (T Position, T Velocity) SamplePositionVelocity(ReadOnlySpan controlPoints, float u)
+ {
+ return (
+ SamplePosition(controlPoints, u),
+ SampleVelocity(controlPoints, u)
+ );
+ }
+
+ protected abstract T Multiply(T op1, float scalar);
+}
diff --git a/Content.Shared/White/Spline/Linear/SplineLinear2D.cs b/Content.Shared/White/Spline/Linear/SplineLinear2D.cs
new file mode 100644
index 0000000000..04272f8b6c
--- /dev/null
+++ b/Content.Shared/White/Spline/Linear/SplineLinear2D.cs
@@ -0,0 +1,31 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace Content.Shared.White.Spline.Linear;
+
+public sealed class SplineLinear2D : SplineLinear
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector2 Add(Vector2 op1, Vector2 op2)
+ {
+ return op1 + op2;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector2 Subtract(Vector2 op1, Vector2 op2)
+ {
+ return op1 - op2;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override float Magnitude(Vector2 op1)
+ {
+ return op1.Length();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector2 Multiply(Vector2 op1, float scalar)
+ {
+ return op1 * scalar;
+ }
+}
diff --git a/Content.Shared/White/Spline/Linear/SplineLinear4D.cs b/Content.Shared/White/Spline/Linear/SplineLinear4D.cs
new file mode 100644
index 0000000000..c8bef048a1
--- /dev/null
+++ b/Content.Shared/White/Spline/Linear/SplineLinear4D.cs
@@ -0,0 +1,30 @@
+using System.Runtime.CompilerServices;
+
+namespace Content.Shared.White.Spline.Linear;
+
+public sealed class SplineLinear4D : SplineLinear
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector4 Add(Vector4 op1, Vector4 op2)
+ {
+ return new Vector4(op1.X + op2.X, op1.Y + op2.Y, op1.Z + op2.Z, op1.W + op2.W);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector4 Subtract(Vector4 op1, Vector4 op2)
+ {
+ return new Vector4(op1.X - op2.X, op1.Y - op2.Y, op1.Z - op2.Z, op1.W - op2.W);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override float Magnitude(Vector4 op1)
+ {
+ return MathF.Sqrt(op1.X * op1.X + op1.Y * op1.Y + op1.Z * op1.Z + op1.W * op1.W);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Vector4 Multiply(Vector4 op1, float scalar)
+ {
+ return new Vector4(op1.X * scalar, op1.Y * scalar, op1.Z * scalar, op1.W * scalar);
+ }
+}
diff --git a/Content.Shared/White/Spline/Linear/SplineLinearColor.cs b/Content.Shared/White/Spline/Linear/SplineLinearColor.cs
new file mode 100644
index 0000000000..1661dddd4c
--- /dev/null
+++ b/Content.Shared/White/Spline/Linear/SplineLinearColor.cs
@@ -0,0 +1,30 @@
+using System.Runtime.CompilerServices;
+
+namespace Content.Shared.White.Spline.Linear;
+
+public sealed class SplineLinearColor : SplineLinear
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Color Add(Color op1, Color op2)
+ {
+ return new Color(op1.R + op2.R, op1.G + op2.G, op1.B + op2.B, op1.A + op2.A);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Color Subtract(Color op1, Color op2)
+ {
+ return new Color(op1.R - op2.R, op1.G - op2.G, op1.B - op2.B, op1.A - op2.A);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override float Magnitude(Color op1)
+ {
+ return MathF.Sqrt(op1.R * op1.R + op1.G * op1.G + op1.B * op1.B + op1.A * op1.A);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override Color Multiply(Color op1, float scalar)
+ {
+ return new Color(op1.R * scalar, op1.G * scalar, op1.B * scalar, op1.A * scalar);
+ }
+}
diff --git a/Content.Shared/White/Spline/Spline.cs b/Content.Shared/White/Spline/Spline.cs
new file mode 100644
index 0000000000..70a4b3bcef
--- /dev/null
+++ b/Content.Shared/White/Spline/Spline.cs
@@ -0,0 +1,44 @@
+using System.Runtime.CompilerServices;
+
+namespace Content.Shared.White.Spline;
+
+public abstract class Spline : ISpline
+{
+ public abstract T SamplePosition(ReadOnlySpan controlPoints, float u);
+
+ public abstract T SampleVelocity(ReadOnlySpan controlPoints, float u);
+
+ public abstract (T Position, T Velocity) SamplePositionVelocity(ReadOnlySpan controlPoints, float u);
+
+ public virtual IEnumerable IteratePointParamsByLength(T[] controlPoints, float lengthStepSize)
+ {
+ //ну а хули нам наивным салюшонам
+ for (var u = 0; u < controlPoints.Length - 1; u++)
+ {
+ var segmentLength = ApproximateArcLength(controlPoints, u);
+ var tStepSize = lengthStepSize / segmentLength;
+ for (var t = 0f; t < 1; t += tStepSize)
+ {
+ yield return u + t;
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public virtual float GetControlGroupAmount(int controlPointAmount)
+ {
+ return controlPointAmount - 1f;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected virtual float ApproximateArcLength(ReadOnlySpan controlPoints, float u)
+ {
+ return Magnitude(Subtract(controlPoints[(int) u], controlPoints[(int) u + 1]));
+ }
+
+ protected abstract T Add(T op1, T op2);
+
+ protected abstract T Subtract(T op1, T op2);
+
+ protected abstract float Magnitude(T op1);
+}
diff --git a/Content.Shared/White/Spline/SplineEnums.cs b/Content.Shared/White/Spline/SplineEnums.cs
new file mode 100644
index 0000000000..ab4ceb613e
--- /dev/null
+++ b/Content.Shared/White/Spline/SplineEnums.cs
@@ -0,0 +1,59 @@
+using System.Numerics;
+using Content.Shared.White.Spline.CatmullRom;
+using Content.Shared.White.Spline.CubicBezier;
+using Content.Shared.White.Spline.Linear;
+using Vector4 = Robust.Shared.Maths.Vector4;
+
+namespace Content.Shared.White.Spline;
+
+public static class Spline
+{
+ public static ISpline From2DType(Spline2DType type)
+ {
+ return type switch
+ {
+ Spline2DType.Linear => new SplineLinear2D(),
+ Spline2DType.CatmullRom => new SplineCatmullRom2D(),
+ _ => throw new NotImplementedException()
+ };
+ }
+
+ public static ISpline From4DType(Spline4DType type)
+ {
+ return type switch
+ {
+ Spline4DType.Linear => new SplineLinear4D(),
+ Spline4DType.Bezier => new SplineCubicBezier4D(),
+ _ => throw new NotImplementedException()
+ };
+ }
+
+ public static ISpline FromColorType(SplineColorType type)
+ {
+ return type switch
+ {
+ SplineColorType.Linear => new SplineLinearColor(),
+ SplineColorType.Bezier => new SplineCubicBezierColor(),
+ _ => throw new NotImplementedException()
+ };
+ }
+}
+
+public enum Spline2DType : byte
+{
+ Linear,
+ CatmullRom
+}
+
+public enum Spline4DType : byte
+{
+ Linear,
+ Bezier
+}
+
+public enum SplineColorType : byte
+{
+ Linear,
+ Bezier
+}
+
diff --git a/Content.Shared/White/Trail/SharedTrailComponent.cs b/Content.Shared/White/Trail/SharedTrailComponent.cs
new file mode 100644
index 0000000000..62fab24df0
--- /dev/null
+++ b/Content.Shared/White/Trail/SharedTrailComponent.cs
@@ -0,0 +1,74 @@
+using System.Numerics;
+using Content.Shared.White.Spline;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Vector4 = Robust.Shared.Maths.Vector4;
+
+namespace Content.Shared.White.Trail;
+
+[NetworkedComponent]
+public abstract partial class SharedTrailComponent : Component, ITrailSettings
+{
+ [DataField("gravity")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual Vector2 Gravity { get; set; }
+
+ [DataField("lifetime", required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual float Lifetime { get; set; }
+
+ [DataField("lengthStep")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual float LengthStep { get; set; }
+
+ [DataField("randomWalk")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual Vector2 MaxRandomWalk { get; set; }
+
+ [DataField("scale", required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual Vector2 Scale { get; set; }
+
+ [DataField("texturePath")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual string? TexurePath { get; set; }
+
+ [DataField("creationOffset")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual Vector2 CreationOffset { get; set; }
+
+ [DataField("сreationDistanceThresholdSquared")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual float СreationDistanceThresholdSquared { get; set; }
+
+ [DataField("creationMethod")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual SegmentCreationMethod СreationMethod { get; set; }
+
+ [DataField("gradient", required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual Vector4[] Gradient { get; set; } = new[] { Vector4.One, new Vector4(1f, 1f, 1f, 0f) };
+
+ [DataField("gradientIteratorType")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual Spline4DType GradientIteratorType { get; set; }
+
+ [DataField("splineIteratorType")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual Spline2DType SplineIteratorType { get; set; }
+
+ [DataField("splineRendererType")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public virtual TrailSplineRendererType SplineRendererType { get; set; }
+}
+
+[Serializable, NetSerializable]
+public sealed class TrailComponentState : ComponentState
+{
+ public TrailSettings Settings;
+
+ public TrailComponentState(TrailSettings settings)
+ {
+ Settings = settings;
+ }
+}
diff --git a/Content.Shared/White/Trail/TrailSettings.cs b/Content.Shared/White/Trail/TrailSettings.cs
new file mode 100644
index 0000000000..3d40444b86
--- /dev/null
+++ b/Content.Shared/White/Trail/TrailSettings.cs
@@ -0,0 +1,97 @@
+using System.Numerics;
+using Content.Shared.White.Spline;
+using Robust.Shared.Serialization;
+using Vector4 = Robust.Shared.Maths.Vector4;
+
+namespace Content.Shared.White.Trail;
+
+[DataDefinition]
+[Serializable, NetSerializable]
+public sealed partial class TrailSettings : ITrailSettings
+{
+ public static readonly TrailSettings Default = new();
+
+ public Vector2 Scale { get; set; } = new(0.5f, 1f);
+
+ public float СreationDistanceThresholdSquared { get; set; } = 0.1f;
+
+ public SegmentCreationMethod СreationMethod { get; set; } = SegmentCreationMethod.OnFrameUpdate;
+
+ public Vector2 CreationOffset { get; set; } = Vector2.Zero;
+
+ public Vector2 Gravity { get; set; } = new(0.01f, 0.01f);
+
+ public Vector2 MaxRandomWalk { get; set; } = new(0.005f, 0.005f);
+
+ public float Lifetime { get; set; }
+
+ public float LengthStep { get; set; } = 0.1f;
+
+ public string? TexurePath { get; set; }
+
+ public Vector4[] Gradient { get; set; } = { new(1f, 1f, 1f, 1f), new(1f, 1f, 1f, 0f) };
+
+ public Spline4DType GradientIteratorType { get; set; }
+
+ public Spline2DType SplineIteratorType { get; set; }
+
+ public TrailSplineRendererType SplineRendererType { get; set; }
+
+ public static void Inject(ITrailSettings into, ITrailSettings from)
+ {
+ into.Scale = from.Scale;
+ into.СreationDistanceThresholdSquared = from.СreationDistanceThresholdSquared;
+ into.СreationMethod = from.СreationMethod;
+ into.CreationOffset = from.CreationOffset;
+ into.Gravity = from.Gravity;
+ into.MaxRandomWalk = from.MaxRandomWalk;
+ into.Lifetime = from.Lifetime;
+ into.LengthStep = from.LengthStep;
+ into.TexurePath = from.TexurePath;
+ into.Gradient = from.Gradient;
+ into.SplineIteratorType = from.SplineIteratorType;
+ into.SplineRendererType = from.SplineRendererType;
+ }
+}
+
+public interface ITrailSettings
+{
+ Vector2 Gravity { get; set; }
+
+ float Lifetime { get; set; }
+
+ float LengthStep { get; set; }
+
+ Vector2 MaxRandomWalk { get; set; }
+
+ Vector2 Scale { get; set; }
+
+ string? TexurePath { get; set; }
+
+ Vector2 CreationOffset { get; set; }
+
+ float СreationDistanceThresholdSquared { get; set; }
+
+ SegmentCreationMethod СreationMethod { get; set; }
+
+ Vector4[] Gradient { get; set; }
+
+ Spline4DType GradientIteratorType { get; set; }
+
+ Spline2DType SplineIteratorType { get; set; }
+
+ TrailSplineRendererType SplineRendererType { get; set; }
+}
+
+public enum SegmentCreationMethod : byte
+{
+ OnFrameUpdate,
+ OnMove
+}
+
+public enum TrailSplineRendererType : byte
+{
+ Continuous,
+ Point,
+ Debug
+}
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml
index a013376757..b658077d12 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml
@@ -136,8 +136,8 @@
- state: mag-unshaded-4
map: ["enum.GunVisualLayers.MagUnshaded"]
shader: unshaded
- - type: HitscanBatteryAmmoProvider
- proto: RedMediumLaser
+ - type: ProjectileBatteryAmmoProvider
+ proto: BulletTrailLaser
fireCost: 62.5
- type: MagazineVisuals
magState: mag
@@ -161,8 +161,8 @@
shader: unshaded
- type: Clothing
sprite: Objects/Weapons/Guns/Battery/makeshift.rsi
- - type: HitscanBatteryAmmoProvider
- proto: RedLaser
+ - type: ProjectileBatteryAmmoProvider
+ proto: BulletTrailLaser
fireCost: 62.5
- type: Battery
maxCharge: 500
@@ -217,8 +217,8 @@
selectedMode: SemiAuto
availableModes:
- SemiAuto
- - type: HitscanBatteryAmmoProvider
- proto: RedLaser
+ - type: ProjectileBatteryAmmoProvider
+ proto: BulletTrailLaser
fireCost: 62.5
- type: entity
@@ -227,8 +227,8 @@
id: WeaponLaserCarbinePractice
description: This modified laser rifle fires harmless beams in the 40-watt range, for target practice.
components:
- - type: HitscanBatteryAmmoProvider
- proto: RedLaserPractice
+ - type: ProjectileBatteryAmmoProvider
+ proto: BulletTrailLaserPractice
fireCost: 62.5
- type: entity
@@ -253,8 +253,8 @@
- SemiAuto
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
- - type: HitscanBatteryAmmoProvider
- proto: Pulse
+ - type: ProjectileBatteryAmmoProvider
+ proto: PulseBoltProjectile
fireCost: 200
- type: Battery
maxCharge: 2000
@@ -284,8 +284,8 @@
- FullAuto
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
- - type: HitscanBatteryAmmoProvider
- proto: Pulse
+ - type: ProjectileBatteryAmmoProvider
+ proto: PulseBoltProjectile
fireCost: 200
- type: Battery
maxCharge: 5000
@@ -311,8 +311,8 @@
fireRate: 1.5
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser3.ogg
- - type: HitscanBatteryAmmoProvider
- proto: Pulse
+ - type: ProjectileBatteryAmmoProvider
+ proto: PulseBoltProjectile
fireCost: 100
- type: Battery
maxCharge: 40000
@@ -338,8 +338,8 @@
fireRate: 1.5
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
- - type: HitscanBatteryAmmoProvider
- proto: RedHeavyLaser
+ - type: ProjectileBatteryAmmoProvider
+ proto: BulletTrailLaserHeavy
fireCost: 100
- type: entity
@@ -392,8 +392,8 @@
- type: Gun
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser3.ogg
- - type: HitscanBatteryAmmoProvider
- proto: XrayLaser
+ - type: ProjectileBatteryAmmoProvider
+ proto: BulletTrailLaserXray
fireCost: 100
- type: MagazineVisuals
magState: mag
@@ -554,8 +554,8 @@
- type: Gun
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
- - type: HitscanBatteryAmmoProvider
- proto: RedMediumLaser
+ - type: ProjectileBatteryAmmoProvider
+ proto: BulletTrailLaserMedium
fireCost: 100
- type: BatterySelfRecharger
autoRecharge: true
@@ -595,8 +595,8 @@
- type: Gun
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
- - type: HitscanBatteryAmmoProvider
- proto: RedMediumLaser
+ - type: ProjectileBatteryAmmoProvider
+ proto: BulletTrailLaserMedium
fireCost: 100
- type: BatterySelfRecharger
autoRecharge: true
@@ -682,8 +682,8 @@
fireRate: 1
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/laser_clown.ogg
- - type: HitscanBatteryAmmoProvider
- proto: RedMediumLaser
+ - type: ProjectileBatteryAmmoProvider
+ proto: BulletTrailLaserMedium
fireCost: 100
- type: BatterySelfRecharger
autoRecharge: true
@@ -721,15 +721,18 @@
- suitStorage
- type: Gun
soundGunshot:
- path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
+ path: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg
+ projectileSpeed: 12
- type: TwoModeEnergyAmmoProvider
- projProto: BulletDisabler
- fireCost: 50
- projFireCost: 50
- hitscanProto: RedLaser
- hitscanFireCost: 100
- projSound: "/Audio/Weapons/Guns/Gunshots/taser2.ogg"
- hitscanSound: "/Audio/Weapons/Guns/Gunshots/laser_cannon.ogg"
+ stunPrototype: BulletDisabler
+ laserPrototype: BulletTrailLaser
+ fireCost: 100
+ laserFireCost: 99
+ stunFireCost: 49
+ stunProjectileSpeed: 25
+ laserProjectileSpeed: 25
+ stunSound: "/Audio/Weapons/Guns/Gunshots/taser2.ogg"
+ laserSound: "/Audio/Weapons/Guns/Gunshots/laser_cannon.ogg"
- type: MagazineVisuals
magState: mag
steps: 5
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
index 5f909d221f..a35718fcac 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
@@ -192,9 +192,11 @@
soundHit:
path: "/Audio/Weapons/Guns/Hits/taser_hit.ogg"
soundForce: true
- - type: StunOnCollide
- stunAmount: 5
- knockdownAmount: 5
+ - type: StaminaDamageOnCollide
+ damage: 100
+ - type: Reflective
+ reflective:
+ - Energy
- type: entity
name : disabler bolt
@@ -879,3 +881,303 @@
- type: Tag
tags:
- HideContextMenu
+
+- type: entity
+ name: laser bolt
+ id: BulletTrailLaser
+ parent: BaseBullet
+ noSpawn: true
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi
+ layers:
+ - shader: unshaded
+ - type: Ammo
+ muzzleFlash: null
+ - type: Physics
+ - type: Fixtures
+ fixtures:
+ projectile:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.2,-0.2,0.2,0.2"
+ hard: false
+ mask:
+ - Opaque
+ fly-by: *flybyfixture
+ - type: Projectile
+ # soundHit: Waiting on serv3
+ damage:
+ types:
+ Heat: 23
+ - type: TimedDespawn
+ lifetime: 3
+ - type: Trail
+ splineIteratorType: Linear
+ splineRendererType: Continuous
+ creationMethod: OnMove
+ scale: 0.05, 0.0
+ lifetime: 0.1
+ randomWalk: 0.001, 0.001
+ gravity: 0, 0
+ gradient:
+ - 1, 0, 0, 1
+ - 1, 0, 0, 0
+ - type: PointLight
+ radius: 3.5
+ color: red
+ energy: 1
+ - type: Reflective
+ reflective:
+ - Energy
+
+
+- type: entity
+ name: practice laser bolt
+ id: BulletTrailLaserPractice
+ parent: BaseBullet
+ noSpawn: true
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi
+ layers:
+ - shader: unshaded
+ - type: Ammo
+ muzzleFlash: null
+ - type: Physics
+ - type: Fixtures
+ fixtures:
+ projectile:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.2,-0.2,0.2,0.2"
+ hard: false
+ mask:
+ - Opaque
+ fly-by: *flybyfixture
+ - type: Projectile
+ # soundHit: Waiting on serv3
+ damage:
+ types:
+ Heat: 0
+ - type: TimedDespawn
+ lifetime: 3
+ - type: Trail
+ splineIteratorType: Linear
+ splineRendererType: Continuous
+ creationMethod: OnMove
+ scale: 0.05, 0.0
+ lifetime: 0.1
+ randomWalk: 0.001, 0.001
+ gravity: 0, 0
+ gradient:
+ - 1, 0, 0, 1
+ - 1, 0, 0, 0
+ - type: PointLight
+ radius: 3.5
+ color: red
+ energy: 1
+ - type: Reflective
+ reflective:
+ - Energy
+
+
+
+- type: entity
+ name: pulse bolt
+ id: PulseBoltProjectile
+ parent: BaseBullet
+ noSpawn: true
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi
+ layers:
+ - shader: unshaded
+ - type: Ammo
+ muzzleFlash: null
+ - type: Physics
+ - type: Fixtures
+ fixtures:
+ projectile:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.2,-0.2,0.2,0.2"
+ hard: false
+ mask:
+ - Opaque
+ fly-by: *flybyfixture
+ - type: Projectile
+ # soundHit: Waiting on serv3
+ damage:
+ types:
+ Heat: 35
+ - type: TimedDespawn
+ lifetime: 3
+ - type: Trail
+ splineIteratorType: Linear
+ splineRendererType: Continuous
+ creationMethod: OnMove
+ scale: 0.10, 0.0
+ lifetime: 0.1
+ randomWalk: 0.001, 0.001
+ gravity: 0, 0
+ gradient:
+ - 0, 0, 1, 1
+ - 0, 0, 1, 0
+ - type: PointLight
+ radius: 3.5
+ color: blue
+ energy: 1
+ - type: Reflective
+ reflective:
+ - Energy
+
+
+- type: entity
+ name: heavy laser bolt
+ id: BulletTrailLaserHeavy
+ parent: BaseBullet
+ noSpawn: true
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi
+ layers:
+ - shader: unshaded
+ - type: Ammo
+ muzzleFlash: null
+ - type: Physics
+ - type: Fixtures
+ fixtures:
+ projectile:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.2,-0.2,0.2,0.2"
+ hard: false
+ mask:
+ - Opaque
+ fly-by: *flybyfixture
+ - type: Projectile
+ # soundHit: Waiting on serv3
+ damage:
+ types:
+ Heat: 35
+ - type: TimedDespawn
+ lifetime: 3
+ - type: Trail
+ splineIteratorType: Linear
+ splineRendererType: Continuous
+ creationMethod: OnMove
+ scale: 0.11, 0.0
+ lifetime: 0.1
+ randomWalk: 0.001, 0.001
+ gravity: 0, 0
+ gradient:
+ - 1, 0, 0, 1
+ - 1, 0, 0, 0
+ - type: PointLight
+ radius: 3.5
+ color: red
+ energy: 1
+ - type: Reflective
+ reflective:
+ - Energy
+
+- type: entity
+ name: xray laser bolt
+ id: BulletTrailLaserXray
+ parent: BaseBullet
+ noSpawn: true
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi
+ layers:
+ - shader: unshaded
+ - type: Ammo
+ muzzleFlash: null
+ - type: Physics
+ - type: Fixtures
+ fixtures:
+ projectile:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.2,-0.2,0.2,0.2"
+ hard: false
+ mask:
+ - Opaque
+ fly-by: *flybyfixture
+ - type: Projectile
+ # soundHit: Waiting on serv3
+ damage:
+ types:
+ Heat: 14
+ Radiation: 14
+ - type: TimedDespawn
+ lifetime: 3
+ - type: Trail
+ splineIteratorType: Linear
+ splineRendererType: Continuous
+ creationMethod: OnMove
+ scale: 0.11, 0.0
+ lifetime: 0.1
+ randomWalk: 0.001, 0.001
+ gravity: 0, 0
+ gradient:
+ - 0, 1, 0, 1
+ - 0, 1, 0, 0
+ - type: PointLight
+ radius: 3.5
+ color: green
+ energy: 1
+ - type: Reflective
+ reflective:
+ - Energy
+
+
+- type: entity
+ name: medium laser bolt
+ id: BulletTrailLaserMedium
+ parent: BaseBullet
+ noSpawn: true
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi
+ layers:
+ - shader: unshaded
+ - type: Ammo
+ muzzleFlash: null
+ - type: Physics
+ - type: Fixtures
+ fixtures:
+ projectile:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.2,-0.2,0.2,0.2"
+ hard: false
+ mask:
+ - Opaque
+ fly-by: *flybyfixture
+ - type: Projectile
+ # soundHit: Waiting on serv3
+ damage:
+ types:
+ Heat: 23
+ - type: TimedDespawn
+ lifetime: 3
+ - type: Trail
+ splineIteratorType: Linear
+ splineRendererType: Continuous
+ creationMethod: OnMove
+ scale: 0.05, 0.0
+ lifetime: 0.1
+ randomWalk: 0.001, 0.001
+ gravity: 0, 0
+ gradient:
+ - 1, 0, 0, 1
+ - 1, 0, 0, 0
+ - type: PointLight
+ radius: 3.5
+ color: red
+ energy: 1
+ - type: Reflective
+ reflective:
+ - Energy