ECS handheld lights (#5864)

This commit is contained in:
metalgearsloth
2021-12-27 18:15:16 +11:00
committed by GitHub
parent b3b171da7f
commit 0705f16898
6 changed files with 325 additions and 321 deletions

View File

@@ -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++)
{ {

View 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;
}
}

View File

@@ -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;
} }
} }
} }

View File

@@ -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);
}
}
}

View 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);
}
}
}
}

View File

@@ -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; }