* Add pointlight despawning visuals for sparks

* Add sprite fade out

* Add spark prototypes

* Add sparking behavior to lights

* Integrate sparks with some different stuff

* Remove invalid field
This commit is contained in:
Nemanja 2025-11-14 10:33:33 -05:00 committed by Janet Blackquill
parent d2e65e8b5c
commit 00ff4d5d9b
14 changed files with 513 additions and 0 deletions

View File

@ -0,0 +1,14 @@
using Content.Shared._ES.Core.Timer.Components;
namespace Content.Client._ES.Lighting.Components;
/// <summary>
/// Handles a point light that fades out while synced to a <see cref="ESTimedDespawnComponent"/>
/// </summary>
[RegisterComponent]
[Access(typeof(ESTimedDespawnLightFadeSystem))]
public sealed partial class ESTimedDespawnLightFadeComponent : Component
{
[DataField]
public TimeSpan FadeTime = TimeSpan.FromSeconds(1);
}

View File

@ -0,0 +1,52 @@
using Content.Client._ES.Lighting.Components;
using Content.Shared._ES.Core.Timer.Components;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.Timing;
namespace Content.Client._ES.Lighting;
public sealed class ESTimedDespawnLightFadeSystem : VisualizerSystem<ESTimedDespawnLightFadeComponent>
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
private const string FadeTrack = "light-fade";
protected override void OnAppearanceChange(EntityUid uid, ESTimedDespawnLightFadeComponent component, ref AppearanceChangeEvent args)
{
base.OnAppearanceChange(uid, component, ref args);
if (_animationPlayer.HasRunningAnimation(uid, FadeTrack))
return;
if (!AppearanceSystem.TryGetData<TimeSpan>(uid, ESTimedDespawnVisuals.DespawnTime, out var time, args.Component) ||
!TryComp<PointLightComponent>(uid, out var light))
return;
var duration = time - _timing.CurTime;
var animation = new Animation
{
Length = duration,
AnimationTracks =
{
new AnimationTrackComponentProperty
{
Property = nameof(PointLightComponent.Energy),
ComponentType = typeof(PointLightComponent),
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(light.Energy, 0f),
new AnimationTrackProperty.KeyFrame(light.Energy, (float) (duration - component.FadeTime).TotalSeconds),
new AnimationTrackProperty.KeyFrame(0f, (float) component.FadeTime.TotalSeconds, Easings.OutSine),
}
}
}
};
_animationPlayer.Play(uid, animation, FadeTrack);
}
}

View File

@ -0,0 +1,14 @@
using Content.Shared._ES.Core.Timer.Components;
namespace Content.Client._ES.Sprite.Components;
/// <summary>
/// Handles a sprite that fades out while synced to a <see cref="ESTimedDespawnComponent"/>
/// </summary>
[RegisterComponent]
[Access(typeof(ESTimedDespawnSpriteFadeSystem))]
public sealed partial class ESTimedDespawnSpriteFadeComponent : Component
{
[DataField]
public TimeSpan FadeTime = TimeSpan.FromSeconds(1);
}

View File

@ -0,0 +1,57 @@
using Content.Client._ES.Sprite.Components;
using Content.Shared._ES.Core.Timer.Components;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.Timing;
namespace Content.Client._ES.Sprite;
/// <summary>
/// This handles <see cref="ESTimedDespawnSpriteFadeComponent"/>
/// </summary>
public sealed class ESTimedDespawnSpriteFadeSystem : VisualizerSystem<ESTimedDespawnSpriteFadeComponent>
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
private const string FadeTrack = "es-sprite-fade";
protected override void OnAppearanceChange(EntityUid uid, ESTimedDespawnSpriteFadeComponent component, ref AppearanceChangeEvent args)
{
base.OnAppearanceChange(uid, component, ref args);
if (args.Sprite is not { } sprite)
return;
if (_animationPlayer.HasRunningAnimation(uid, FadeTrack))
return;
if (!AppearanceSystem.TryGetData<TimeSpan>(uid, ESTimedDespawnVisuals.DespawnTime, out var time, args.Component))
return;
var duration = time - _timing.CurTime;
var animation = new Animation
{
Length = duration,
AnimationTracks =
{
new AnimationTrackComponentProperty
{
Property = nameof(SpriteComponent.Color),
ComponentType = typeof(SpriteComponent),
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(sprite.Color, 0f),
new AnimationTrackProperty.KeyFrame(sprite.Color, MathF.Max((float) (duration - component.FadeTime).TotalSeconds, 0f)),
new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(0f), (float) component.FadeTime.TotalSeconds, Easings.OutSine),
},
},
},
};
_animationPlayer.Play(uid, animation, FadeTrack);
}
}

