diff --git a/Content.Client/GameObjects/Components/ExpendableLightVisualizer.cs b/Content.Client/GameObjects/Components/ExpendableLightVisualizer.cs new file mode 100644 index 0000000000..e24a50ea84 --- /dev/null +++ b/Content.Client/GameObjects/Components/ExpendableLightVisualizer.cs @@ -0,0 +1,39 @@ + +using Content.Shared.GameObjects.Components; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using System; + +namespace Content.Client.GameObjects.Components +{ + [UsedImplicitly] + public class ExpendableLightVisualizer : AppearanceVisualizer + { + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + if (component.Deleted) + { + return; + } + + if (component.TryGetData(ExpendableLightVisuals.State, out string lightBehaviourID)) + { + if (component.Owner.TryGetComponent(out var lightBehaviour)) + { + lightBehaviour.StopLightBehaviour(); + + if (lightBehaviourID != string.Empty) + { + lightBehaviour.StartLightBehaviour(lightBehaviourID); + } + else if (component.Owner.TryGetComponent(out var light)) + { + light.Enabled = false; + } + } + } + } + } +} diff --git a/Content.Client/GameObjects/Components/Interactable/ExpendableLightComponent.cs b/Content.Client/GameObjects/Components/Interactable/ExpendableLightComponent.cs new file mode 100644 index 0000000000..7559d71356 --- /dev/null +++ b/Content.Client/GameObjects/Components/Interactable/ExpendableLightComponent.cs @@ -0,0 +1,16 @@ + +using Content.Shared.GameObjects.Components; +using Robust.Shared.GameObjects; +using Robust.Client.GameObjects; + +namespace Content.Client.GameObjects.Components.Interactable +{ + /// + /// Component that represents a handheld expendable light which can be activated and eventually dies over time. + /// + [RegisterComponent] + public class ExpendableLightComponent : SharedExpendableLightComponent + { + + } +} diff --git a/Content.Client/GameObjects/Components/LightBehaviourComponent.cs b/Content.Client/GameObjects/Components/LightBehaviourComponent.cs new file mode 100644 index 0000000000..83ff452c0e --- /dev/null +++ b/Content.Client/GameObjects/Components/LightBehaviourComponent.cs @@ -0,0 +1,539 @@ + +using System; +using System.Collections.Generic; +using Robust.Client.GameObjects; +using Robust.Shared.Animations; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; +using Content.Shared.GameObjects.Components; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Interfaces.Serialization; +using Robust.Client.Animations; +using Robust.Shared.Interfaces.GameObjects; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Robust.Client.GameObjects.Components.Animations; +using System.Linq; + +namespace Content.Client.GameObjects.Components +{ + #region LIGHT_BEHAVIOURS + /// + /// Base class for all light behaviours to derive from. + /// This AnimationTrack derivative does not rely on keyframes since it often needs to have a randomized duration. + /// + [Serializable] + public abstract class LightBehaviourAnimationTrack : AnimationTrackProperty, IExposeData + { + [ViewVariables] public string ID { get; set; } + [ViewVariables] public string Property { get; protected set; } + [ViewVariables] public bool IsLooped { get; set; } + [ViewVariables] public bool Enabled { get; set; } + [ViewVariables] public float StartValue { get; set; } + [ViewVariables] public float EndValue { get; set; } + [ViewVariables] public float MinDuration { get; set; } + [ViewVariables] public float MaxDuration { get; set; } + [ViewVariables] public AnimationInterpolationMode InterpolateMode { get; set; } + + [ViewVariables] protected float MaxTime { get; set; } + protected PointLightComponent Light = default; + protected IRobustRandom RobustRandom = default; + + private float _maxTime = default; + + public virtual void ExposeData(ObjectSerializer serializer) + { + serializer.DataField(this, x => x.ID, "id", string.Empty); + serializer.DataField(this, x => x.IsLooped, "isLooped", false); + serializer.DataField(this, x => x.Enabled, "enabled", false); + serializer.DataField(this, x => x.StartValue, "startValue", 0f); + serializer.DataField(this, x => x.EndValue, "endValue", 2f); + serializer.DataField(this, x => x.MinDuration, "minDuration", -1f); + serializer.DataField(this, x => x.MaxDuration, "maxDuration", 2f); + serializer.DataField(this, x => x.Property, "property", "Radius"); + serializer.DataField(this, x => x.InterpolateMode, "interpolate", AnimationInterpolationMode.Linear); + } + + public void Initialize(PointLightComponent light) + { + Light = light; + RobustRandom = IoCManager.Resolve(); + + if (Enabled) + { + Light.Enabled = true; + } + + OnInitialize(); + } + + public void UpdatePlaybackValues(Animation owner) + { + Light.Enabled = true; + + if (MinDuration > 0) + { + MaxTime = (float) RobustRandom.NextDouble() * (MaxDuration - MinDuration) + MinDuration; + } + else + { + MaxTime = MaxDuration; + } + + owner.Length = TimeSpan.FromSeconds(MaxTime); + } + + public override (int KeyFrameIndex, float FramePlayingTime) InitPlayback() + { + OnStart(); + + return (-1, _maxTime); + } + + protected void ApplyProperty(object value) + { + if (Property == null) + { + throw new InvalidOperationException("Property parameter is null! Check the prototype!"); + } + + if (Light is IAnimationProperties properties) + { + properties.SetAnimatableProperty(Property, value); + } + else + { + AnimationHelper.SetAnimatableProperty(Light, Property, value); + } + } + + protected override void ApplyProperty(object context, object value) + { + ApplyProperty(value); + } + + public virtual void OnInitialize() { } + public virtual void OnStart() { } + } + + /// + /// A light behaviour that alternates between StartValue and EndValue + /// + public class PulseBehaviour: LightBehaviourAnimationTrack + { + public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback( + object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime) + { + var playingTime = prevPlayingTime + frameTime; + var interpolateValue = playingTime / MaxTime; + + if (Property == "Enabled") // special case for boolean + { + ApplyProperty(interpolateValue < 0.5f? true : false); + return (-1, playingTime); + } + + if (interpolateValue < 0.5f) + { + switch (InterpolateMode) + { + case AnimationInterpolationMode.Linear: + ApplyProperty(InterpolateLinear(StartValue, EndValue, interpolateValue * 2f)); + break; + case AnimationInterpolationMode.Cubic: + ApplyProperty(InterpolateCubic(EndValue, StartValue, EndValue, StartValue, interpolateValue * 2f)); + break; + default: + case AnimationInterpolationMode.Nearest: + ApplyProperty(StartValue); + break; + } + } + else + { + switch (InterpolateMode) + { + case AnimationInterpolationMode.Linear: + ApplyProperty(InterpolateLinear(EndValue, StartValue, (interpolateValue - 0.5f) * 2f)); + break; + case AnimationInterpolationMode.Cubic: + ApplyProperty(InterpolateCubic(StartValue, EndValue, StartValue, EndValue, (interpolateValue - 0.5f) * 2f)); + break; + default: + case AnimationInterpolationMode.Nearest: + ApplyProperty(EndValue); + break; + } + } + + return (-1, playingTime); + } + } + + /// + /// A light behaviour that interpolates from StartValue to EndValue + /// + public class FadeBehaviour : LightBehaviourAnimationTrack + { + public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback( + object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime) + { + var playingTime = prevPlayingTime + frameTime; + var interpolateValue = playingTime / MaxTime; + + if (Property == "Enabled") // special case for boolean + { + ApplyProperty(interpolateValue < EndValue? true : false); + return (-1, playingTime); + } + + switch (InterpolateMode) + { + case AnimationInterpolationMode.Linear: + ApplyProperty(InterpolateLinear(StartValue, EndValue, interpolateValue)); + break; + case AnimationInterpolationMode.Cubic: + ApplyProperty(InterpolateCubic(EndValue, StartValue, EndValue, StartValue, interpolateValue)); + break; + default: + case AnimationInterpolationMode.Nearest: + ApplyProperty(interpolateValue < 0.5f ? StartValue : EndValue); + break; + } + + return (-1, playingTime); + } + } + + /// + /// A light behaviour that interpolates using random values chosen between StartValue and EndValue. + /// + public class RandomizeBehaviour : LightBehaviourAnimationTrack + { + private object _randomValue1 = default; + private object _randomValue2 = default; + private object _randomValue3 = default; + private object _randomValue4 = default; + + public override void OnInitialize() + { + _randomValue2 = InterpolateLinear(StartValue, EndValue, (float) RobustRandom.NextDouble()); + _randomValue3 = InterpolateLinear(StartValue, EndValue, (float) RobustRandom.NextDouble()); + _randomValue4 = InterpolateLinear(StartValue, EndValue, (float) RobustRandom.NextDouble()); + } + + public override void OnStart() + { + if (Property == "Enabled") // special case for boolean, we randomize it + { + ApplyProperty(RobustRandom.NextDouble() < 0.5 ? true : false); + return; + } + + if (InterpolateMode == AnimationInterpolationMode.Cubic) + { + _randomValue1 = _randomValue2; + _randomValue2 = _randomValue3; + } + + _randomValue3 = _randomValue4; + _randomValue4 = InterpolateLinear(StartValue, EndValue, (float) RobustRandom.NextDouble()); + } + + public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback( + object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime) + { + var playingTime = prevPlayingTime + frameTime; + var interpolateValue = playingTime / MaxTime; + + if (Property == "Enabled") + { + return (-1, playingTime); + } + + switch (InterpolateMode) + { + case AnimationInterpolationMode.Linear: + ApplyProperty(InterpolateLinear(_randomValue3, _randomValue4, interpolateValue)); + break; + case AnimationInterpolationMode.Cubic: + ApplyProperty(InterpolateCubic(_randomValue1, _randomValue2, _randomValue3, _randomValue4, interpolateValue)); + break; + default: + case AnimationInterpolationMode.Nearest: + ApplyProperty(interpolateValue < 0.5f ? _randomValue3 : _randomValue4); + break; + } + + return (-1, playingTime); + } + } + + /// + /// A light behaviour that cycles through a list of colors. + /// + public class ColorCycleBehaviour : LightBehaviourAnimationTrack + { + public List ColorsToCycle { get; set; } + + private int _colorIndex = 0; + + public override void OnStart() + { + _colorIndex++; + + if (_colorIndex > ColorsToCycle.Count - 1) + { + _colorIndex = 0; + } + } + + public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback( + object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime) + { + var playingTime = prevPlayingTime + frameTime; + var interpolateValue = playingTime / MaxTime; + + switch (InterpolateMode) + { + case AnimationInterpolationMode.Linear: + ApplyProperty(InterpolateLinear(ColorsToCycle[(_colorIndex - 1) % ColorsToCycle.Count], + ColorsToCycle[_colorIndex], + interpolateValue)); + break; + case AnimationInterpolationMode.Cubic: + ApplyProperty(InterpolateCubic(ColorsToCycle[_colorIndex], + ColorsToCycle[(_colorIndex + 1) % ColorsToCycle.Count], + ColorsToCycle[(_colorIndex + 2) % ColorsToCycle.Count], + ColorsToCycle[(_colorIndex + 3) % ColorsToCycle.Count], + interpolateValue)); + break; + default: + case AnimationInterpolationMode.Nearest: + ApplyProperty(ColorsToCycle[_colorIndex]); + break; + } + + return (-1, playingTime); + } + + public override void ExposeData(ObjectSerializer serializer) + { + serializer.DataField(this, x => x.ID, "id", string.Empty); + serializer.DataField(this, x => x.IsLooped, "isLooped", false); + serializer.DataField(this, x => x.Enabled, "enabled", false); + serializer.DataField(this, x => x.MinDuration, "minDuration", -1f); + serializer.DataField(this, x => x.MaxDuration, "maxDuration", 2f); + serializer.DataField(this, x => x.InterpolateMode, "interpolate", AnimationInterpolationMode.Linear); + ColorsToCycle = serializer.ReadDataField("colors", new List()); + Property = "Color"; + + if (ColorsToCycle.Count < 2) + { + throw new InvalidOperationException($"{nameof(ColorCycleBehaviour)} has less than 2 colors to cycle"); + } + } + + } + #endregion + + /// + /// A component which applies a specific behaviour to a PointLightComponent on its owner. + /// + [RegisterComponent] + public class LightBehaviourComponent : SharedLightBehaviourComponent + { + private const string KeyPrefix = nameof(LightBehaviourComponent); + + private class AnimationContainer + { + public AnimationContainer(int key, Animation animation, LightBehaviourAnimationTrack track) + { + Key = key; + Animation = animation; + LightBehaviour = track; + } + + public string FullKey => KeyPrefix + Key; + public int Key { get; set; } + public Animation Animation { get; set; } + public LightBehaviourAnimationTrack LightBehaviour { get; set; } + } + + [ViewVariables(VVAccess.ReadOnly)] + private List _animations = new List(); + + private float _originalRadius = default; + private float _originalEnergy = default; + private Angle _originalRotation = default; + private Color _originalColor = default; + private bool _originalEnabled = default; + private PointLightComponent _lightComponent = default; + private AnimationPlayerComponent _animationPlayer = default; + + protected override void Startup() + { + base.Startup(); + + CopyLightSettings(); + _animationPlayer = Owner.EnsureComponent(); + _animationPlayer.AnimationCompleted += s => OnAnimationCompleted(s); + + foreach (var container in _animations) + { + container.LightBehaviour.Initialize(_lightComponent); + } + + // we need to initialize all behaviours before starting any + foreach (var container in _animations) + { + if (container.LightBehaviour.Enabled) + { + StartLightBehaviour(container.LightBehaviour.ID); + } + } + } + + private void OnAnimationCompleted(string key) + { + var container = _animations.FirstOrDefault(x => x.FullKey == key); + + if (container.LightBehaviour.IsLooped) + { + container.LightBehaviour.UpdatePlaybackValues(container.Animation); + _animationPlayer.Play(container.Animation, container.FullKey); + } + } + + /// + /// If we disable all the light behaviours we want to be able to revert the light to its original state. + /// + private void CopyLightSettings() + { + if (Owner.TryGetComponent(out _lightComponent)) + { + _originalColor = _lightComponent.Color; + _originalEnabled = _lightComponent.Enabled; + _originalEnergy = _lightComponent.Energy; + _originalRadius = _lightComponent.Radius; + _originalRotation = _lightComponent.Rotation; + } + else + { + Logger.Warning($"{Owner.Name} has a {nameof(LightBehaviourComponent)} but it has no {nameof(PointLightComponent)}! Check the prototype!"); + } + } + + /// + /// Start animating a light behaviour with the specified ID. If the specified ID is empty, it will start animating all light behaviour entries. + /// If specified light behaviours are already animating, calling this does nothing. + /// Multiple light behaviours can have the same ID. + /// + public void StartLightBehaviour(string id = "") + { + foreach (var container in _animations) + { + if (container.LightBehaviour.ID == id || id == string.Empty) + { + if (!_animationPlayer.HasRunningAnimation(KeyPrefix + container.Key)) + { + container.LightBehaviour.UpdatePlaybackValues(container.Animation); + _animationPlayer.Play(container.Animation, KeyPrefix + container.Key); + } + } + } + } + + /// + /// If any light behaviour with the specified ID is animating, then stop it. + /// If no ID is specified then all light behaviours will be stopped. + /// Multiple light behaviours can have the same ID. + /// + /// + /// Should the behaviour(s) also be removed permanently? + /// Should the light have its original settings applied? + public void StopLightBehaviour(string id = "", bool removeBehaviour = false, bool resetToOriginalSettings = false) + { + var toRemove = new List(); + + foreach (var container in _animations) + { + if (container.LightBehaviour.ID == id || id == string.Empty) + { + if (_animationPlayer.HasRunningAnimation(KeyPrefix + container.Key)) + { + _animationPlayer.Stop(KeyPrefix + container.Key); + } + + if (removeBehaviour) + { + toRemove.Add(container); + } + } + } + + foreach (var container in toRemove) + { + _animations.Remove(container); + } + + if (resetToOriginalSettings) + { + _lightComponent.Color = _originalColor; + _lightComponent.Enabled = _originalEnabled; + _lightComponent.Energy = _originalEnergy; + _lightComponent.Radius = _originalRadius; + _lightComponent.Rotation = _originalRotation; + } + } + + /// + /// Add a new light behaviour to the component and start it immediately unless otherwise specified. + /// + public void AddNewLightBehaviour(LightBehaviourAnimationTrack behaviour, bool playImmediately = true) + { + int key = 0; + + while (_animations.Any(x => x.Key == key)) + { + key++; + } + + var animation = new Animation() + { + AnimationTracks = { behaviour } + }; + + behaviour.Initialize(_lightComponent); + var container = new AnimationContainer(key, animation, behaviour); + _animations.Add(container); + + if (playImmediately) + { + StartLightBehaviour(behaviour.ID); + } + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + var behaviours = serializer.ReadDataField("behaviours", new List()); + var key = 0; + + foreach (LightBehaviourAnimationTrack behaviour in behaviours) + { + var animation = new Animation() + { + AnimationTracks = { behaviour } + }; + + _animations.Add(new AnimationContainer(key, animation, behaviour)); + key++; + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Interactable/ExpendableLightComponent.cs b/Content.Server/GameObjects/Components/Interactable/ExpendableLightComponent.cs new file mode 100644 index 0000000000..da34c87cb6 --- /dev/null +++ b/Content.Server/GameObjects/Components/Interactable/ExpendableLightComponent.cs @@ -0,0 +1,221 @@ + +using Content.Server.GameObjects.Components.Items.Clothing; +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.Sound; +using Content.Shared.GameObjects.Components; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.GameObjects.Verbs; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.EntitySystemMessages; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.ViewVariables; +using Robust.Shared.Audio; + +namespace Content.Server.GameObjects.Components.Interactable +{ + /// + /// Component that represents a handheld expendable light which can be activated and eventually dies over time. + /// + [RegisterComponent] + public class ExpendableLightComponent : SharedExpendableLightComponent, IUse + { + private static readonly AudioParams LoopedSoundParams = new AudioParams(0, 1, "Master", 62.5f, 1, AudioMixTarget.Stereo, true, 0.3f); + + /// + /// Status of light, whether or not it is emitting light. + /// + [ViewVariables] + public bool Activated => CurrentState == ExpendableLightState.Lit || CurrentState == ExpendableLightState.Fading; + + [ViewVariables] + private float _stateExpiryTime = default; + private AppearanceComponent _appearance = default; + + bool IUse.UseEntity(UseEntityEventArgs eventArgs) + { + return TryActivate(); + } + + public override void Initialize() + { + base.Initialize(); + + if (Owner.TryGetComponent(out var item)) + { + item.EquippedPrefix = "off"; + } + + CurrentState = ExpendableLightState.BrandNew; + Owner.EnsureComponent(); + Owner.TryGetComponent(out _appearance); + } + + /// + /// Enables the light if it is not active. Once active it cannot be turned off. + /// + private bool TryActivate() + { + if (!Activated) + { + if (Owner.TryGetComponent(out var item)) + { + item.EquippedPrefix = "on"; + } + + CurrentState = ExpendableLightState.Lit; + _stateExpiryTime = GlowDuration; + + UpdateSpriteAndSounds(Activated); + UpdateVisualizer(); + + return true; + } + + return false; + } + + private void UpdateVisualizer() + { + switch (CurrentState) + { + case ExpendableLightState.Lit: + _appearance.SetData(ExpendableLightVisuals.State, TurnOnBehaviourID); + break; + + case ExpendableLightState.Fading: + _appearance.SetData(ExpendableLightVisuals.State, FadeOutBehaviourID); + break; + + case ExpendableLightState.Dead: + _appearance.SetData(ExpendableLightVisuals.State, string.Empty); + break; + + default: + break; + } + } + + private void UpdateSpriteAndSounds(bool on) + { + if (Owner.TryGetComponent(out SpriteComponent sprite)) + { + switch (CurrentState) + { + case ExpendableLightState.Lit: + + if (LoopedSound != string.Empty && Owner.TryGetComponent(out var loopingSound)) + { + loopingSound.Play(LoopedSound, LoopedSoundParams); + } + + if (LitSound != string.Empty) + { + EntitySystem.Get().PlayFromEntity(LitSound, Owner); + } + + sprite.LayerSetVisible(1, true); + sprite.LayerSetState(2, IconStateLit); + sprite.LayerSetShader(2, "unshaded"); + break; + + case ExpendableLightState.Fading: + break; + + default: + case ExpendableLightState.Dead: + + if (DieSound != string.Empty) + { + EntitySystem.Get().PlayFromEntity(DieSound, Owner); + } + + if (LoopedSound != string.Empty && Owner.TryGetComponent(out var loopSound)) + { + loopSound.StopAllSounds(); + } + + sprite.LayerSetVisible(1, false); + sprite.LayerSetState(2, IconStateSpent); + sprite.LayerSetShader(2, "shaded"); + break; + } + } + + if (Owner.TryGetComponent(out ClothingComponent clothing)) + { + clothing.ClothingEquippedPrefix = on ? "Activated" : string.Empty; + } + } + + public void Update(float frameTime) + { + if (!Activated) return; + + _stateExpiryTime -= frameTime; + + if (_stateExpiryTime <= 0f) + { + switch (CurrentState) + { + case ExpendableLightState.Lit: + + CurrentState = ExpendableLightState.Fading; + _stateExpiryTime = FadeOutDuration; + + UpdateVisualizer(); + + break; + + default: + case ExpendableLightState.Fading: + + CurrentState = ExpendableLightState.Dead; + Owner.Name = SpentName; + Owner.Description = SpentDesc; + + UpdateSpriteAndSounds(Activated); + UpdateVisualizer(); + + if (Owner.TryGetComponent(out var item)) + { + item.EquippedPrefix = "off"; + } + + break; + } + } + } + + [Verb] + public sealed class ActivateVerb : Verb + { + protected override void GetData(IEntity user, ExpendableLightComponent component, VerbData data) + { + if (!ActionBlockerSystem.CanInteract(user)) + { + data.Visibility = VerbVisibility.Invisible; + return; + } + + if (component.CurrentState == ExpendableLightState.BrandNew) + { + data.Text = "Activate"; + data.Visibility = VerbVisibility.Visible; + } + else + { + data.Visibility = VerbVisibility.Invisible; + } + } + + protected override void Activate(IEntity user, ExpendableLightComponent component) + { + component.TryActivate(); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/LightBehaviourComponent.cs b/Content.Server/GameObjects/Components/LightBehaviourComponent.cs new file mode 100644 index 0000000000..0ab3b8951c --- /dev/null +++ b/Content.Server/GameObjects/Components/LightBehaviourComponent.cs @@ -0,0 +1,15 @@ + +using Robust.Shared.GameObjects; +using Content.Shared.GameObjects.Components; + +namespace Content.Server.GameObjects.Components +{ + /// + /// A component which applies a specific behaviour to a PointLightComponent on its owner. + /// + [RegisterComponent] + public class LightBehaviourComponent : SharedLightBehaviourComponent + { + + } +} diff --git a/Content.Server/GameObjects/EntitySystems/ExpendableLightSystem.cs b/Content.Server/GameObjects/EntitySystems/ExpendableLightSystem.cs new file mode 100644 index 0000000000..70927c4975 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/ExpendableLightSystem.cs @@ -0,0 +1,18 @@ + +using JetBrains.Annotations; +using Robust.Shared.GameObjects.Systems; +using Content.Server.GameObjects.Components.Interactable; + +namespace Content.Server.GameObjects.EntitySystems +{ + public class ExpendableLightSystem : EntitySystem + { + public override void Update(float frameTime) + { + foreach (var light in ComponentManager.EntityQuery()) + { + light.Update(frameTime); + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/SharedExpendableLightComponent.cs b/Content.Shared/GameObjects/Components/SharedExpendableLightComponent.cs new file mode 100644 index 0000000000..5db6ad8f9b --- /dev/null +++ b/Content.Shared/GameObjects/Components/SharedExpendableLightComponent.cs @@ -0,0 +1,80 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.GameObjects.Components +{ + [Serializable, NetSerializable] + public enum ExpendableLightVisuals + { + State + } + + [Serializable, NetSerializable] + public enum ExpendableLightState + { + BrandNew, + Lit, + Fading, + Dead + } + + public abstract class SharedExpendableLightComponent: Component + { + public sealed override string Name => "ExpendableLight"; + + [ViewVariables(VVAccess.ReadOnly)] + protected ExpendableLightState CurrentState { get; set; } + + [ViewVariables] + protected string TurnOnBehaviourID { get; set; } + + [ViewVariables] + protected string FadeOutBehaviourID { get; set; } + + [ViewVariables] + protected float GlowDuration { get; set; } + + [ViewVariables] + protected float FadeOutDuration { get; set; } + + [ViewVariables] + protected string SpentDesc { get; set; } + + [ViewVariables] + protected string SpentName { get; set; } + + [ViewVariables] + protected string IconStateSpent { get; set; } + + [ViewVariables] + protected string IconStateLit { get; set; } + + [ViewVariables] + protected string LitSound { get; set; } + + [ViewVariables] + protected string LoopedSound { get; set; } + + [ViewVariables] + protected string DieSound { get; set; } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(this, x => TurnOnBehaviourID, "turnOnBehaviourID", string.Empty); + serializer.DataField(this, x => FadeOutBehaviourID, "fadeOutBehaviourID", string.Empty); + serializer.DataField(this, x => GlowDuration, "glowDuration", 60 * 15f); + serializer.DataField(this, x => FadeOutDuration, "fadeOutDuration", 60 * 5f); + serializer.DataField(this, x => SpentName, "spentName", string.Empty); + serializer.DataField(this, x => SpentDesc, "spentDesc", string.Empty); + serializer.DataField(this, x => IconStateLit, "iconStateOn", string.Empty); + serializer.DataField(this, x => IconStateSpent, "iconStateSpent", string.Empty); + serializer.DataField(this, x => LitSound, "litSound", string.Empty); + serializer.DataField(this, x => LoopedSound, "loopedSound", string.Empty); + serializer.DataField(this, x => DieSound, "dieSound", string.Empty); + } + } +} diff --git a/Content.Shared/GameObjects/Components/SharedLightBehaviourComponent.cs b/Content.Shared/GameObjects/Components/SharedLightBehaviourComponent.cs new file mode 100644 index 0000000000..9965cced26 --- /dev/null +++ b/Content.Shared/GameObjects/Components/SharedLightBehaviourComponent.cs @@ -0,0 +1,13 @@ + +using Robust.Shared.GameObjects; + +namespace Content.Shared.GameObjects.Components +{ + /// + /// A component which applies a specific behaviour to a PointLightComponent on its owner. + /// + public class SharedLightBehaviourComponent : Component + { + public override string Name => "LightBehaviour"; + } +} diff --git a/Resources/Audio/Items/Flare/flare_burn.ogg b/Resources/Audio/Items/Flare/flare_burn.ogg new file mode 100644 index 0000000000..ea20d8d1f1 Binary files /dev/null and b/Resources/Audio/Items/Flare/flare_burn.ogg differ diff --git a/Resources/Audio/Items/Flare/flare_on.ogg b/Resources/Audio/Items/Flare/flare_on.ogg new file mode 100644 index 0000000000..f855723f47 Binary files /dev/null and b/Resources/Audio/Items/Flare/flare_on.ogg differ diff --git a/Resources/Prototypes/Entities/Objects/Tools/flare.yml b/Resources/Prototypes/Entities/Objects/Tools/flare.yml new file mode 100644 index 0000000000..e9311b8b39 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Tools/flare.yml @@ -0,0 +1,77 @@ +- type: entity + name: emergency flare # todo: we need some sort of IgnitionSourceComponent we can add to this, so when it's lit it will cause fires when touching fuel + parent: BaseItem + id: FlareBase + description: A flare that produces a very bright light for a short while. Point the flame away from yourself. + components: + - type: ExpendableLight + spentName: spent flare + spentDesc: It looks like this flare has burnt out. What a bummer. + glowDuration: 400 + fadeOutDuration: 4 + iconStateOn: flare_unlit + iconStateSpent: flare_spent + turnOnBehaviourID: turn_on + fadeOutBehaviourID: fade_out + litSound: /Audio/Items/Flare/flare_on.ogg + loopedSound: /Audio/Items/Flare/flare_burn.ogg + - type: Sprite + sprite: Objects/Misc/flare.rsi + layers: + - state: flare_base + - state: flare_burn + color: "#FFFFFF" + visible: false + shader: unshaded + - state: flare_unlit + color: "#FF0000" + - type: Icon + sprite: Objects/Misc/flare.rsi + state: flare_spent + - type: Item + sprite: Objects/Misc/flare.rsi + color: "#FF0000" + HeldPrefix: off + - type: LoopingSound + - type: Appearance + visuals: + - type: ExpendableLightVisualizer + - type: PointLight + enabled: false + color: "#FF8080" + radius: 1.0 + energy: 9.0 + - type: LightBehaviour + behaviours: + - !type:RandomizeBehaviour # immediately make it bright and flickery + id: turn_on + interpolate: Nearest + minDuration: 0.02 + maxDuration: 0.06 + startValue: 6.0 + endValue: 9.0 + property: Energy + isLooped: true + - !type:FadeBehaviour # have the radius start small and get larger as it starts to burn + id: turn_on + interpolate: Linear + maxDuration: 8.0 + startValue: 1.0 + endValue: 6.0 + property: Radius + - !type:RandomizeBehaviour # weaker flicker as it fades out + id: fade_out + interpolate: Nearest + minDuration: 0.02 + maxDuration: 0.06 + startValue: 4.0 + endValue: 8.0 + property: Energy + isLooped: true + - !type:FadeBehaviour # fade out radius as it burns out + id: fade_out + interpolate: Linear + maxDuration: 4.0 + startValue: 6.0 + endValue: 1.0 + property: Radius \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml b/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml new file mode 100644 index 0000000000..6b42ea0bbe --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml @@ -0,0 +1,417 @@ +- type: entity + name: green glowstick + parent: BaseItem + id: GlowstickBase + description: Useful for raves and emergencies. + components: + - type: ExpendableLight + spentName: spent green glowstick + spentDesc: It looks like this glowstick has stopped glowing. How tragic. + glowDuration: 900 # time in seconds + fadeOutDuration: 300 + iconStateOn: glowstick_lit + iconStateSpent: glowstick_unlit + turnOnBehaviourID: turn_on + fadeOutBehaviourID: fade_out + litSound: /Audio/Items/Handcuffs/rope_breakout.ogg + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_glow + color: "#00FF00" + visible: false + shader: unshaded + - state: glowstick_unlit + color: "#00FF00" + - type: Icon + sprite: Objects/Misc/glowstick.rsi + state: glowstick_unlit + - type: Item + sprite: Objects/Misc/glowstick.rsi + color: "#00FF00" + HeldPrefix: off + - type: Appearance + visuals: + - type: ExpendableLightVisualizer + - type: PointLight + enabled: false + color: "#00FF00" + radius: 5 + energy: 0 + - type: LightBehaviour + behaviours: + - !type:FadeBehaviour # slowly fade in once activated + id: turn_on + interpolate: Linear + maxDuration: 10.0 + startValue: 0.0 + endValue: 3.0 + property: Energy + - !type:FadeBehaviour # fade out energy as it burns out + id: fade_out + interpolate: Linear + maxDuration: 10 # 300.0 + startValue: 3.0 + endValue: 0.2 + property: Energy + - !type:FadeBehaviour # fade out radius as it burns out + id: fade_out + interpolate: Linear + maxDuration: 10 # 300.0 + startValue: 5.0 + endValue: 1.5 + property: Radius + +- type: entity + name: red glowstick + parent: GlowstickBase + id: GlowstickRed + components: + - type: ExpendableLight + spentName: spent red glowstick + spentDesc: It looks like this glowstick has stopped glowing. How tragic. + glowDuration: 900 + fadeOutDuration: 300 + iconStateOn: glowstick_lit + iconStateSpent: glowstick_unlit + turnOnBehaviourID: turn_on + fadeOutBehaviourID: fade_out + litSound: /Audio/Items/Handcuffs/rope_breakout.ogg + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_glow + color: "#FF0000" + visible: false + shader: unshaded + - state: glowstick_unlit + color: "#FF0000" + - type: Item + sprite: Objects/Misc/glowstick.rsi + color: "#FF0000" + HeldPrefix: off + - type: PointLight + enabled: false + color: "#FF0000" + radius: 5 + energy: 0 + +- type: entity + name: purple glowstick + parent: GlowstickBase + id: GlowstickPurple + components: + - type: ExpendableLight + spentName: spent purple glowstick + spentDesc: It looks like this glowstick has stopped glowing. How tragic. + glowDuration: 900 + fadeOutDuration: 300 + iconStateOn: glowstick_lit + iconStateSpent: glowstick_unlit + turnOnBehaviourID: turn_on + fadeOutBehaviourID: fade_out + litSound: /Audio/Items/Handcuffs/rope_breakout.ogg + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_glow + color: "#FF00FF" + visible: false + shader: unshaded + - state: glowstick_unlit + color: "#FF00FF" + - type: Item + sprite: Objects/Misc/glowstick.rsi + color: "#FF00FF" + HeldPrefix: off + - type: PointLight + enabled: false + color: "#FF00FF" + radius: 5 + energy: 0 + +- type: entity + name: yellow glowstick + parent: GlowstickBase + id: GlowstickYellow + components: + - type: ExpendableLight + spentName: spent yellow glowstick + spentDesc: It looks like this glowstick has stopped glowing. How tragic. + glowDuration: 900 + fadeOutDuration: 300 + iconStateOn: glowstick_lit + iconStateSpent: glowstick_unlit + turnOnBehaviourID: turn_on + fadeOutBehaviourID: fade_out + litSound: /Audio/Items/Handcuffs/rope_breakout.ogg + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_glow + color: "#FFFF00" + visible: false + shader: unshaded + - state: glowstick_unlit + color: "#FFFF00" + - type: Item + sprite: Objects/Misc/glowstick.rsi + color: "#FFFF00" + HeldPrefix: off + - type: PointLight + enabled: false + color: "#FFFF00" + radius: 5 + energy: 0 + +- type: entity + name: blue glowstick + parent: GlowstickBase + id: GlowstickBlue + components: + - type: ExpendableLight + spentName: spent blue glowstick + spentDesc: It looks like this glowstick has stopped glowing. How tragic. + glowDuration: 900 + fadeOutDuration: 300 + iconStateOn: glowstick_lit + iconStateSpent: glowstick_unlit + turnOnBehaviourID: turn_on + fadeOutBehaviourID: fade_out + litSound: /Audio/Items/Handcuffs/rope_breakout.ogg + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_glow + color: "#0000FF" + visible: false + shader: unshaded + - state: glowstick_unlit + color: "#0000FF" + - type: Item + sprite: Objects/Misc/glowstick.rsi + color: "#0000FF" + HeldPrefix: off + - type: PointLight + enabled: false + color: "#0000FF" + radius: 5 + energy: 0 + +# ---------------------------------------------------------------------------- +# THE FOLLOWING ARE ALL DUMMY ENTITIES USED TO TEST THE LIGHT BEHAVIOUR SYSTEM +# ---------------------------------------------------------------------------- +- type: entity + name: light pulse test + parent: BaseItem + id: LightBehaviourTest1 + components: + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_unlit + shader: unshaded + color: "#FF0000" + - type: Item + sprite: Objects/Misc/glowstick.rsi + HeldPrefix: off + - type: Icon + sprite: Objects/Misc/glowstick.rsi + state: glowstick_unlit + - type: PointLight + enabled: true + color: "#FF0000" + radius: 5 + - type: LightBehaviour + behaviours: + - !type:PulseBehaviour + interpolate: Cubic + maxDuration: 10.0 + minValue: 1.0 + maxValue: 7.0 + isLooped: true + property: Energy + enabled: true + +- type: entity + name: color cycle test + parent: BaseItem + id: LightBehaviourTest2 + components: + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_unlit + shader: unshaded + color: "#FF0000" + - type: Item + sprite: Objects/Misc/glowstick.rsi + HeldPrefix: off + - type: Icon + sprite: Objects/Misc/glowstick.rsi + state: glowstick_unlit + - type: PointLight + enabled: true + color: "#FF0000" + radius: 5 + - type: LightBehaviour + behaviours: + - !type:ColorCycleBehaviour + interpolate: Nearest + maxDuration: 0.8 + isLooped: true + enabled: true + colors: + - red + - blue + - green + +- type: entity + name: multi-behaviour light test + parent: BaseItem + id: LightBehaviourTest3 + components: + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_unlit + shader: unshaded + color: "#FF0000" + - type: Item + sprite: Objects/Misc/glowstick.rsi + HeldPrefix: off + - type: Icon + sprite: Objects/Misc/glowstick.rsi + state: glowstick_unlit + - type: PointLight + enabled: false + color: "#FF0000" + radius: 5 + - type: LightBehaviour + behaviours: + - !type:PulseBehaviour + interpolate: Nearest + minDuration: 0.2 + maxDuration: 1.0 + maxValue: 0.2 + property: Enabled + isLooped: true + enabled: true + - !type:ColorCycleBehaviour + interpolate: Cubic + maxDuration: 0.8 + isLooped: true + enabled: true + colors: + - red + - blue + - green + +- type: entity + name: light fade in test + parent: BaseItem + id: LightBehaviourTest4 + components: + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_unlit + shader: unshaded + color: "#FF0000" + - type: Item + sprite: Objects/Misc/glowstick.rsi + HeldPrefix: off + - type: Icon + sprite: Objects/Misc/glowstick.rsi + state: glowstick_unlit + - type: PointLight + enabled: false + color: "#FF0000" + radius: 5 + - type: LightBehaviour + behaviours: + - !type:FadeBehaviour + interpolate: Cubic + maxDuration: 5.0 + minValue: 0.0 + maxValue: 10.0 + isLooped: true + property: Energy + enabled: true + +- type: entity + name: light pulse radius test + parent: BaseItem + id: LightBehaviourTest5 + components: + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_unlit + shader: unshaded + color: "#FF0000" + - type: Item + sprite: Objects/Misc/glowstick.rsi + HeldPrefix: off + - type: Icon + sprite: Objects/Misc/glowstick.rsi + state: glowstick_unlit + - type: PointLight + enabled: false + color: "#FF0000" + radius: 5 + - type: LightBehaviour + behaviours: + - !type:PulseBehaviour + interpolate: Cubic + minDuration: 1.0 + maxDuration: 5.0 + minValue: 2.0 + maxValue: 10.0 + isLooped: true + property: Radius + enabled: true + +- type: entity + name: light randomize radius test + parent: BaseItem + id: LightBehaviourTest6 + components: + - type: Sprite + sprite: Objects/Misc/glowstick.rsi + layers: + - state: glowstick_base + - state: glowstick_unlit + shader: unshaded + color: "#FF0000" + - type: Item + sprite: Objects/Misc/glowstick.rsi + HeldPrefix: off + - type: Icon + sprite: Objects/Misc/glowstick.rsi + state: glowstick_unlit + - type: PointLight + enabled: false + color: "#FF0000" + radius: 5 + energy: 10 + - type: LightBehaviour + behaviours: + - !type:RandomizeBehaviour + interpolate: Nearest + maxDuration: 0.5 + minValue: 10.0 + maxValue: 25.0 + isLooped: true + property: Radius + enabled: true \ No newline at end of file diff --git a/Resources/Textures/Objects/Misc/flare.rsi/flare_base.png b/Resources/Textures/Objects/Misc/flare.rsi/flare_base.png new file mode 100644 index 0000000000..9701612e68 Binary files /dev/null and b/Resources/Textures/Objects/Misc/flare.rsi/flare_base.png differ diff --git a/Resources/Textures/Objects/Misc/flare.rsi/flare_burn.png b/Resources/Textures/Objects/Misc/flare.rsi/flare_burn.png new file mode 100644 index 0000000000..352e47d043 Binary files /dev/null and b/Resources/Textures/Objects/Misc/flare.rsi/flare_burn.png differ diff --git a/Resources/Textures/Objects/Misc/flare.rsi/flare_spent.png b/Resources/Textures/Objects/Misc/flare.rsi/flare_spent.png new file mode 100644 index 0000000000..eb35b0b7f9 Binary files /dev/null and b/Resources/Textures/Objects/Misc/flare.rsi/flare_spent.png differ diff --git a/Resources/Textures/Objects/Misc/flare.rsi/flare_unlit.png b/Resources/Textures/Objects/Misc/flare.rsi/flare_unlit.png new file mode 100644 index 0000000000..199c041e19 Binary files /dev/null and b/Resources/Textures/Objects/Misc/flare.rsi/flare_unlit.png differ diff --git a/Resources/Textures/Objects/Misc/flare.rsi/meta.json b/Resources/Textures/Objects/Misc/flare.rsi/meta.json new file mode 100644 index 0000000000..ad01494edd --- /dev/null +++ b/Resources/Textures/Objects/Misc/flare.rsi/meta.json @@ -0,0 +1,126 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Sprites created by https://github.com/nuke-makes-games", + "states": + [ + { + "name": "off-inhand-left", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "off-inhand-right", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "on-inhand-left", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "on-inhand-right", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "flare_burn", + "select": [], + "flags": {}, + "directions": 1, + "delays": [ + [ + 0.05, + 0.05, + 0.05, + 0.05 + ] + ] + }, + { + "name": "flare_spent", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + }, + { + "name": "flare_base", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + }, + { + "name": "flare_unlit", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + } + ] +} + diff --git a/Resources/Textures/Objects/Misc/flare.rsi/off-inhand-left.png b/Resources/Textures/Objects/Misc/flare.rsi/off-inhand-left.png new file mode 100644 index 0000000000..251bd852bb Binary files /dev/null and b/Resources/Textures/Objects/Misc/flare.rsi/off-inhand-left.png differ diff --git a/Resources/Textures/Objects/Misc/flare.rsi/off-inhand-right.png b/Resources/Textures/Objects/Misc/flare.rsi/off-inhand-right.png new file mode 100644 index 0000000000..f019038811 Binary files /dev/null and b/Resources/Textures/Objects/Misc/flare.rsi/off-inhand-right.png differ diff --git a/Resources/Textures/Objects/Misc/flare.rsi/on-inhand-left.png b/Resources/Textures/Objects/Misc/flare.rsi/on-inhand-left.png new file mode 100644 index 0000000000..7d5e391e47 Binary files /dev/null and b/Resources/Textures/Objects/Misc/flare.rsi/on-inhand-left.png differ diff --git a/Resources/Textures/Objects/Misc/flare.rsi/on-inhand-right.png b/Resources/Textures/Objects/Misc/flare.rsi/on-inhand-right.png new file mode 100644 index 0000000000..56e77879d0 Binary files /dev/null and b/Resources/Textures/Objects/Misc/flare.rsi/on-inhand-right.png differ diff --git a/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_base.png b/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_base.png new file mode 100644 index 0000000000..9148f4e09c Binary files /dev/null and b/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_base.png differ diff --git a/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_glow.png b/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_glow.png new file mode 100644 index 0000000000..e5aa5d029d Binary files /dev/null and b/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_glow.png differ diff --git a/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_lit.png b/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_lit.png new file mode 100644 index 0000000000..44476eae3e Binary files /dev/null and b/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_lit.png differ diff --git a/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_unlit.png b/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_unlit.png new file mode 100644 index 0000000000..87c7e26383 Binary files /dev/null and b/Resources/Textures/Objects/Misc/glowstick.rsi/glowstick_unlit.png differ diff --git a/Resources/Textures/Objects/Misc/glowstick.rsi/meta.json b/Resources/Textures/Objects/Misc/glowstick.rsi/meta.json new file mode 100644 index 0000000000..f6822ba045 --- /dev/null +++ b/Resources/Textures/Objects/Misc/glowstick.rsi/meta.json @@ -0,0 +1,121 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Sprites created by https://github.com/nuke-makes-games", + "states": + [ + { + "name": "off-inhand-left", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "off-inhand-right", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "on-inhand-left", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "on-inhand-right", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "glowstick_base", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + }, + { + "name": "glowstick_lit", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + }, + { + "name": "glowstick_glow", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + }, + { + "name": "glowstick_unlit", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + } + ] +} + diff --git a/Resources/Textures/Objects/Misc/glowstick.rsi/off-inhand-left.png b/Resources/Textures/Objects/Misc/glowstick.rsi/off-inhand-left.png new file mode 100644 index 0000000000..251bd852bb Binary files /dev/null and b/Resources/Textures/Objects/Misc/glowstick.rsi/off-inhand-left.png differ diff --git a/Resources/Textures/Objects/Misc/glowstick.rsi/off-inhand-right.png b/Resources/Textures/Objects/Misc/glowstick.rsi/off-inhand-right.png new file mode 100644 index 0000000000..f019038811 Binary files /dev/null and b/Resources/Textures/Objects/Misc/glowstick.rsi/off-inhand-right.png differ diff --git a/Resources/Textures/Objects/Misc/glowstick.rsi/on-inhand-left.png b/Resources/Textures/Objects/Misc/glowstick.rsi/on-inhand-left.png new file mode 100644 index 0000000000..e6528b346d Binary files /dev/null and b/Resources/Textures/Objects/Misc/glowstick.rsi/on-inhand-left.png differ diff --git a/Resources/Textures/Objects/Misc/glowstick.rsi/on-inhand-right.png b/Resources/Textures/Objects/Misc/glowstick.rsi/on-inhand-right.png new file mode 100644 index 0000000000..0d26e9dca9 Binary files /dev/null and b/Resources/Textures/Objects/Misc/glowstick.rsi/on-inhand-right.png differ