diff --git a/Content.Client/Light/Components/LightBehaviourComponent.cs b/Content.Client/Light/Components/LightBehaviourComponent.cs
index dbd77a846c..a4a45d01d6 100644
--- a/Content.Client/Light/Components/LightBehaviourComponent.cs
+++ b/Content.Client/Light/Components/LightBehaviourComponent.cs
@@ -177,6 +177,14 @@ namespace Content.Client.Light.Components
[UsedImplicitly]
public sealed class FadeBehaviour : LightBehaviourAnimationTrack
{
+ ///
+ /// Automatically reverse the animation when EndValue is reached. In this particular case, MaxTime specifies the
+ /// time of the full animation, including the reverse interpolation.
+ ///
+ [DataField("reverseWhenFinished")]
+ [ViewVariables]
+ public bool ReverseWhenFinished { get; set; }
+
public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback(
object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime)
{
@@ -189,22 +197,42 @@ namespace Content.Client.Light.Components
return (-1, playingTime);
}
- switch (InterpolateMode)
+ // From 0 to MaxTime/2, we go from StartValue to EndValue. From MaxTime/2 to MaxTime, we reverse this interpolation.
+ if (ReverseWhenFinished)
{
- 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;
+ if (interpolateValue < 0.5f)
+ {
+ ApplyInterpolation(StartValue, EndValue, interpolateValue*2);
+ }
+ else
+ {
+ ApplyInterpolation(EndValue, StartValue, (interpolateValue-0.5f)*2);
+ }
+ }
+ else
+ {
+ ApplyInterpolation(StartValue, EndValue, interpolateValue);
}
return (-1, playingTime);
}
+
+ private void ApplyInterpolation(float start, float end, float interpolateValue)
+ {
+ switch (InterpolateMode)
+ {
+ case AnimationInterpolationMode.Linear:
+ ApplyProperty(InterpolateLinear(start, end, interpolateValue));
+ break;
+ case AnimationInterpolationMode.Cubic:
+ ApplyProperty(InterpolateCubic(end, start, end, start, interpolateValue));
+ break;
+ default:
+ case AnimationInterpolationMode.Nearest:
+ ApplyProperty(interpolateValue < 0.5f ? start : end);
+ break;
+ }
+ }
}
///
@@ -369,11 +397,8 @@ namespace Content.Client.Light.Components
[ViewVariables(VVAccess.ReadOnly)]
private readonly List _animations = new();
- private float _originalRadius;
- private float _originalEnergy;
- private Angle _originalRotation;
- private Color _originalColor;
- private bool _originalEnabled;
+ [ViewVariables(VVAccess.ReadOnly)]
+ private Dictionary _originalPropertyValues = new();
void ISerializationHooks.AfterDeserialization()
{
@@ -395,8 +420,6 @@ namespace Content.Client.Light.Components
{
base.Startup();
- CopyLightSettings();
-
// TODO: Do NOT ensure component here. And use eventbus events instead...
Owner.EnsureComponent();
@@ -445,15 +468,15 @@ namespace Content.Client.Light.Components
///
/// If we disable all the light behaviours we want to be able to revert the light to its original state.
///
- private void CopyLightSettings()
+ private void CopyLightSettings(string property)
{
if (_entMan.TryGetComponent(Owner, out PointLightComponent? light))
{
- _originalColor = light.Color;
- _originalEnabled = light.Enabled;
- _originalEnergy = light.Energy;
- _originalRadius = light.Radius;
- _originalRotation = light.Rotation;
+ var propertyValue = AnimationHelper.GetAnimatableProperty(light, property);
+ if (propertyValue != null)
+ {
+ _originalPropertyValues.Add(property, propertyValue);
+ }
}
else
{
@@ -479,6 +502,7 @@ namespace Content.Client.Light.Components
{
if (!animation.HasRunningAnimation(KeyPrefix + container.Key))
{
+ CopyLightSettings(container.LightBehaviour.Property);
container.LightBehaviour.UpdatePlaybackValues(container.Animation);
animation.Play(container.Animation, KeyPrefix + container.Key);
}
@@ -526,12 +550,27 @@ namespace Content.Client.Light.Components
if (resetToOriginalSettings && _entMan.TryGetComponent(Owner, out PointLightComponent? light))
{
- light.Color = _originalColor;
- light.Enabled = _originalEnabled;
- light.Energy = _originalEnergy;
- light.Radius = _originalRadius;
- light.Rotation = _originalRotation;
+ foreach (var (property, value) in _originalPropertyValues)
+ {
+ AnimationHelper.SetAnimatableProperty(light, property, value);
+ }
}
+
+ _originalPropertyValues.Clear();
+ }
+
+ ///
+ /// Checks if at least one behaviour is running.
+ ///
+ /// Whether at least one behaviour is running, false if none is.
+ public bool HasRunningBehaviours()
+ {
+ if (!_entMan.TryGetComponent(Owner, out AnimationPlayerComponent? animation))
+ {
+ return false;
+ }
+
+ return _animations.Any(container => animation.HasRunningAnimation(KeyPrefix + container.Key));
}
///
diff --git a/Content.Client/Light/HandheldLightSystem.cs b/Content.Client/Light/HandheldLightSystem.cs
index a78464abb9..d8a04aa5a1 100644
--- a/Content.Client/Light/HandheldLightSystem.cs
+++ b/Content.Client/Light/HandheldLightSystem.cs
@@ -1,7 +1,10 @@
using Content.Client.Items;
using Content.Client.Light.Components;
using Content.Shared.Light;
-using Content.Shared.Light.Component;
+using Content.Shared.Toggleable;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Shared.Animations;
namespace Content.Client.Light;
@@ -12,10 +15,56 @@ public sealed class HandheldLightSystem : SharedHandheldLightSystem
base.Initialize();
SubscribeLocalEvent(OnGetStatusControl);
+ SubscribeLocalEvent(OnAppearanceChange);
}
-
+
private static void OnGetStatusControl(EntityUid uid, HandheldLightComponent component, ItemStatusCollectMessage args)
{
args.Controls.Add(new HandheldLightStatus(component));
}
+
+ private void OnAppearanceChange(EntityUid uid, HandheldLightComponent? component, ref AppearanceChangeEvent args)
+ {
+ if (!Resolve(uid, ref component))
+ {
+ return;
+ }
+
+ if (!args.Component.TryGetData(ToggleableLightVisuals.Enabled, out bool enabled))
+ {
+ return;
+ }
+
+ if (!args.Component.TryGetData(HandheldLightVisuals.Power,
+ out HandheldLightPowerStates state))
+ {
+ return;
+ }
+
+ if (TryComp(uid, out var lightBehaviour))
+ {
+ // Reset any running behaviour to reset the animated properties back to the original value, to avoid conflicts between resets
+ if (lightBehaviour.HasRunningBehaviours())
+ {
+ lightBehaviour.StopLightBehaviour(resetToOriginalSettings: true);
+ }
+
+ if (!enabled)
+ {
+ return;
+ }
+
+ switch (state)
+ {
+ case HandheldLightPowerStates.FullPower:
+ break; // We just needed to reset all behaviours
+ case HandheldLightPowerStates.LowPower:
+ lightBehaviour.StartLightBehaviour(component.RadiatingBehaviourId);
+ break;
+ case HandheldLightPowerStates.Dying:
+ lightBehaviour.StartLightBehaviour(component.BlinkingBehaviourId);
+ break;
+ }
+ }
+ }
}
diff --git a/Content.Client/Light/LanternVisualizer.cs b/Content.Client/Light/LanternVisualizer.cs
deleted file mode 100644
index 5c6bc03305..0000000000
--- a/Content.Client/Light/LanternVisualizer.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System;
-using JetBrains.Annotations;
-using Robust.Client.Animations;
-using Robust.Client.GameObjects;
-using Robust.Shared.Animations;
-using Robust.Shared.GameObjects;
-
-namespace Content.Client.Light
-{
- [UsedImplicitly]
- public sealed class LanternVisualizer : AppearanceVisualizer
- {
- private readonly Animation _radiatingLightAnimation = new()
- {
- Length = TimeSpan.FromSeconds(5),
- AnimationTracks =
- {
- new AnimationTrackComponentProperty
- {
- ComponentType = typeof(PointLightComponent),
- InterpolationMode = AnimationInterpolationMode.Linear,
- Property = nameof(PointLightComponent.Radius),
- KeyFrames =
- {
- new AnimationTrackProperty.KeyFrame(3.0f, 0),
- new AnimationTrackProperty.KeyFrame(2.0f, 1.5f),
- new AnimationTrackProperty.KeyFrame(3.0f, 3f)
- }
- }
- }
- };
-
- [Obsolete("Subscribe to AppearanceChangeEvent instead.")]
- public override void OnChangeData(AppearanceComponent component)
- {
- base.OnChangeData(component);
-
- if (!component.Initialized)
- return;
-
- PlayAnimation(component);
- }
-
- private void PlayAnimation(AppearanceComponent component)
- {
- component.Owner.EnsureComponent(out AnimationPlayerComponent animationPlayer);
- if (animationPlayer.HasRunningAnimation("radiatingLight")) return;
- animationPlayer.Play(_radiatingLightAnimation, "radiatingLight");
- animationPlayer.AnimationCompleted += s => animationPlayer.Play(_radiatingLightAnimation, s);
- }
- }
-}
diff --git a/Content.Client/Light/Visualizers/FlashLightVisualizer.cs b/Content.Client/Light/Visualizers/FlashLightVisualizer.cs
deleted file mode 100644
index 1fdacef51d..0000000000
--- a/Content.Client/Light/Visualizers/FlashLightVisualizer.cs
+++ /dev/null
@@ -1,115 +0,0 @@
-using System;
-using Content.Shared.Light;
-using Content.Shared.Light.Component;
-using JetBrains.Annotations;
-using Robust.Client.Animations;
-using Robust.Client.GameObjects;
-using Robust.Shared.Animations;
-using Robust.Shared.GameObjects;
-
-namespace Content.Client.Light.Visualizers
-{
- [UsedImplicitly]
- public sealed class FlashLightVisualizer : AppearanceVisualizer
- {
- private readonly Animation _radiatingLightAnimation = new()
- {
- Length = TimeSpan.FromSeconds(1),
- AnimationTracks =
- {
- new AnimationTrackComponentProperty
- {
- ComponentType = typeof(PointLightComponent),
- InterpolationMode = AnimationInterpolationMode.Linear,
- Property = nameof(PointLightComponent.Radius),
- KeyFrames =
- {
- new AnimationTrackProperty.KeyFrame(3.0f, 0),
- new AnimationTrackProperty.KeyFrame(2.0f, 0.5f),
- new AnimationTrackProperty.KeyFrame(3.0f, 1)
- }
- }
- }
- };
-
- private readonly Animation _blinkingLightAnimation = new()
- {
- Length = TimeSpan.FromSeconds(1),
- AnimationTracks =
- {
- new AnimationTrackComponentProperty()
- {
- ComponentType = typeof(PointLightComponent),
- //To create the blinking effect we go from nearly zero radius, to the light radius, and back
- //We do this instead of messing with the `PointLightComponent.enabled` because we don't want the animation to affect component behavior
- InterpolationMode = AnimationInterpolationMode.Nearest,
- Property = nameof(PointLightComponent.Radius),
- KeyFrames =
- {
- new AnimationTrackProperty.KeyFrame(0.1f, 0),
- new AnimationTrackProperty.KeyFrame(2f, 0.5f),
- new AnimationTrackProperty.KeyFrame(0.1f, 1)
- }
- }
- }
- };
-
- private Action? _radiatingCallback;
- private Action? _blinkingCallback;
-
- [Obsolete("Subscribe to AppearanceChangeEvent instead.")]
- public override void OnChangeData(AppearanceComponent component)
- {
- base.OnChangeData(component);
-
- if (component.TryGetData(HandheldLightVisuals.Power,
- out HandheldLightPowerStates state))
- {
- PlayAnimation(component, state);
- }
- }
-
- private void PlayAnimation(AppearanceComponent component, HandheldLightPowerStates state)
- {
- component.Owner.EnsureComponent(out AnimationPlayerComponent animationPlayer);
-
- switch (state)
- {
- case HandheldLightPowerStates.LowPower:
- if (!animationPlayer.HasRunningAnimation("radiatingLight"))
- {
- animationPlayer.Play(_radiatingLightAnimation, "radiatingLight");
- _radiatingCallback = (s) => animationPlayer.Play(_radiatingLightAnimation, s);
- animationPlayer.AnimationCompleted += _radiatingCallback;
- }
-
- break;
- case HandheldLightPowerStates.Dying:
- animationPlayer.Stop("radiatingLight");
- animationPlayer.AnimationCompleted -= _radiatingCallback;
- if (!animationPlayer.HasRunningAnimation("blinkingLight"))
- {
- animationPlayer.Play(_blinkingLightAnimation, "blinkingLight");
- _blinkingCallback = (s) => animationPlayer.Play(_blinkingLightAnimation, s);
- animationPlayer.AnimationCompleted += _blinkingCallback;
- }
-
- break;
- case HandheldLightPowerStates.FullPower:
- if (animationPlayer.HasRunningAnimation("blinkingLight"))
- {
- animationPlayer.Stop("blinkingLight");
- animationPlayer.AnimationCompleted -= _blinkingCallback;
- }
-
- if (animationPlayer.HasRunningAnimation("radiatingLight"))
- {
- animationPlayer.Stop("radiatingLight");
- animationPlayer.AnimationCompleted -= _radiatingCallback;
- }
-
- break;
- }
- }
- }
-}
diff --git a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs
index 78473ee022..5227ff6e0e 100644
--- a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs
+++ b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs
@@ -187,8 +187,12 @@ namespace Content.Server.Light.EntitySystems
public bool TurnOff(HandheldLightComponent component, bool makeNoise = true)
{
- if (!component.Activated) return false;
+ if (!component.Activated || !TryComp(component.Owner, out var pointLightComponent))
+ {
+ return false;
+ }
+ pointLightComponent.Enabled = false;
SetActivated(component.Owner, false, component, makeNoise);
component.Level = null;
_activeLights.Remove(component);
@@ -197,7 +201,10 @@ namespace Content.Server.Light.EntitySystems
public bool TurnOn(EntityUid user, HandheldLightComponent component)
{
- if (component.Activated) return false;
+ if (component.Activated || !TryComp(component.Owner, out var pointLightComponent))
+ {
+ return false;
+ }
if (!_powerCell.TryGetBatteryFromSlot(component.Owner, out var battery) &&
!TryComp(component.Owner, out battery))
@@ -217,6 +224,7 @@ namespace Content.Server.Light.EntitySystems
return false;
}
+ pointLightComponent.Enabled = true;
SetActivated(component.Owner, true, component, true);
_activeLights.Add(component);
diff --git a/Content.Shared/Light/Component/HandheldLightComponent.cs b/Content.Shared/Light/Component/HandheldLightComponent.cs
index 1db963ac56..2d795d0041 100644
--- a/Content.Shared/Light/Component/HandheldLightComponent.cs
+++ b/Content.Shared/Light/Component/HandheldLightComponent.cs
@@ -45,6 +45,20 @@ namespace Content.Shared.Light
public const int StatusLevels = 6;
+ ///
+ /// Specify the ID of the light behaviour to use when the state of the light is Dying
+ ///
+ [ViewVariables]
+ [DataField("blinkingBehaviourId")]
+ public string BlinkingBehaviourId { get; set; } = string.Empty;
+
+ ///
+ /// Specify the ID of the light behaviour to use when the state of the light is LowPower
+ ///
+ [ViewVariables]
+ [DataField("radiatingBehaviourId")]
+ public string RadiatingBehaviourId { get; set; } = string.Empty;
+
[Serializable, NetSerializable]
public sealed class HandheldLightComponentState : ComponentState
{
diff --git a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
index 92dbebdb95..74d56b482c 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
@@ -54,10 +54,31 @@
mask: /Textures/Effects/LightMasks/cone.png
autoRot: true
- type: Appearance
- visuals:
- - type: FlashLightVisualizer
- type: HandheldLight
addPrefix: true
+ blinkingBehaviourId: blinking
+ radiatingBehaviourId: radiating
+ - type: LightBehaviour
+ behaviours:
+ - !type:FadeBehaviour
+ id: radiating
+ interpolate: Linear
+ maxDuration: 2.0
+ startValue: 3.0
+ endValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
+ reverseWhenFinished: true
+ - !type:PulseBehaviour
+ id: blinking
+ interpolate: Nearest
+ maxDuration: 1.0
+ minValue: 0.1
+ maxValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
- type: PowerCellSlot
cellSlotId: cell_slot
- type: ItemSlots
@@ -155,10 +176,31 @@
mask: /Textures/Effects/LightMasks/cone.png
autoRot: true
- type: Appearance
- visuals:
- - type: FlashLightVisualizer
- type: HandheldLight
addPrefix: true
+ blinkingBehaviourId: blinking
+ radiatingBehaviourId: radiating
+ - type: LightBehaviour
+ behaviours:
+ - !type:FadeBehaviour
+ id: radiating
+ interpolate: Linear
+ maxDuration: 2.0
+ startValue: 3.0
+ endValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
+ reverseWhenFinished: true
+ - !type:PulseBehaviour
+ id: blinking
+ interpolate: Nearest
+ maxDuration: 1.0
+ minValue: 0.1
+ maxValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
- type: Battery
maxCharge: 600 #lights drain 3/s but recharge of 2 makes this 1/s. Therefore 600 is 10 minutes of light.
startingCharge: 600
diff --git a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml
index 83774cc8f1..43c2f48a18 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml
@@ -20,10 +20,31 @@
autoRot: true
radius: 3
- type: Appearance
- visuals:
- - type: FlashLightVisualizer
- type: HandheldLight
addPrefix: false
+ blinkingBehaviourId: blinking
+ radiatingBehaviourId: radiating
+ - type: LightBehaviour
+ behaviours:
+ - !type:FadeBehaviour
+ id: radiating
+ interpolate: Linear
+ maxDuration: 2.0
+ startValue: 3.0
+ endValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
+ reverseWhenFinished: true
+ - !type:PulseBehaviour
+ id: blinking
+ interpolate: Nearest
+ maxDuration: 1.0
+ minValue: 0.1
+ maxValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
- type: ToggleableLightVisuals
spriteLayer: light
inhandVisuals:
diff --git a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml
index 7f44193281..a261d9f3d1 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml
@@ -6,6 +6,29 @@
components:
- type: HandheldLight
addPrefix: true
+ blinkingBehaviourId: blinking
+ radiatingBehaviourId: radiating
+ - type: LightBehaviour
+ behaviours:
+ - !type:FadeBehaviour
+ id: radiating
+ interpolate: Linear
+ maxDuration: 2.0
+ startValue: 3.0
+ endValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
+ reverseWhenFinished: true
+ - !type:PulseBehaviour
+ id: blinking
+ interpolate: Nearest
+ maxDuration: 1.0
+ minValue: 0.1
+ maxValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
- type: PowerCellSlot
cellSlotId: cell_slot
- type: ItemSlots
@@ -29,8 +52,6 @@
energy: 2
- type: ToggleableLightVisuals
- type: Appearance
- visuals:
- - type: FlashLightVisualizer # todo move to light visuals component.
- type: entity
name: lamp
diff --git a/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml b/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml
index 0f2fa8443c..9974ef2eef 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml
@@ -10,6 +10,29 @@
- DroneUsable
- type: HandheldLight
addPrefix: false
+ blinkingBehaviourId: blinking
+ radiatingBehaviourId: radiating
+ - type: LightBehaviour
+ behaviours:
+ - !type:FadeBehaviour
+ id: radiating
+ interpolate: Linear
+ maxDuration: 2.0
+ startValue: 3.0
+ endValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
+ reverseWhenFinished: true
+ - !type:PulseBehaviour
+ id: blinking
+ interpolate: Nearest
+ maxDuration: 1.0
+ minValue: 0.1
+ maxValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
toggleAction:
name: action-name-toggle-light
description: action-description-toggle-light
@@ -53,8 +76,6 @@
autoRot: true
radius: 4.5
- type: Appearance
- visuals:
- - type: FlashLightVisualizer
- type: StaticPrice
price: 40
diff --git a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml
index 15abd2d80c..b3c32d64d8 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml
@@ -6,6 +6,29 @@
components:
- type: HandheldLight
addPrefix: true
+ blinkingBehaviourId: blinking
+ radiatingBehaviourId: radiating
+ - type: LightBehaviour
+ behaviours:
+ - !type:FadeBehaviour
+ id: radiating
+ interpolate: Linear
+ maxDuration: 2.0
+ startValue: 3.0
+ endValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
+ reverseWhenFinished: true
+ - !type:PulseBehaviour
+ id: blinking
+ interpolate: Nearest
+ maxDuration: 1.0
+ minValue: 0.1
+ maxValue: 2.0
+ isLooped: true
+ property: Radius
+ enabled: false
- type: Sprite
sprite: Objects/Tools/lantern.rsi
layers:
@@ -24,8 +47,6 @@
energy: 2.5
color: "#FFC458"
- type: Appearance
- visuals:
- - type: LanternVisualizer
- type: ToggleableLightVisuals
- type: PowerCellSlot
cellSlotId: cell_slot