View File

@ -4,6 +4,10 @@ namespace Content.Server.Entry
public static class IgnoredComponents
{
public static string[] List => new[] {
// ES START
"ESTimedDespawnLightFade",
"ESTimedDespawnSpriteFade",
// ES END
"ConstructionGhost",
"IconSmooth",
"InteractionOutline",

View File

@ -0,0 +1,37 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Spawners;
namespace Content.Shared._ES.Core.Timer.Components;
/// <summary>
/// ES-specific version of <see cref="TimedDespawnComponent"/> with networking capabilities
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(ESTimedDespawnSystem), Other = AccessPermissions.None)]
public sealed partial class ESTimedDespawnComponent : Component
{
/// <summary>
/// How long the entity will exist before despawning
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan Lifetime;
/// <summary>
/// The time at which the entity will despawn
/// </summary>
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan DespawnTime;
/// <summary>
/// The time at which the entity spawned
/// </summary>
[ViewVariables]
public TimeSpan SpawnTime => DespawnTime - Lifetime;
}
[Serializable, NetSerializable]
public enum ESTimedDespawnVisuals : byte
{
DespawnTime,
}

View File

@ -0,0 +1,111 @@
using Content.Shared._ES.Core.Timer.Components;
using JetBrains.Annotations;
using Robust.Shared.Spawners;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared._ES.Core.Timer;
/// <summary>
/// This handles <see cref="ESTimedDespawnComponent"/>
/// </summary>
public sealed class ESTimedDespawnSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
private readonly HashSet<EntityUid> _toDelete = [];
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ESTimedDespawnComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<ESTimedDespawnComponent> ent, ref MapInitEvent args)
{
SetLifetime(ent.AsNullable(), ent.Comp.Lifetime);
}
/// <summary>
/// Sets the lifetime of the entity, adjusting the despawn time to compensate
/// </summary>
[PublicAPI]
public void SetLifetime(Entity<ESTimedDespawnComponent?> ent, TimeSpan lifetime)
{
if (!Resolve(ent, ref ent.Comp))
return;
DebugTools.Assert(lifetime >= TimeSpan.Zero, "Lifetime must be positive");
ent.Comp.Lifetime = lifetime;
ent.Comp.DespawnTime = _timing.CurTime + ent.Comp.Lifetime;
_appearance.SetData(ent, ESTimedDespawnVisuals.DespawnTime, ent.Comp.DespawnTime);
Dirty(ent);
}
/// <summary>
/// Gets the amount of time this entity will be alive before despawning
/// </summary>
[PublicAPI]
public TimeSpan GetLifetime(Entity<ESTimedDespawnComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return TimeSpan.Zero;
return ent.Comp.Lifetime;
}
/// <summary>
/// Sets the lifetime of the entity, adjusting the despawn time to compensate
/// </summary>
[PublicAPI]
public void SetDespawnTime(Entity<ESTimedDespawnComponent?> ent, TimeSpan despawnTime)
{
SetLifetime(ent, despawnTime - _timing.CurTime);
}
/// <summary>
/// Gets the time at which the entity will despawn
/// </summary>
[PublicAPI]
public TimeSpan GetDespawnTime(Entity<ESTimedDespawnComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return TimeSpan.Zero;
return ent.Comp.DespawnTime;
}
/// <summary>
/// Returns how far along through the timedDespawn the entity is (as a percentage [0, 1])
/// </summary>
[PublicAPI]
public double GetProgress(Entity<ESTimedDespawnComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return 0;
return Math.Clamp((_timing.CurTime - ent.Comp.SpawnTime) / ent.Comp.Lifetime, 0, 1);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_toDelete.Clear();
var query = EntityQueryEnumerator<ESTimedDespawnComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (_timing.CurTime < comp.DespawnTime)
continue;
_toDelete.Add(uid);
}
foreach (var toDelete in _toDelete)
{
// Same event as engine TimedDespawn
var ev = new TimedDespawnEvent();
RaiseLocalEvent(toDelete, ref ev);
PredictedQueueDel(toDelete);
}
}
}

View File

