ECS handheld lights (#5864)
This commit is contained in:
@@ -3,6 +3,7 @@ using Content.Shared.Light.Component;
|
|||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Shared.Analyzers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
@@ -12,27 +13,18 @@ using static Robust.Client.UserInterface.Controls.BoxContainer;
|
|||||||
namespace Content.Client.Light.Components
|
namespace Content.Client.Light.Components
|
||||||
{
|
{
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
|
[Friend(typeof(HandheldLightSystem))]
|
||||||
public sealed class HandheldLightComponent : SharedHandheldLightComponent, IItemStatus
|
public sealed class HandheldLightComponent : SharedHandheldLightComponent, IItemStatus
|
||||||
{
|
{
|
||||||
[ViewVariables] protected override bool HasCell => _level != null;
|
[ViewVariables] protected override bool HasCell => Level != null;
|
||||||
|
|
||||||
private byte? _level;
|
public byte? Level;
|
||||||
|
|
||||||
public Control MakeControl()
|
public Control MakeControl()
|
||||||
{
|
{
|
||||||
return new StatusControl(this);
|
return new StatusControl(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
|
||||||
{
|
|
||||||
base.HandleComponentState(curState, nextState);
|
|
||||||
|
|
||||||
if (curState is not HandheldLightComponentState cast)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_level = cast.Charge;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class StatusControl : Control
|
private sealed class StatusControl : Control
|
||||||
{
|
{
|
||||||
private const float TimerCycle = 1;
|
private const float TimerCycle = 1;
|
||||||
@@ -83,7 +75,7 @@ namespace Content.Client.Light.Components
|
|||||||
_timer += args.DeltaSeconds;
|
_timer += args.DeltaSeconds;
|
||||||
_timer %= TimerCycle;
|
_timer %= TimerCycle;
|
||||||
|
|
||||||
var level = _parent._level;
|
var level = _parent.Level;
|
||||||
|
|
||||||
for (var i = 0; i < _sections.Length; i++)
|
for (var i = 0; i < _sections.Length; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
23
Content.Client/Light/HandheldLightSystem.cs
Normal file
23
Content.Client/Light/HandheldLightSystem.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Content.Client.Light.Components;
|
||||||
|
using Content.Shared.Light.Component;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
|
namespace Content.Client.Light;
|
||||||
|
|
||||||
|
public sealed class HandheldLightSystem : EntitySystem
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
SubscribeLocalEvent<HandheldLightComponent, ComponentHandleState>(OnHandleState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHandleState(EntityUid uid, HandheldLightComponent component, ref ComponentHandleState args)
|
||||||
|
{
|
||||||
|
if (args.Current is not SharedHandheldLightComponent.HandheldLightComponentState state)
|
||||||
|
return;
|
||||||
|
|
||||||
|
component.Level = state.Charge;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,13 @@
|
|||||||
using System.Threading.Tasks;
|
using Content.Server.Light.EntitySystems;
|
||||||
using Content.Server.Clothing.Components;
|
|
||||||
using Content.Server.Items;
|
|
||||||
using Content.Server.PowerCell.Components;
|
using Content.Server.PowerCell.Components;
|
||||||
using Content.Shared.ActionBlocker;
|
|
||||||
using Content.Shared.Actions;
|
|
||||||
using Content.Shared.Actions.Behaviors.Item;
|
using Content.Shared.Actions.Behaviors.Item;
|
||||||
using Content.Shared.Actions.Components;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Light.Component;
|
using Content.Shared.Light.Component;
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Rounding;
|
|
||||||
using Content.Shared.Sound;
|
using Content.Shared.Sound;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Shared.Analyzers;
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
using Robust.Shared.Localization;
|
|
||||||
using Robust.Shared.Maths;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Utility;
|
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
namespace Content.Server.Light.Components
|
namespace Content.Server.Light.Components
|
||||||
@@ -30,222 +16,29 @@ namespace Content.Server.Light.Components
|
|||||||
/// Component that represents a powered handheld light source which can be toggled on and off.
|
/// Component that represents a powered handheld light source which can be toggled on and off.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
#pragma warning disable 618
|
[Friend(typeof(HandheldLightSystem))]
|
||||||
internal sealed class HandheldLightComponent : SharedHandheldLightComponent, IUse, IExamine, IInteractUsing
|
public sealed class HandheldLightComponent : SharedHandheldLightComponent
|
||||||
#pragma warning restore 618
|
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("wattage")] public float Wattage { get; set; } = 3f;
|
[ViewVariables(VVAccess.ReadWrite)] [DataField("wattage")] public float Wattage { get; set; } = 3f;
|
||||||
[ViewVariables] private PowerCellSlotComponent _cellSlot = default!;
|
[ViewVariables] public PowerCellSlotComponent CellSlot = default!;
|
||||||
private PowerCellComponent? Cell => _cellSlot.Cell;
|
public PowerCellComponent? Cell => CellSlot.Cell;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Status of light, whether or not it is emitting light.
|
/// Status of light, whether or not it is emitting light.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
public bool Activated { get; private set; }
|
public bool Activated { get; set; }
|
||||||
|
|
||||||
[ViewVariables] protected override bool HasCell => _cellSlot.HasCell;
|
[ViewVariables] protected override bool HasCell => CellSlot.HasCell;
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("turnOnSound")] public SoundSpecifier TurnOnSound = new SoundPathSpecifier("/Audio/Items/flashlight_on.ogg");
|
[ViewVariables(VVAccess.ReadWrite)] [DataField("turnOnSound")] public SoundSpecifier TurnOnSound = new SoundPathSpecifier("/Audio/Items/flashlight_on.ogg");
|
||||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("turnOnFailSound")] public SoundSpecifier TurnOnFailSound = new SoundPathSpecifier("/Audio/Machines/button.ogg");
|
[ViewVariables(VVAccess.ReadWrite)] [DataField("turnOnFailSound")] public SoundSpecifier TurnOnFailSound = new SoundPathSpecifier("/Audio/Machines/button.ogg");
|
||||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("turnOffSound")] public SoundSpecifier TurnOffSound = new SoundPathSpecifier("/Audio/Items/flashlight_off.ogg");
|
[ViewVariables(VVAccess.ReadWrite)] [DataField("turnOffSound")] public SoundSpecifier TurnOffSound = new SoundPathSpecifier("/Audio/Items/flashlight_off.ogg");
|
||||||
|
|
||||||
[ComponentDependency] private readonly ItemActionsComponent? _itemActions = null;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client-side ItemStatus level
|
/// Client-side ItemStatus level
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private byte? _lastLevel;
|
public byte? LastLevel;
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
Owner.EnsureComponent<PointLightComponent>();
|
|
||||||
_cellSlot = Owner.EnsureComponent<PowerCellSlotComponent>();
|
|
||||||
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnRemove()
|
|
||||||
{
|
|
||||||
base.OnRemove();
|
|
||||||
_entMan.EventBus.QueueEvent(EventSource.Local, new DeactivateHandheldLightMessage(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(eventArgs.User)) return false;
|
|
||||||
if (!_cellSlot.InsertCell(eventArgs.Using)) return false;
|
|
||||||
Dirty();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
|
|
||||||
{
|
|
||||||
if (Activated)
|
|
||||||
{
|
|
||||||
message.AddMarkup(Loc.GetString("handheld-light-component-on-examine-is-on-message"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
message.AddMarkup(Loc.GetString("handheld-light-component-on-examine-is-off-message"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
return ToggleStatus(eventArgs.User);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Illuminates the light if it is not active, extinguishes it if it is active.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True if the light's status was toggled, false otherwise.</returns>
|
|
||||||
public bool ToggleStatus(EntityUid user)
|
|
||||||
{
|
|
||||||
if (!EntitySystem.Get<ActionBlockerSystem>().CanUse(user)) return false;
|
|
||||||
return Activated ? TurnOff() : TurnOn(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TurnOff(bool makeNoise = true)
|
|
||||||
{
|
|
||||||
if (!Activated)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetState(false);
|
|
||||||
Activated = false;
|
|
||||||
UpdateLightAction();
|
|
||||||
_entMan.EventBus.QueueEvent(EventSource.Local, new DeactivateHandheldLightMessage(this));
|
|
||||||
|
|
||||||
if (makeNoise)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), TurnOffSound.GetSound(), Owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TurnOn(EntityUid user)
|
|
||||||
{
|
|
||||||
if (Activated)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Cell == null)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), TurnOnFailSound.GetSound(), Owner);
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("handheld-light-component-cell-missing-message"));
|
|
||||||
UpdateLightAction();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// To prevent having to worry about frame time in here.
|
|
||||||
// Let's just say you need a whole second of charge before you can turn it on.
|
|
||||||
// Simple enough.
|
|
||||||
if (Wattage > Cell.CurrentCharge)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), TurnOnFailSound.GetSound(), Owner);
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("handheld-light-component-cell-dead-message"));
|
|
||||||
UpdateLightAction();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Activated = true;
|
|
||||||
UpdateLightAction();
|
|
||||||
SetState(true);
|
|
||||||
_entMan.EventBus.QueueEvent(EventSource.Local, new ActivateHandheldLightMessage(this));
|
|
||||||
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), TurnOnSound.GetSound(), Owner);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetState(bool on)
|
|
||||||
{
|
|
||||||
if (_entMan.TryGetComponent(Owner, out SpriteComponent? sprite))
|
|
||||||
{
|
|
||||||
sprite.LayerSetVisible(1, on);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_entMan.TryGetComponent(Owner, out PointLightComponent? light))
|
|
||||||
{
|
|
||||||
light.Enabled = on;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_entMan.TryGetComponent(Owner, out ClothingComponent? clothing))
|
|
||||||
{
|
|
||||||
clothing.ClothingEquippedPrefix = Loc.GetString(on ? "on" : "off");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_entMan.TryGetComponent(Owner, out ItemComponent? item))
|
|
||||||
{
|
|
||||||
item.EquippedPrefix = Loc.GetString(on ? "on" : "off");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateLightAction()
|
|
||||||
{
|
|
||||||
_itemActions?.Toggle(ItemActionType.ToggleLight, Activated);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnUpdate(float frameTime)
|
|
||||||
{
|
|
||||||
if (Cell == null)
|
|
||||||
{
|
|
||||||
TurnOff(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var appearanceComponent = _entMan.GetComponent<AppearanceComponent>(Owner);
|
|
||||||
|
|
||||||
if (Cell.MaxCharge - Cell.CurrentCharge < Cell.MaxCharge * 0.70)
|
|
||||||
{
|
|
||||||
appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower);
|
|
||||||
}
|
|
||||||
else if (Cell.MaxCharge - Cell.CurrentCharge < Cell.MaxCharge * 0.90)
|
|
||||||
{
|
|
||||||
appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.LowPower);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.Dying);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Activated && !Cell.TryUseCharge(Wattage * frameTime)) TurnOff(false);
|
|
||||||
|
|
||||||
var level = GetLevel();
|
|
||||||
|
|
||||||
if (level != _lastLevel)
|
|
||||||
{
|
|
||||||
_lastLevel = level;
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Curently every single flashlight has the same number of levels for status and that's all it uses the charge for
|
|
||||||
// Thus we'll just check if the level changes.
|
|
||||||
private byte? GetLevel()
|
|
||||||
{
|
|
||||||
if (Cell == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var currentCharge = Cell.CurrentCharge;
|
|
||||||
|
|
||||||
if (MathHelper.CloseToPercent(currentCharge, 0) || Wattage > currentCharge)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return (byte?) ContentHelpers.RoundToNearestLevels(currentCharge / Cell.MaxCharge * 255, 255, StatusLevels);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ComponentState GetComponentState()
|
|
||||||
{
|
|
||||||
return new HandheldLightComponentState(GetLevel());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
@@ -256,27 +49,7 @@ namespace Content.Server.Light.Components
|
|||||||
{
|
{
|
||||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<HandheldLightComponent?>(args.Item, out var lightComponent)) return false;
|
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<HandheldLightComponent?>(args.Item, out var lightComponent)) return false;
|
||||||
if (lightComponent.Activated == args.ToggledOn) return false;
|
if (lightComponent.Activated == args.ToggledOn) return false;
|
||||||
return lightComponent.ToggleStatus(args.Performer);
|
return EntitySystem.Get<HandheldLightSystem>().ToggleStatus(args.Performer, lightComponent);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ActivateHandheldLightMessage : EntityEventArgs
|
|
||||||
{
|
|
||||||
public HandheldLightComponent Component { get; }
|
|
||||||
|
|
||||||
public ActivateHandheldLightMessage(HandheldLightComponent component)
|
|
||||||
{
|
|
||||||
Component = component;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class DeactivateHandheldLightMessage : EntityEventArgs
|
|
||||||
{
|
|
||||||
public HandheldLightComponent Component { get; }
|
|
||||||
|
|
||||||
public DeactivateHandheldLightMessage(HandheldLightComponent component)
|
|
||||||
{
|
|
||||||
Component = component;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Content.Server.Light.Components;
|
|
||||||
using Content.Shared.Verbs;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.Localization;
|
|
||||||
|
|
||||||
namespace Content.Server.Light.EntitySystems
|
|
||||||
{
|
|
||||||
[UsedImplicitly]
|
|
||||||
internal sealed class HandHeldLightSystem : EntitySystem
|
|
||||||
{
|
|
||||||
// TODO: Ideally you'd be able to subscribe to power stuff to get events at certain percentages.. or something?
|
|
||||||
// But for now this will be better anyway.
|
|
||||||
private HashSet<HandheldLightComponent> _activeLights = new();
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
SubscribeLocalEvent<ActivateHandheldLightMessage>(HandleActivate);
|
|
||||||
SubscribeLocalEvent<DeactivateHandheldLightMessage>(HandleDeactivate);
|
|
||||||
SubscribeLocalEvent<HandheldLightComponent, GetActivationVerbsEvent>(AddToggleLightVerb);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Shutdown()
|
|
||||||
{
|
|
||||||
base.Shutdown();
|
|
||||||
_activeLights.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleActivate(ActivateHandheldLightMessage message)
|
|
||||||
{
|
|
||||||
_activeLights.Add(message.Component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleDeactivate(DeactivateHandheldLightMessage message)
|
|
||||||
{
|
|
||||||
_activeLights.Remove(message.Component);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
|
||||||
{
|
|
||||||
foreach (var handheld in _activeLights.ToArray())
|
|
||||||
{
|
|
||||||
if (handheld.Deleted || handheld.Paused) continue;
|
|
||||||
handheld.OnUpdate(frameTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddToggleLightVerb(EntityUid uid, HandheldLightComponent component, GetActivationVerbsEvent args)
|
|
||||||
{
|
|
||||||
if (!args.CanAccess || !args.CanInteract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Verb verb = new();
|
|
||||||
verb.Text = Loc.GetString("verb-common-toggle-light");
|
|
||||||
verb.IconTexture = "/Textures/Interface/VerbIcons/light.svg.192dpi.png";
|
|
||||||
verb.Act = component.Activated
|
|
||||||
? () => component.TurnOff()
|
|
||||||
: () => component.TurnOn(args.User);
|
|
||||||
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
283
Content.Server/Light/EntitySystems/HandheldLightSystem.cs
Normal file
283
Content.Server/Light/EntitySystems/HandheldLightSystem.cs
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Server.Clothing.Components;
|
||||||
|
using Content.Server.Items;
|
||||||
|
using Content.Server.Light.Components;
|
||||||
|
using Content.Server.Popups;
|
||||||
|
using Content.Server.PowerCell.Components;
|
||||||
|
using Content.Shared.ActionBlocker;
|
||||||
|
using Content.Shared.Actions;
|
||||||
|
using Content.Shared.Actions.Components;
|
||||||
|
using Content.Shared.Examine;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Light.Component;
|
||||||
|
using Content.Shared.Rounding;
|
||||||
|
using Content.Shared.Verbs;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Light.EntitySystems
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class HandheldLightSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||||
|
[Dependency] private readonly PopupSystem _popup = default!;
|
||||||
|
|
||||||
|
// TODO: Ideally you'd be able to subscribe to power stuff to get events at certain percentages.. or something?
|
||||||
|
// But for now this will be better anyway.
|
||||||
|
private readonly HashSet<HandheldLightComponent> _activeLights = new();
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<HandheldLightComponent, ComponentInit>(OnInit);
|
||||||
|
SubscribeLocalEvent<HandheldLightComponent, ComponentRemove>(OnRemove);
|
||||||
|
SubscribeLocalEvent<HandheldLightComponent, ComponentGetState>(OnGetState);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<HandheldLightComponent, ExaminedEvent>(OnExamine);
|
||||||
|
SubscribeLocalEvent<HandheldLightComponent, GetActivationVerbsEvent>(AddToggleLightVerb);
|
||||||
|
SubscribeLocalEvent<HandheldLightComponent, InteractUsingEvent>(OnInteractUsing);
|
||||||
|
SubscribeLocalEvent<HandheldLightComponent, UseInHandEvent>(OnUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGetState(EntityUid uid, HandheldLightComponent component, ref ComponentGetState args)
|
||||||
|
{
|
||||||
|
args.State = new SharedHandheldLightComponent.HandheldLightComponentState(GetLevel(component));
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte? GetLevel(HandheldLightComponent component)
|
||||||
|
{
|
||||||
|
// Curently every single flashlight has the same number of levels for status and that's all it uses the charge for
|
||||||
|
// Thus we'll just check if the level changes.
|
||||||
|
if (component.Cell == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var currentCharge = component.Cell.CurrentCharge;
|
||||||
|
|
||||||
|
if (MathHelper.CloseToPercent(currentCharge, 0) || component.Wattage > currentCharge)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return (byte?) ContentHelpers.RoundToNearestLevels(currentCharge / component.Cell.MaxCharge * 255, 255, SharedHandheldLightComponent.StatusLevels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInit(EntityUid uid, HandheldLightComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
EntityManager.EnsureComponent<PointLightComponent>(uid);
|
||||||
|
component.CellSlot = EntityManager.EnsureComponent<PowerCellSlotComponent>(uid);
|
||||||
|
|
||||||
|
// Want to make sure client has latest data on level so battery displays properly.
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRemove(EntityUid uid, HandheldLightComponent component, ComponentRemove args)
|
||||||
|
{
|
||||||
|
_activeLights.Remove(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInteractUsing(EntityUid uid, HandheldLightComponent component, InteractUsingEvent args)
|
||||||
|
{
|
||||||
|
// TODO: https://github.com/space-wizards/space-station-14/pull/5864#discussion_r775191916
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (!_blocker.CanInteract(args.User)) return;
|
||||||
|
if (!component.CellSlot.InsertCell(args.Used)) return;
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUse(EntityUid uid, HandheldLightComponent component, UseInHandEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (ToggleStatus(args.User, component))
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Illuminates the light if it is not active, extinguishes it if it is active.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if the light's status was toggled, false otherwise.</returns>
|
||||||
|
public bool ToggleStatus(EntityUid user, HandheldLightComponent component)
|
||||||
|
{
|
||||||
|
if (!_blocker.CanUse(user)) return false;
|
||||||
|
return component.Activated ? TurnOff(component) : TurnOn(user, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExamine(EntityUid uid, HandheldLightComponent component, ExaminedEvent args)
|
||||||
|
{
|
||||||
|
args.PushMarkup(component.Activated
|
||||||
|
? Loc.GetString("handheld-light-component-on-examine-is-on-message")
|
||||||
|
: Loc.GetString("handheld-light-component-on-examine-is-off-message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
base.Shutdown();
|
||||||
|
_activeLights.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
var toRemove = new RemQueue<HandheldLightComponent>();
|
||||||
|
|
||||||
|
foreach (var handheld in _activeLights)
|
||||||
|
{
|
||||||
|
if (handheld.Deleted)
|
||||||
|
{
|
||||||
|
toRemove.Add(handheld);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handheld.Paused) continue;
|
||||||
|
TryUpdate(handheld, frameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var light in toRemove)
|
||||||
|
{
|
||||||
|
_activeLights.Remove(light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToggleLightVerb(EntityUid uid, HandheldLightComponent component, GetActivationVerbsEvent args)
|
||||||
|
{
|
||||||
|
if (!args.CanAccess || !args.CanInteract) return;
|
||||||
|
|
||||||
|
Verb verb = new()
|
||||||
|
{
|
||||||
|
Text = Loc.GetString("verb-common-toggle-light"),
|
||||||
|
IconTexture = "/Textures/Interface/VerbIcons/light.svg.192dpi.png",
|
||||||
|
Act = component.Activated
|
||||||
|
? () => TurnOff(component)
|
||||||
|
: () => TurnOn(args.User, component)
|
||||||
|
};
|
||||||
|
|
||||||
|
args.Verbs.Add(verb);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TurnOff(HandheldLightComponent component, bool makeNoise = true)
|
||||||
|
{
|
||||||
|
if (!component.Activated) return false;
|
||||||
|
|
||||||
|
SetState(component, false);
|
||||||
|
component.Activated = false;
|
||||||
|
UpdateLightAction(component);
|
||||||
|
_activeLights.Remove(component);
|
||||||
|
component.LastLevel = null;
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
|
||||||
|
if (makeNoise)
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOffSound.GetSound(), component.Owner);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TurnOn(EntityUid user, HandheldLightComponent component)
|
||||||
|
{
|
||||||
|
if (component.Activated) return false;
|
||||||
|
|
||||||
|
if (component.Cell == null)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOnFailSound.GetSound(), component.Owner);
|
||||||
|
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), component.Owner, Filter.Entities(user));
|
||||||
|
UpdateLightAction(component);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// To prevent having to worry about frame time in here.
|
||||||
|
// Let's just say you need a whole second of charge before you can turn it on.
|
||||||
|
// Simple enough.
|
||||||
|
if (component.Wattage > component.Cell.CurrentCharge)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOnFailSound.GetSound(), component.Owner);
|
||||||
|
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), component.Owner, Filter.Entities(user));
|
||||||
|
UpdateLightAction(component);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Activated = true;
|
||||||
|
UpdateLightAction(component);
|
||||||
|
SetState(component, true);
|
||||||
|
_activeLights.Add(component);
|
||||||
|
component.LastLevel = GetLevel(component);
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOnSound.GetSound(), component.Owner);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetState(HandheldLightComponent component, bool on)
|
||||||
|
{
|
||||||
|
// TODO: Oh dear
|
||||||
|
if (EntityManager.TryGetComponent(component.Owner, out SpriteComponent? sprite))
|
||||||
|
{
|
||||||
|
sprite.LayerSetVisible(1, on);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EntityManager.TryGetComponent(component.Owner, out PointLightComponent? light))
|
||||||
|
{
|
||||||
|
light.Enabled = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EntityManager.TryGetComponent(component.Owner, out ClothingComponent? clothing))
|
||||||
|
{
|
||||||
|
clothing.ClothingEquippedPrefix = Loc.GetString(on ? "on" : "off");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EntityManager.TryGetComponent(component.Owner, out ItemComponent? item))
|
||||||
|
{
|
||||||
|
item.EquippedPrefix = Loc.GetString(on ? "on" : "off");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateLightAction(HandheldLightComponent component)
|
||||||
|
{
|
||||||
|
if (!EntityManager.TryGetComponent(component.Owner, out ItemActionsComponent? actions)) return;
|
||||||
|
|
||||||
|
actions.Toggle(ItemActionType.ToggleLight, component.Activated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TryUpdate(HandheldLightComponent component, float frameTime)
|
||||||
|
{
|
||||||
|
if (component.Cell == null)
|
||||||
|
{
|
||||||
|
TurnOff(component, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var appearanceComponent = EntityManager.GetComponent<AppearanceComponent>(component.Owner);
|
||||||
|
|
||||||
|
if (component.Cell.MaxCharge - component.Cell.CurrentCharge < component.Cell.MaxCharge * 0.70)
|
||||||
|
{
|
||||||
|
appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower);
|
||||||
|
}
|
||||||
|
else if (component.Cell.MaxCharge - component.Cell.CurrentCharge < component.Cell.MaxCharge * 0.90)
|
||||||
|
{
|
||||||
|
appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.LowPower);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.Dying);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.Activated && !component.Cell.TryUseCharge(component.Wattage * frameTime)) TurnOff(component, false);
|
||||||
|
|
||||||
|
var level = GetLevel(component);
|
||||||
|
|
||||||
|
if (level != component.LastLevel)
|
||||||
|
{
|
||||||
|
component.LastLevel = level;
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,17 +5,16 @@ using Robust.Shared.Serialization;
|
|||||||
|
|
||||||
namespace Content.Shared.Light.Component
|
namespace Content.Shared.Light.Component
|
||||||
{
|
{
|
||||||
[NetworkedComponent()]
|
[NetworkedComponent]
|
||||||
|
[ComponentProtoName("HandheldLight")]
|
||||||
public abstract class SharedHandheldLightComponent : Robust.Shared.GameObjects.Component
|
public abstract class SharedHandheldLightComponent : Robust.Shared.GameObjects.Component
|
||||||
{
|
{
|
||||||
public sealed override string Name => "HandheldLight";
|
|
||||||
|
|
||||||
protected abstract bool HasCell { get; }
|
protected abstract bool HasCell { get; }
|
||||||
|
|
||||||
protected const int StatusLevels = 6;
|
public const int StatusLevels = 6;
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
protected sealed class HandheldLightComponentState : ComponentState
|
public sealed class HandheldLightComponentState : ComponentState
|
||||||
{
|
{
|
||||||
public byte? Charge { get; }
|
public byte? Charge { get; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user