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