@ -0,0 +1,31 @@
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._ES.Sparks.Components;
/// <summary>
/// An entity that sparks when damaged by something
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(ESSparkOnHitSystem))]
public sealed partial class ESSparkOnHitComponent : Component
{
/// <summary>
/// Amount of damage that needs to be dealt to cause sparks
/// </summary>
[DataField]
public FixedPoint2 Threshold = 1;
/// <summary>
/// Number of sparks
/// </summary>
[DataField]
public int Count = 3;
/// <summary>
/// Spark prototypes
/// </summary>
[DataField]
public EntProtoId SparkPrototype = ESSparksSystem.DefaultSparks;
}

View File

@ -0,0 +1,26 @@
using Content.Shared._ES.Sparks.Components;
using Content.Shared.Damage.Systems;
namespace Content.Shared._ES.Sparks;
public sealed class ESSparkOnHitSystem : EntitySystem
{
[Dependency] private readonly ESSparksSystem _sparks = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ESSparkOnHitComponent, DamageChangedEvent>(OnDamaged);
}
private void OnDamaged(Entity<ESSparkOnHitComponent> ent, ref DamageChangedEvent args)
{
if (args.DamageDelta is null)
return;
if (args.DamageDelta.GetTotal() < ent.Comp.Threshold)
return;
_sparks.DoSparks(ent, ent.Comp.Count, ent.Comp.SparkPrototype);
}
}

View File

@ -0,0 +1,57 @@
using Content.Shared.Physics;
using Content.Shared.Throwing;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Shared._ES.Sparks;
public sealed class ESSparksSystem : EntitySystem
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public static readonly EntProtoId DefaultSparks = "ESEffectSparks";
public void DoSparks(EntityUid source, int number = 4, EntProtoId? sparksPrototype = null)
{
var coords = _transform.GetMapCoordinates(source);
DoSparks(coords, number, sparksPrototype, source);
}
public void DoSparks(EntityCoordinates coordinates, int number = 4, EntProtoId? sparksPrototype = null, EntityUid? ignored = null)
{
var mapCoordinates = _transform.ToMapCoordinates(coordinates);
DoSparks(mapCoordinates, number, sparksPrototype, ignored);
}
public void DoSparks(MapCoordinates coordinates, int number = 4, EntProtoId? sparksPrototype = null, EntityUid? ignored = null)
{
if (_net.IsClient)
return;
sparksPrototype ??= DefaultSparks;
var angleDelta = (Angle) (MathF.Tau / number);
var angle = _random.NextAngle();
for (var i = 0; i < number; i++)
{
var sparks = EntityManager.Spawn(sparksPrototype, coordinates, rotation: angle);
angle += angleDelta;
_throwing.TryThrow(sparks, angle.ToVec(), 2f, animated: false);
PreventCollide(sparks, ignored);
}
}
private void PreventCollide(EntityUid sparks, EntityUid? ignored)
{
if (!ignored.HasValue || TerminatingOrDeleted(ignored))
return;
var comp = EnsureComp<PreventCollideComponent>(sparks);
comp.Uid = ignored.Value;
Dirty(sparks, comp);
}
}

View File

@ -88,6 +88,9 @@
collection: GlassBreak
- type: PlacementReplacement
key: lights
# ES START
- type: ESSparkOnHit
# ES END
placement:
mode: SnapgridCenter
snap:

View File

@ -0,0 +1,50 @@
- type: entity
id: ESEffectSparks
name: sparks
components:
- type: Sprite
sprite: _ES/Effects/sparks.rsi
state: sparks
drawdepth: Effects
noRot: true
loop: false
- type: Appearance
- type: Physics
bodyType: Dynamic
bodyStatus: InAir
sleepingAllowed: false
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeCircle
radius: 0.25
hard: true
restitution: 0.0
density: 100
mask:
- Impassable
- type: ESTimedDespawn
lifetime: 1.1
- type: EmitSoundOnSpawn
positional: true
sound:
collection: sparks
params:
variation: 0.250
- type: ESTimedDespawnSpriteFade
fadeTime: 0.3
- type: ESTimedDespawnLightFade
fadeTime: 0.5
- type: AnimationPlayer
- type: PointLight
radius: 1.5
energy: 2.4
falloff: 6
color: "#FAA019"
netsync: false
- type: IgnitionSource
ignited: true
- type: Tag
tags:
- HideContextMenu

View File

@ -0,0 +1,57 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/29b6d6c129c56124200b7fbe46e41178f5373ca4/icons/effects/effects.dmi",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "sparks",
"directions": 4,
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
1
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB