diff --git a/Content.Client/ADT/NightVision/NightVisionOverlay.cs b/Content.Client/ADT/NightVision/NightVisionOverlay.cs new file mode 100644 index 00000000000..108a88c46a9 --- /dev/null +++ b/Content.Client/ADT/NightVision/NightVisionOverlay.cs @@ -0,0 +1,117 @@ +// taken and adapted from https://github.com/RMC-14/RMC-14?ysclid=lzx00zxd6e53093995 + +using System.Numerics; +using Content.Shared.ADT.NightVision; +// using Content.Shared._RMC14.Xenonids; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Map; + +namespace Content.Client.ADT.NightVision; + +public sealed class NightVisionOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _entity = default!; + [Dependency] private readonly IPlayerManager _players = default!; + + private readonly ContainerSystem _container; + private readonly TransformSystem _transform; + // private readonly EntityQuery _xenoQuery; + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + private readonly List _entries = new(); + + public NightVisionOverlay() + { + IoCManager.InjectDependencies(this); + + _container = _entity.System(); + _transform = _entity.System(); + // _xenoQuery = _entity.GetEntityQuery(); + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (!_entity.TryGetComponent(_players.LocalEntity, out NightVisionComponent? nightVision) || + nightVision.State == NightVisionState.Off) + { + return; + } + + var handle = args.WorldHandle; + var eye = args.Viewport.Eye; + var eyeRot = eye?.Rotation ?? default; + + _entries.Clear(); + var entities = _entity.EntityQueryEnumerator(); + while (entities.MoveNext(out var uid, out var visible, out var sprite, out var xform)) + { + _entries.Add(new NightVisionRenderEntry((uid, sprite, xform), + eye?.Position.MapId, + eyeRot, + nightVision.SeeThroughContainers, + visible.Priority, + visible.Transparency)); + } + + _entries.Sort(SortPriority); + + foreach (var entry in _entries) + { + Render(entry.Ent, + entry.Map, + handle, + entry.EyeRot, + entry.NightVisionSeeThroughContainers, + entry.Transparency); + } + + handle.SetTransform(Matrix3x2.Identity); + } + + private static int SortPriority(NightVisionRenderEntry x, NightVisionRenderEntry y) + { + return x.Priority.CompareTo(y.Priority); + } + + private void Render(Entity ent, + MapId? map, + DrawingHandleWorld handle, + Angle eyeRot, + bool seeThroughContainers, + float? transparency) + { + var (uid, sprite, xform) = ent; + if (xform.MapID != map) + return; + + var seeThrough = seeThroughContainers; // && !_xenoQuery.HasComp(uid); + if (!seeThrough && _container.IsEntityOrParentInContainer(uid, xform: xform)) + return; + + var (position, rotation) = _transform.GetWorldPositionRotation(xform); + + var colorCache = sprite.Color; + if (transparency != null) + { + var color = sprite.Color * Color.White.WithAlpha(transparency.Value); + sprite.Color = color; + } + sprite.Render(handle, eyeRot, rotation, position: position); + if (transparency != null) + { + sprite.Color = colorCache; + } + } +} + +public record struct NightVisionRenderEntry( + (EntityUid, SpriteComponent, TransformComponent) Ent, + MapId? Map, + Angle EyeRot, + bool NightVisionSeeThroughContainers, + int Priority, + float? Transparency); diff --git a/Content.Client/ADT/NightVision/NightVisionSystem.cs b/Content.Client/ADT/NightVision/NightVisionSystem.cs new file mode 100644 index 00000000000..d69b4338c9a --- /dev/null +++ b/Content.Client/ADT/NightVision/NightVisionSystem.cs @@ -0,0 +1,84 @@ +// taken and adapted from https://github.com/RMC-14/RMC-14?ysclid=lzx00zxd6e53093995 + +using Content.Shared.ADT.NightVision; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Player; + +namespace Content.Client.ADT.NightVision; + +public sealed class NightVisionSystem : SharedNightVisionSystem +{ + [Dependency] private readonly ILightManager _light = default!; + [Dependency] private readonly IOverlayManager _overlay = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnNightVisionAttached); + SubscribeLocalEvent(OnNightVisionDetached); + } + + private void OnNightVisionAttached(Entity ent, ref LocalPlayerAttachedEvent args) + { + NightVisionChanged(ent); + } + + private void OnNightVisionDetached(Entity ent, ref LocalPlayerDetachedEvent args) + { + Off(); + } + + protected override void NightVisionChanged(Entity ent) + { + if (ent != _player.LocalEntity) + return; + + switch (ent.Comp.State) + { + case NightVisionState.Off: + Off(); + break; + case NightVisionState.Half: + Half(ent); + break; + case NightVisionState.Full: + Full(ent); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + protected override void NightVisionRemoved(Entity ent) + { + if (ent != _player.LocalEntity) + return; + + Off(); + } + + private void Off() + { + _overlay.RemoveOverlay(new NightVisionOverlay()); + _light.DrawLighting = true; + } + + private void Half(Entity ent) + { + if (ent.Comp.Overlay) + _overlay.AddOverlay(new NightVisionOverlay()); + + _light.DrawLighting = true; + } + + private void Full(Entity ent) + { + if (ent.Comp.Overlay) + _overlay.AddOverlay(new NightVisionOverlay()); + + _light.DrawLighting = false; + } +} diff --git a/Content.Server/ADT/NightVision/NightVisionSystem.cs b/Content.Server/ADT/NightVision/NightVisionSystem.cs new file mode 100644 index 00000000000..5f6a3602a1c --- /dev/null +++ b/Content.Server/ADT/NightVision/NightVisionSystem.cs @@ -0,0 +1,7 @@ +// taken and adapted from https://github.com/RMC-14/RMC-14?ysclid=lzx00zxd6e53093995 + +using Content.Shared.ADT.NightVision; + +namespace Content.Server.ADT.NightVision; + +public sealed class NightVisionSystem : SharedNightVisionSystem; diff --git a/Content.Shared/ADT/NightVision/ADTNightVisionVisibleComponent.cs b/Content.Shared/ADT/NightVision/ADTNightVisionVisibleComponent.cs new file mode 100644 index 00000000000..6c5a997453e --- /dev/null +++ b/Content.Shared/ADT/NightVision/ADTNightVisionVisibleComponent.cs @@ -0,0 +1,25 @@ +// taken and adapted from https://github.com/RMC-14/RMC-14?ysclid=lzx00zxd6e53093995 + +using Robust.Shared.GameStates; + +namespace Content.Shared.ADT.NightVision; + +/// +/// For rendering sprites on top of FOV when the user has a . +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class NightVisionVisibleComponent : Component +{ + /// + /// Priority for rendering order. + /// Rendered from lowest to highest, which means higher numbers will be rendered above lower numbers. + /// + [DataField, AutoNetworkedField] + public int Priority = 0; + + /// + /// Transparency of the rendered sprite. + /// + [DataField, AutoNetworkedField] + public float? Transparency = null; +} diff --git a/Content.Shared/ADT/NightVision/NightVisionComponent.cs b/Content.Shared/ADT/NightVision/NightVisionComponent.cs new file mode 100644 index 00000000000..f51be334412 --- /dev/null +++ b/Content.Shared/ADT/NightVision/NightVisionComponent.cs @@ -0,0 +1,36 @@ +// taken and adapted from https://github.com/RMC-14/RMC-14?ysclid=lzx00zxd6e53093995 + +using Content.Shared.Alert; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.ADT.NightVision; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(SharedNightVisionSystem))] +public sealed partial class NightVisionComponent : Component +{ + [DataField] + public ProtoId? Alert; + + [DataField, AutoNetworkedField] + public NightVisionState State = NightVisionState.Full; + + [DataField, AutoNetworkedField] + public bool Overlay; + + [DataField, AutoNetworkedField] + public bool Innate; + + [DataField, AutoNetworkedField] + public bool SeeThroughContainers; +} + +[Serializable, NetSerializable] +public enum NightVisionState +{ + Off, + Half, + Full +} diff --git a/Content.Shared/ADT/NightVision/NightVisionItemComponent.cs b/Content.Shared/ADT/NightVision/NightVisionItemComponent.cs new file mode 100644 index 00000000000..887d94c0a5a --- /dev/null +++ b/Content.Shared/ADT/NightVision/NightVisionItemComponent.cs @@ -0,0 +1,28 @@ +// taken and adapted from https://github.com/RMC-14/RMC-14?ysclid=lzx00zxd6e53093995 + +using Content.Shared.Inventory; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.ADT.NightVision; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedNightVisionSystem))] +public sealed partial class NightVisionItemComponent : Component +{ + [DataField, AutoNetworkedField] + public EntProtoId ActionId = "ActionToggleNinjaNightVision"; + + [DataField, AutoNetworkedField] + public EntityUid? Action; + + [DataField, AutoNetworkedField] + public EntityUid? User; + + [DataField, AutoNetworkedField] + public bool Toggleable = true; + + // Only allows for a single slotflag right now because some code uses strings and some code uses enums to determine slots :( + [DataField, AutoNetworkedField] + public SlotFlags SlotFlags { get; set; } = SlotFlags.EYES; +} diff --git a/Content.Shared/ADT/NightVision/NightVisionItemVisuals.cs b/Content.Shared/ADT/NightVision/NightVisionItemVisuals.cs new file mode 100644 index 00000000000..a0602cd1910 --- /dev/null +++ b/Content.Shared/ADT/NightVision/NightVisionItemVisuals.cs @@ -0,0 +1,11 @@ +// taken and adapted from https://github.com/RMC-14/RMC-14?ysclid=lzx00zxd6e53093995 + +using Robust.Shared.Serialization; + +namespace Content.Shared.ADT.NightVision; + +[Serializable, NetSerializable] +public enum NightVisionItemVisuals +{ + Active, +} diff --git a/Content.Shared/ADT/NightVision/SharedNightVisionSystem.cs b/Content.Shared/ADT/NightVision/SharedNightVisionSystem.cs new file mode 100644 index 00000000000..85a7bff3663 --- /dev/null +++ b/Content.Shared/ADT/NightVision/SharedNightVisionSystem.cs @@ -0,0 +1,201 @@ +// taken and adapted from https://github.com/RMC-14/RMC-14?ysclid=lzx00zxd6e53093995 + +using Content.Shared.Actions; +using Content.Shared.Alert; +using Content.Shared.Inventory.Events; +using Content.Shared.Rounding; +using Content.Shared.Toggleable; +using Robust.Shared.Timing; + +namespace Content.Shared.ADT.NightVision; + +public abstract class SharedNightVisionSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnNightVisionStartup); + SubscribeLocalEvent(OnNightVisionMapInit); + SubscribeLocalEvent(OnNightVisionAfterHandle); + SubscribeLocalEvent(OnNightVisionRemove); + + SubscribeLocalEvent(OnNightVisionItemGetActions); + SubscribeLocalEvent(OnNightVisionItemToggle); + SubscribeLocalEvent(OnNightVisionItemGotEquipped); + SubscribeLocalEvent(OnNightVisionItemGotUnequipped); + SubscribeLocalEvent(OnNightVisionItemActionRemoved); + SubscribeLocalEvent(OnNightVisionItemRemove); + SubscribeLocalEvent(OnNightVisionItemTerminating); + } + + private void OnNightVisionStartup(Entity ent, ref ComponentStartup args) + { + NightVisionChanged(ent); + } + + private void OnNightVisionAfterHandle(Entity ent, ref AfterAutoHandleStateEvent args) + { + NightVisionChanged(ent); + } + + private void OnNightVisionMapInit(Entity ent, ref MapInitEvent args) + { + UpdateAlert(ent); + } + + private void OnNightVisionRemove(Entity ent, ref ComponentRemove args) + { + if (ent.Comp.Alert is { } alert) + _alerts.ClearAlert(ent, alert); + + NightVisionRemoved(ent); + } + + private void OnNightVisionItemGetActions(Entity ent, ref GetItemActionsEvent args) + { + if (args.InHands || !ent.Comp.Toggleable) + return; + + if (ent.Comp.SlotFlags != args.SlotFlags) + return; + + args.AddAction(ref ent.Comp.Action, ent.Comp.ActionId); + } + + private void OnNightVisionItemToggle(Entity ent, ref ToggleActionEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + ToggleNightVisionItem(ent, args.Performer); + } + + private void OnNightVisionItemGotEquipped(Entity ent, ref GotEquippedEvent args) + { + if (ent.Comp.SlotFlags != args.SlotFlags) + return; + + EnableNightVisionItem(ent, args.Equipee); + } + + private void OnNightVisionItemGotUnequipped(Entity ent, ref GotUnequippedEvent args) + { + if (ent.Comp.SlotFlags != args.SlotFlags) + return; + + DisableNightVisionItem(ent, args.Equipee); + } + + private void OnNightVisionItemActionRemoved(Entity ent, ref ActionRemovedEvent args) + { + DisableNightVisionItem(ent, ent.Comp.User); + } + + private void OnNightVisionItemRemove(Entity ent, ref ComponentRemove args) + { + DisableNightVisionItem(ent, ent.Comp.User); + } + + private void OnNightVisionItemTerminating(Entity ent, ref EntityTerminatingEvent args) + { + DisableNightVisionItem(ent, ent.Comp.User); + } + + public void Toggle(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + ent.Comp.State = ent.Comp.State switch + { + NightVisionState.Off => NightVisionState.Half, + NightVisionState.Half => NightVisionState.Full, + NightVisionState.Full => NightVisionState.Off, + _ => throw new ArgumentOutOfRangeException(), + }; + + Dirty(ent); + UpdateAlert((ent, ent.Comp)); + } + + private void UpdateAlert(Entity ent) + { + if (ent.Comp.Alert is { } alert) + { + var level = MathF.Max((int) NightVisionState.Off, (int) ent.Comp.State); + var max = _alerts.GetMaxSeverity(alert); + var severity = max - ContentHelpers.RoundToLevels(level, (int) NightVisionState.Full, max + 1); + _alerts.ShowAlert(ent, alert, (short) severity); + } + + NightVisionChanged(ent); + } + + private void ToggleNightVisionItem(Entity item, EntityUid user) + { + if (item.Comp.User == user && item.Comp.Toggleable) + { + DisableNightVisionItem(item, item.Comp.User); + return; + } + + EnableNightVisionItem(item, user); + } + + private void EnableNightVisionItem(Entity item, EntityUid user) + { + DisableNightVisionItem(item, item.Comp.User); + + item.Comp.User = user; + Dirty(item); + + _appearance.SetData(item, NightVisionItemVisuals.Active, true); + + if (!_timing.ApplyingState) + { + var nightVision = EnsureComp(user); + nightVision.State = NightVisionState.Full; + Dirty(user, nightVision); + } + + _actions.SetToggled(item.Comp.Action, true); + } + + protected virtual void NightVisionChanged(Entity ent) + { + } + + protected virtual void NightVisionRemoved(Entity ent) + { + } + + protected void DisableNightVisionItem(Entity item, EntityUid? user) + { + _actions.SetToggled(item.Comp.Action, false); + + item.Comp.User = null; + Dirty(item); + + _appearance.SetData(item, NightVisionItemVisuals.Active, false); + + if (TryComp(user, out NightVisionComponent? nightVision) && + !nightVision.Innate) + { + RemCompDeferred(user.Value); + } + } + + public void SetSeeThroughContainers(Entity ent, bool see) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + ent.Comp.SeeThroughContainers = see; + Dirty(ent); + } +} diff --git a/Content.Shared/ADT/NightVision/ToggleNightVision.cs b/Content.Shared/ADT/NightVision/ToggleNightVision.cs new file mode 100644 index 00000000000..d7b3a10f064 --- /dev/null +++ b/Content.Shared/ADT/NightVision/ToggleNightVision.cs @@ -0,0 +1,13 @@ +using Content.Shared.Alert; + +namespace Content.Shared.ADT.NightVision; + +[DataDefinition] +public sealed partial class ToggleNightVision : IAlertClick +{ + public void AlertClicked(EntityUid player) + { + var entities = IoCManager.Resolve(); + entities.System().Toggle(player); + } +} diff --git a/Resources/Prototypes/ADT/Actions/nightvision.yml b/Resources/Prototypes/ADT/Actions/nightvision.yml new file mode 100644 index 00000000000..d22c98b6cf7 --- /dev/null +++ b/Resources/Prototypes/ADT/Actions/nightvision.yml @@ -0,0 +1,16 @@ +- type: entity + id: ActionToggleNinjaNightVision + categories: [HideSpawnMenu] + name: Toggle ninja visor nightvision + description: Allows you to see even in complete darkness. + components: + - type: InstantAction + icon: + sprite: Clothing/Eyes/Glasses/ninjavisor.rsi + state: icon + iconOn: + sprite: Clothing/Eyes/Glasses/ninjavisor.rsi + state: icon + event: !type:ToggleActionEvent + useDelay: 0.25 + diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml index d5200747445..c8c03b0237e 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml @@ -243,3 +243,11 @@ - type: Clothing sprite: Clothing/Eyes/Glasses/ninjavisor.rsi - type: FlashImmunity + - type: NightVisionItem + - type: Appearance + - type: GenericVisualizer + visuals: + enum.NightVisionItemVisuals.Active: + icon: + True: { state: icon } + False: { state: icon }