diff --git a/Content.Client/_ES/Lighting/Components/ESTimedDespawnLightFadeComponent.cs b/Content.Client/_ES/Lighting/Components/ESTimedDespawnLightFadeComponent.cs new file mode 100644 index 00000000000..0e02dfe79ab --- /dev/null +++ b/Content.Client/_ES/Lighting/Components/ESTimedDespawnLightFadeComponent.cs @@ -0,0 +1,14 @@ +using Content.Shared._ES.Core.Timer.Components; + +namespace Content.Client._ES.Lighting.Components; + +/// +/// Handles a point light that fades out while synced to a +/// +[RegisterComponent] +[Access(typeof(ESTimedDespawnLightFadeSystem))] +public sealed partial class ESTimedDespawnLightFadeComponent : Component +{ + [DataField] + public TimeSpan FadeTime = TimeSpan.FromSeconds(1); +} diff --git a/Content.Client/_ES/Lighting/ESTimedDespawnLightFadeSystem.cs b/Content.Client/_ES/Lighting/ESTimedDespawnLightFadeSystem.cs new file mode 100644 index 00000000000..3cc547628b2 --- /dev/null +++ b/Content.Client/_ES/Lighting/ESTimedDespawnLightFadeSystem.cs @@ -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 +{ + [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(uid, ESTimedDespawnVisuals.DespawnTime, out var time, args.Component) || + !TryComp(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); + } +} diff --git a/Content.Client/_ES/Sprite/Components/ESTimedDespawnSpriteFadeComponent.cs b/Content.Client/_ES/Sprite/Components/ESTimedDespawnSpriteFadeComponent.cs new file mode 100644 index 00000000000..a3f2320048c --- /dev/null +++ b/Content.Client/_ES/Sprite/Components/ESTimedDespawnSpriteFadeComponent.cs @@ -0,0 +1,14 @@ +using Content.Shared._ES.Core.Timer.Components; + +namespace Content.Client._ES.Sprite.Components; + +/// +/// Handles a sprite that fades out while synced to a +/// +[RegisterComponent] +[Access(typeof(ESTimedDespawnSpriteFadeSystem))] +public sealed partial class ESTimedDespawnSpriteFadeComponent : Component +{ + [DataField] + public TimeSpan FadeTime = TimeSpan.FromSeconds(1); +} diff --git a/Content.Client/_ES/Sprite/ESTimedDespawnSpriteFadeSystem.cs b/Content.Client/_ES/Sprite/ESTimedDespawnSpriteFadeSystem.cs new file mode 100644 index 00000000000..f394b9315b6 --- /dev/null +++ b/Content.Client/_ES/Sprite/ESTimedDespawnSpriteFadeSystem.cs @@ -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; + +/// +/// This handles +/// +public sealed class ESTimedDespawnSpriteFadeSystem : VisualizerSystem +{ + [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(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); + } +} diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index 28089cbd47f..44b35001dd5 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -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", diff --git a/Content.Shared/_ES/Core/Timer/Components/ESTimedDespawnComponent.cs b/Content.Shared/_ES/Core/Timer/Components/ESTimedDespawnComponent.cs new file mode 100644 index 00000000000..4766d841246 --- /dev/null +++ b/Content.Shared/_ES/Core/Timer/Components/ESTimedDespawnComponent.cs @@ -0,0 +1,37 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Spawners; + +namespace Content.Shared._ES.Core.Timer.Components; + +/// +/// ES-specific version of with networking capabilities +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(ESTimedDespawnSystem), Other = AccessPermissions.None)] +public sealed partial class ESTimedDespawnComponent : Component +{ + /// + /// How long the entity will exist before despawning + /// + [DataField, AutoNetworkedField] + public TimeSpan Lifetime; + + /// + /// The time at which the entity will despawn + /// + [DataField, AutoNetworkedField, AutoPausedField] + public TimeSpan DespawnTime; + + /// + /// The time at which the entity spawned + /// + [ViewVariables] + public TimeSpan SpawnTime => DespawnTime - Lifetime; +} + +[Serializable, NetSerializable] +public enum ESTimedDespawnVisuals : byte +{ + DespawnTime, +} diff --git a/Content.Shared/_ES/Core/Timer/ESTimedDespawnSystem.cs b/Content.Shared/_ES/Core/Timer/ESTimedDespawnSystem.cs new file mode 100644 index 00000000000..8843ed45403 --- /dev/null +++ b/Content.Shared/_ES/Core/Timer/ESTimedDespawnSystem.cs @@ -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; + +/// +/// This handles +/// +public sealed class ESTimedDespawnSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + + private readonly HashSet _toDelete = []; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + SetLifetime(ent.AsNullable(), ent.Comp.Lifetime); + } + + /// + /// Sets the lifetime of the entity, adjusting the despawn time to compensate + /// + [PublicAPI] + public void SetLifetime(Entity 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); + } + + /// + /// Gets the amount of time this entity will be alive before despawning + /// + [PublicAPI] + public TimeSpan GetLifetime(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return TimeSpan.Zero; + return ent.Comp.Lifetime; + } + + /// + /// Sets the lifetime of the entity, adjusting the despawn time to compensate + /// + [PublicAPI] + public void SetDespawnTime(Entity ent, TimeSpan despawnTime) + { + SetLifetime(ent, despawnTime - _timing.CurTime); + } + + /// + /// Gets the time at which the entity will despawn + /// + [PublicAPI] + public TimeSpan GetDespawnTime(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return TimeSpan.Zero; + return ent.Comp.DespawnTime; + } + + /// + /// Returns how far along through the timedDespawn the entity is (as a percentage [0, 1]) + /// + [PublicAPI] + public double GetProgress(Entity 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(); + 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); + } + } +} diff --git a/Content.Shared/_ES/Sparks/Components/ESSparkOnHitComponent.cs b/Content.Shared/_ES/Sparks/Components/ESSparkOnHitComponent.cs new file mode 100644 index 00000000000..14b721998d9 --- /dev/null +++ b/Content.Shared/_ES/Sparks/Components/ESSparkOnHitComponent.cs @@ -0,0 +1,31 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._ES.Sparks.Components; + +/// +/// An entity that sparks when damaged by something +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(ESSparkOnHitSystem))] +public sealed partial class ESSparkOnHitComponent : Component +{ + /// + /// Amount of damage that needs to be dealt to cause sparks + /// + [DataField] + public FixedPoint2 Threshold = 1; + + /// + /// Number of sparks + /// + [DataField] + public int Count = 3; + + /// + /// Spark prototypes + /// + [DataField] + public EntProtoId SparkPrototype = ESSparksSystem.DefaultSparks; +} diff --git a/Content.Shared/_ES/Sparks/ESSparkOnHitSystem.cs b/Content.Shared/_ES/Sparks/ESSparkOnHitSystem.cs new file mode 100644 index 00000000000..04730fc2300 --- /dev/null +++ b/Content.Shared/_ES/Sparks/ESSparkOnHitSystem.cs @@ -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!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnDamaged); + } + + private void OnDamaged(Entity 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); + } +} diff --git a/Content.Shared/_ES/Sparks/ESSparksSystem.cs b/Content.Shared/_ES/Sparks/ESSparksSystem.cs new file mode 100644 index 00000000000..15a28d4c590 --- /dev/null +++ b/Content.Shared/_ES/Sparks/ESSparksSystem.cs @@ -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(sparks); + comp.Uid = ignored.Value; + Dirty(sparks, comp); + } +} diff --git a/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml b/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml index 3519a36f895..b34b00a34c6 100644 --- a/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml +++ b/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml @@ -88,6 +88,9 @@ collection: GlassBreak - type: PlacementReplacement key: lights +# ES START + - type: ESSparkOnHit +# ES END placement: mode: SnapgridCenter snap: diff --git a/Resources/Prototypes/_ES/Entities/Effects/sparks.yml b/Resources/Prototypes/_ES/Entities/Effects/sparks.yml new file mode 100644 index 00000000000..4f8833b975a --- /dev/null +++ b/Resources/Prototypes/_ES/Entities/Effects/sparks.yml @@ -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 diff --git a/Resources/Textures/_ES/Effects/sparks.rsi/meta.json b/Resources/Textures/_ES/Effects/sparks.rsi/meta.json new file mode 100644 index 00000000000..aa22d92aa20 --- /dev/null +++ b/Resources/Textures/_ES/Effects/sparks.rsi/meta.json @@ -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 + ] + ] + } + ] +} diff --git a/Resources/Textures/_ES/Effects/sparks.rsi/sparks.png b/Resources/Textures/_ES/Effects/sparks.rsi/sparks.png new file mode 100644 index 00000000000..44c08e50930 Binary files /dev/null and b/Resources/Textures/_ES/Effects/sparks.rsi/sparks.png differ