diff --git a/Content.Client/Antag/AntagStatusIconSystem.cs b/Content.Client/Antag/AntagStatusIconSystem.cs index 4e2468eea59..63fc434946a 100644 --- a/Content.Client/Antag/AntagStatusIconSystem.cs +++ b/Content.Client/Antag/AntagStatusIconSystem.cs @@ -29,6 +29,7 @@ public override void Initialize() SubscribeLocalEvent(GetIcon); SubscribeLocalEvent(GetIcon); SubscribeLocalEvent(GetIcon); + SubscribeLocalEvent(GetIcon); //end-backmen: antag SubscribeLocalEvent(GetIcon); } diff --git a/Content.Client/Chemistry/Components/HyposprayComponent.cs b/Content.Client/Chemistry/Components/HyposprayComponent.cs deleted file mode 100644 index 705b79ad84c..00000000000 --- a/Content.Client/Chemistry/Components/HyposprayComponent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Shared.Chemistry.Components; -using Content.Shared.FixedPoint; - -namespace Content.Client.Chemistry.Components -{ - [RegisterComponent] - public sealed partial class HyposprayComponent : SharedHyposprayComponent - { - [ViewVariables] - public FixedPoint2 CurrentVolume; - [ViewVariables] - public FixedPoint2 TotalVolume; - [ViewVariables(VVAccess.ReadWrite)] - public bool UiUpdateNeeded; - } -} diff --git a/Content.Client/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Client/Chemistry/EntitySystems/HypospraySystem.cs new file mode 100644 index 00000000000..ee7aa3aafe3 --- /dev/null +++ b/Content.Client/Chemistry/EntitySystems/HypospraySystem.cs @@ -0,0 +1,15 @@ +using Content.Client.Chemistry.UI; +using Content.Client.Items; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; + +namespace Content.Client.Chemistry.EntitySystems; + +public sealed class HypospraySystem : SharedHypospraySystem +{ + public override void Initialize() + { + base.Initialize(); + Subs.ItemStatus(ent => new HyposprayStatusControl(ent, _solutionContainers)); + } +} diff --git a/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs index 12eb7f3d14d..0131a283c8c 100644 --- a/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs +++ b/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs @@ -1,4 +1,3 @@ -using Content.Client.Chemistry.Components; using Content.Client.Chemistry.UI; using Content.Client.Items; using Content.Shared.Chemistry.Components; @@ -13,17 +12,5 @@ public override void Initialize() { base.Initialize(); Subs.ItemStatus(ent => new InjectorStatusControl(ent, SolutionContainers)); - SubscribeLocalEvent(OnHandleHyposprayState); - Subs.ItemStatus(ent => new HyposprayStatusControl(ent)); - } - - private void OnHandleHyposprayState(EntityUid uid, HyposprayComponent component, ref ComponentHandleState args) - { - if (args.Current is not HyposprayComponentState cState) - return; - - component.CurrentVolume = cState.CurVolume; - component.TotalVolume = cState.MaxVolume; - component.UiUpdateNeeded = true; } } diff --git a/Content.Client/Chemistry/UI/HyposprayStatusControl.cs b/Content.Client/Chemistry/UI/HyposprayStatusControl.cs index bd85cd546cc..4a4d90dc4d5 100644 --- a/Content.Client/Chemistry/UI/HyposprayStatusControl.cs +++ b/Content.Client/Chemistry/UI/HyposprayStatusControl.cs @@ -1,6 +1,8 @@ -using Content.Client.Chemistry.Components; using Content.Client.Message; using Content.Client.Stylesheets; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.FixedPoint; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Timing; @@ -9,34 +11,48 @@ namespace Content.Client.Chemistry.UI; public sealed class HyposprayStatusControl : Control { - private readonly HyposprayComponent _parent; + private readonly Entity _parent; private readonly RichTextLabel _label; + private readonly SharedSolutionContainerSystem _solutionContainers; - public HyposprayStatusControl(HyposprayComponent parent) + private FixedPoint2 PrevVolume; + private FixedPoint2 PrevMaxVolume; + private bool PrevOnlyAffectsMobs; + + public HyposprayStatusControl(Entity parent, SharedSolutionContainerSystem solutionContainers) { _parent = parent; - _label = new RichTextLabel {StyleClasses = {StyleNano.StyleClassItemStatus}}; + _solutionContainers = solutionContainers; + _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; AddChild(_label); - - Update(); } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); - if (!_parent.UiUpdateNeeded) + + if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution)) return; - Update(); - } - public void Update() - { + // only updates the UI if any of the details are different than they previously were + if (PrevVolume == solution.Volume + && PrevMaxVolume == solution.MaxVolume + && PrevOnlyAffectsMobs == _parent.Comp.OnlyAffectsMobs) + return; + + PrevVolume = solution.Volume; + PrevMaxVolume = solution.MaxVolume; + PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs; - _parent.UiUpdateNeeded = false; + var modeStringLocalized = Loc.GetString(_parent.Comp.OnlyAffectsMobs switch + { + false => "hypospray-all-mode-text", + true => "hypospray-mobs-only-mode-text", + }); - _label.SetMarkup(Loc.GetString( - "hypospray-volume-text", - ("currentVolume", _parent.CurrentVolume), - ("totalVolume", _parent.TotalVolume))); + _label.SetMarkup(Loc.GetString("hypospray-volume-label", + ("currentVolume", solution.Volume), + ("totalVolume", solution.MaxVolume), + ("modeString", modeStringLocalized))); } } diff --git a/Content.Client/Chemistry/UI/InjectorStatusControl.cs b/Content.Client/Chemistry/UI/InjectorStatusControl.cs index 9cb699330c2..ba1f97cd1e4 100644 --- a/Content.Client/Chemistry/UI/InjectorStatusControl.cs +++ b/Content.Client/Chemistry/UI/InjectorStatusControl.cs @@ -17,6 +17,7 @@ public sealed class InjectorStatusControl : Control private FixedPoint2 PrevVolume; private FixedPoint2 PrevMaxVolume; + private FixedPoint2 PrevTransferAmount; private InjectorToggleMode PrevToggleState; public InjectorStatusControl(Entity parent, SharedSolutionContainerSystem solutionContainers) @@ -37,11 +38,13 @@ protected override void FrameUpdate(FrameEventArgs args) // only updates the UI if any of the details are different than they previously were if (PrevVolume == solution.Volume && PrevMaxVolume == solution.MaxVolume + && PrevTransferAmount == _parent.Comp.TransferAmount && PrevToggleState == _parent.Comp.ToggleState) return; PrevVolume = solution.Volume; PrevMaxVolume = solution.MaxVolume; + PrevTransferAmount = _parent.Comp.TransferAmount; PrevToggleState = _parent.Comp.ToggleState; // Update current volume and injector state diff --git a/Content.Client/Disposal/Systems/DisposalUnitSystem.cs b/Content.Client/Disposal/Systems/DisposalUnitSystem.cs index 8c40c784219..b9e4a386604 100644 --- a/Content.Client/Disposal/Systems/DisposalUnitSystem.cs +++ b/Content.Client/Disposal/Systems/DisposalUnitSystem.cs @@ -96,24 +96,22 @@ private void OnAppearanceChange(EntityUid uid, SharedDisposalUnitComponent unit, private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, SpriteComponent sprite, AppearanceComponent appearance) { if (!_appearanceSystem.TryGetData(uid, Visuals.VisualState, out var state, appearance)) - { return; - } sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == VisualState.UnAnchored); sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == VisualState.Anchored); - sprite.LayerSetVisible(DisposalUnitVisualLayers.BaseFlush, state is VisualState.Flushing or VisualState.Charging); + sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFlush, state is VisualState.OverlayFlushing or VisualState.OverlayCharging); var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer) ? sprite.LayerGetState(chargingLayer) : new RSI.StateId(DefaultChargeState); // This is a transient state so not too worried about replaying in range. - if (state == VisualState.Flushing) + if (state == VisualState.OverlayFlushing) { if (!_animationSystem.HasRunningAnimation(uid, AnimationKey)) { - var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseFlush, out var flushLayer) + var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.OverlayFlush, out var flushLayer) ? sprite.LayerGetState(flushLayer) : new RSI.StateId(DefaultFlushState); @@ -125,7 +123,7 @@ private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, Sprite { new AnimationTrackSpriteFlick { - LayerKey = DisposalUnitVisualLayers.BaseFlush, + LayerKey = DisposalUnitVisualLayers.OverlayFlush, KeyFrames = { // Play the flush animation @@ -154,26 +152,18 @@ private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, Sprite _animationSystem.Play(uid, anim, AnimationKey); } } - else if (state == VisualState.Charging) - { - sprite.LayerSetState(DisposalUnitVisualLayers.BaseFlush, chargingState); - } + else if (state == VisualState.OverlayCharging) + sprite.LayerSetState(DisposalUnitVisualLayers.OverlayFlush, new RSI.StateId("disposal-charging")); else - { _animationSystem.Stop(uid, AnimationKey); - } if (!_appearanceSystem.TryGetData(uid, Visuals.Handle, out var handleState, appearance)) - { handleState = HandleState.Normal; - } sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != HandleState.Normal); if (!_appearanceSystem.TryGetData(uid, Visuals.Light, out var lightState, appearance)) - { lightState = LightStates.Off; - } sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayCharging, (lightState & LightStates.Charging) != 0); @@ -189,7 +179,7 @@ public enum DisposalUnitVisualLayers : byte Unanchored, Base, BaseCharging, - BaseFlush, + OverlayFlush, OverlayCharging, OverlayReady, OverlayFull, diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 589de6d6a78..2e888b3df98 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -45,6 +45,9 @@ public static void SetupContexts(IInputContextContainer contexts) // Not in engine because the engine doesn't understand what a flipped object is common.AddFunction(ContentKeyFunctions.EditorFlipObject); + // Not in engine so that the RCD can rotate objects + common.AddFunction(EngineKeyFunctions.EditorRotateObject); + var human = contexts.GetContext("human"); human.AddFunction(EngineKeyFunctions.MoveUp); human.AddFunction(EngineKeyFunctions.MoveDown); diff --git a/Content.Client/Paint/PaintVisualizerSystem.cs b/Content.Client/Paint/PaintVisualizerSystem.cs deleted file mode 100644 index 8d037811fab..00000000000 --- a/Content.Client/Paint/PaintVisualizerSystem.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Linq; -using Robust.Client.GameObjects; -using static Robust.Client.GameObjects.SpriteComponent; -using Content.Shared.Clothing; -using Content.Shared.Hands; -using Content.Shared.Paint; -using Robust.Client.Graphics; -using Robust.Shared.Prototypes; - -namespace Content.Client.Paint; - -public sealed class PaintedVisualizerSystem : VisualizerSystem -{ - /// - /// Visualizer for Paint which applies a shader and colors the entity. - /// - - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly IPrototypeManager _protoMan = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnHeldVisualsUpdated); - SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnEquipmentVisualsUpdated); - } - - protected override void OnAppearanceChange(EntityUid uid, PaintedComponent component, ref AppearanceChangeEvent args) - { - var shader = _protoMan.Index(component.ShaderName).Instance(); - - if (args.Sprite == null) - return; - - // What is this even doing? It's not even checking what the value is. - if (!_appearance.TryGetData(uid, PaintVisuals.Painted, out bool isPainted)) - return; - - var sprite = args.Sprite; - - foreach (var spriteLayer in sprite.AllLayers) - { - if (spriteLayer is not Layer layer) - continue; - - if (layer.Shader == null) // If shader isn't null we dont want to replace the original shader. - { - layer.Shader = shader; - layer.Color = component.Color; - } - } - } - - private void OnHeldVisualsUpdated(EntityUid uid, PaintedComponent component, HeldVisualsUpdatedEvent args) - { - if (args.RevealedLayers.Count == 0) - return; - - if (!TryComp(args.User, out SpriteComponent? sprite)) - return; - - foreach (var revealed in args.RevealedLayers) - { - if (!sprite.LayerMapTryGet(revealed, out var layer)) - continue; - - sprite.LayerSetShader(layer, component.ShaderName); - sprite.LayerSetColor(layer, component.Color); - } - } - - private void OnEquipmentVisualsUpdated(EntityUid uid, PaintedComponent component, EquipmentVisualsUpdatedEvent args) - { - if (args.RevealedLayers.Count == 0) - return; - - if (!TryComp(args.Equipee, out SpriteComponent? sprite)) - return; - - foreach (var revealed in args.RevealedLayers) - { - if (!sprite.LayerMapTryGet(revealed, out var layer)) - continue; - - sprite.LayerSetShader(layer, component.ShaderName); - sprite.LayerSetColor(layer, component.Color); - } - } - - private void OnShutdown(EntityUid uid, PaintedComponent component, ref ComponentShutdown args) - { - if (!TryComp(uid, out SpriteComponent? sprite)) - return; - - component.BeforeColor = sprite.Color; - var shader = _protoMan.Index(component.ShaderName).Instance(); - - if (!Terminating(uid)) - { - foreach (var spriteLayer in sprite.AllLayers) - { - if (spriteLayer is not Layer layer) - continue; - - if (layer.Shader == shader) // If shader isn't same as one in component we need to ignore it. - { - layer.Shader = null; - if (layer.Color == component.Color) // If color isn't the same as one in component we don't want to change it. - layer.Color = component.BeforeColor; - } - } - } - } -} diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs index a8ec7b37a0b..677092e1918 100644 --- a/Content.Client/Pinpointer/UI/NavMapControl.cs +++ b/Content.Client/Pinpointer/UI/NavMapControl.cs @@ -114,9 +114,16 @@ public NavMapControl() : base(MinDisplayedRange, MaxDisplayedRange, DefaultDispl VerticalExpand = false, Children = { - _zoom, - _beacons, - _recenter, + new BoxContainer() + { + Orientation = BoxContainer.LayoutOrientation.Horizontal, + Children = + { + _zoom, + _beacons, + _recenter + } + } } }; diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs index 479fb02906c..fcc8bfc420a 100644 --- a/Content.Client/Popups/PopupSystem.cs +++ b/Content.Client/Popups/PopupSystem.cs @@ -163,10 +163,13 @@ public override void PopupEntity(string? message, EntityUid uid, Filter filter, PopupEntity(message, uid, type); } - public override void PopupClient(string? message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small) + public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) { + if (recipient == null) + return; + if (_timing.IsFirstTimePredicted) - PopupEntity(message, uid, recipient, type); + PopupEntity(message, uid, recipient.Value, type); } public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small) diff --git a/Content.Client/RCD/AlignRCDConstruction.cs b/Content.Client/RCD/AlignRCDConstruction.cs new file mode 100644 index 00000000000..da7b22c91a8 --- /dev/null +++ b/Content.Client/RCD/AlignRCDConstruction.cs @@ -0,0 +1,122 @@ +using System.Numerics; +using Content.Client.Gameplay; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.RCD.Components; +using Content.Shared.RCD.Systems; +using Robust.Client.Placement; +using Robust.Client.Player; +using Robust.Client.State; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; + +namespace Content.Client.RCD; + +public sealed class AlignRCDConstruction : PlacementMode +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly SharedMapSystem _mapSystem = default!; + [Dependency] private readonly RCDSystem _rcdSystem = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IStateManager _stateManager = default!; + + private const float SearchBoxSize = 2f; + private const float PlaceColorBaseAlpha = 0.5f; + + private EntityCoordinates _unalignedMouseCoords = default; + + /// + /// This placement mode is not on the engine because it is content specific (i.e., for the RCD) + /// + public AlignRCDConstruction(PlacementManager pMan) : base(pMan) + { + var dependencies = IoCManager.Instance!; + _entityManager = dependencies.Resolve(); + _mapManager = dependencies.Resolve(); + _playerManager = dependencies.Resolve(); + _stateManager = dependencies.Resolve(); + + _mapSystem = _entityManager.System(); + _rcdSystem = _entityManager.System(); + _transformSystem = _entityManager.System(); + + ValidPlaceColor = ValidPlaceColor.WithAlpha(PlaceColorBaseAlpha); + } + + public override void AlignPlacementMode(ScreenCoordinates mouseScreen) + { + _unalignedMouseCoords = ScreenToCursorGrid(mouseScreen); + MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager); + + var gridId = MouseCoords.GetGridUid(_entityManager); + + if (!_entityManager.TryGetComponent(gridId, out var mapGrid)) + return; + + CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords); + + float tileSize = mapGrid.TileSize; + GridDistancing = tileSize; + + if (pManager.CurrentPermission!.IsTile) + { + MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2, + CurrentTile.Y + tileSize / 2)); + } + else + { + MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X, + CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y)); + } + } + + public override bool IsValidPosition(EntityCoordinates position) + { + var player = _playerManager.LocalSession?.AttachedEntity; + + // If the destination is out of interaction range, set the placer alpha to zero + if (!_entityManager.TryGetComponent(player, out var xform)) + return false; + + if (!xform.Coordinates.InRange(_entityManager, _transformSystem, position, SharedInteractionSystem.InteractionRange)) + { + InvalidPlaceColor = InvalidPlaceColor.WithAlpha(0); + return false; + } + + // Otherwise restore the alpha value + else + { + InvalidPlaceColor = InvalidPlaceColor.WithAlpha(PlaceColorBaseAlpha); + } + + // Determine if player is carrying an RCD in their active hand + if (!_entityManager.TryGetComponent(player, out var hands)) + return false; + + var heldEntity = hands.ActiveHand?.HeldEntity; + + if (!_entityManager.TryGetComponent(heldEntity, out var rcd)) + return false; + + // Retrieve the map grid data for the position + if (!_rcdSystem.TryGetMapGridData(position, out var mapGridData)) + return false; + + // Determine if the user is hovering over a target + var currentState = _stateManager.CurrentState; + + if (currentState is not GameplayStateBase screen) + return false; + + var target = screen.GetClickedEntity(_unalignedMouseCoords.ToMap(_entityManager, _transformSystem)); + + // Determine if the RCD operation is valid or not + if (!_rcdSystem.IsRCDOperationStillValid(heldEntity.Value, rcd, mapGridData.Value, target, player.Value, false)) + return false; + + return true; + } +} diff --git a/Content.Client/RCD/RCDConstructionGhostSystem.cs b/Content.Client/RCD/RCDConstructionGhostSystem.cs new file mode 100644 index 00000000000..792916b8922 --- /dev/null +++ b/Content.Client/RCD/RCDConstructionGhostSystem.cs @@ -0,0 +1,78 @@ +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.RCD; +using Content.Shared.RCD.Components; +using Content.Shared.RCD.Systems; +using Robust.Client.Placement; +using Robust.Client.Player; +using Robust.Shared.Enums; + +namespace Content.Client.RCD; + +public sealed class RCDConstructionGhostSystem : EntitySystem +{ + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly RCDSystem _rcdSystem = default!; + [Dependency] private readonly IPlacementManager _placementManager = default!; + + private string _placementMode = typeof(AlignRCDConstruction).Name; + private Direction _placementDirection = default; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // Get current placer data + var placerEntity = _placementManager.CurrentPermission?.MobUid; + var placerProto = _placementManager.CurrentPermission?.EntityType; + var placerIsRCD = HasComp(placerEntity); + + // Exit if erasing or the current placer is not an RCD (build mode is active) + if (_placementManager.Eraser || (placerEntity != null && !placerIsRCD)) + return; + + // Determine if player is carrying an RCD in their active hand + var player = _playerManager.LocalSession?.AttachedEntity; + + if (!TryComp(player, out var hands)) + return; + + var heldEntity = hands.ActiveHand?.HeldEntity; + + if (!TryComp(heldEntity, out var rcd)) + { + // If the player was holding an RCD, but is no longer, cancel placement + if (placerIsRCD) + _placementManager.Clear(); + + return; + } + + // Update the direction the RCD prototype based on the placer direction + if (_placementDirection != _placementManager.Direction) + { + _placementDirection = _placementManager.Direction; + RaiseNetworkEvent(new RCDConstructionGhostRotationEvent(GetNetEntity(heldEntity.Value), _placementDirection)); + } + + // If the placer has not changed, exit + _rcdSystem.UpdateCachedPrototype(heldEntity.Value, rcd); + + if (heldEntity == placerEntity && rcd.CachedPrototype.Prototype == placerProto) + return; + + // Create a new placer + var newObjInfo = new PlacementInformation + { + MobUid = heldEntity.Value, + PlacementOption = _placementMode, + EntityType = rcd.CachedPrototype.Prototype, + Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange), + IsTile = (rcd.CachedPrototype.Mode == RcdMode.ConstructTile), + UseEditorContext = false, + }; + + _placementManager.Clear(); + _placementManager.BeginPlacing(newObjInfo); + } +} diff --git a/Content.Client/RCD/RCDMenu.xaml b/Content.Client/RCD/RCDMenu.xaml new file mode 100644 index 00000000000..b3d5367a5fd --- /dev/null +++ b/Content.Client/RCD/RCDMenu.xaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/RCD/RCDMenu.xaml.cs b/Content.Client/RCD/RCDMenu.xaml.cs new file mode 100644 index 00000000000..8679e789dc7 --- /dev/null +++ b/Content.Client/RCD/RCDMenu.xaml.cs @@ -0,0 +1,137 @@ +using Content.Client.UserInterface.Controls; +using Content.Shared.RCD; +using Content.Shared.RCD.Components; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using System.Numerics; + +namespace Content.Client.RCD; + +[GenerateTypedNameReferences] +public sealed partial class RCDMenu : RadialMenu +{ + [Dependency] private readonly EntityManager _entManager = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + + private readonly SpriteSystem _spriteSystem; + + public event Action>? SendRCDSystemMessageAction; + + public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui) + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + + _spriteSystem = _entManager.System(); + + // Find the main radial container + var main = FindControl("Main"); + + if (main == null) + return; + + // Populate secondary radial containers + if (!_entManager.TryGetComponent(owner, out var rcd)) + return; + + foreach (var protoId in rcd.AvailablePrototypes) + { + if (!_protoManager.TryIndex(protoId, out var proto)) + continue; + + if (proto.Mode == RcdMode.Invalid) + continue; + + var parent = FindControl(proto.Category); + + if (parent == null) + continue; + + var name = Loc.GetString(proto.SetName); + name = char.ToUpper(name[0]) + name.Remove(0, 1); + + var button = new RCDMenuButton() + { + StyleClasses = { "RadialMenuButton" }, + SetSize = new Vector2(64f, 64f), + ToolTip = name, + ProtoId = protoId, + }; + + if (proto.Sprite != null) + { + var tex = new TextureRect() + { + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Texture = _spriteSystem.Frame0(proto.Sprite), + TextureScale = new Vector2(2f, 2f), + }; + + button.AddChild(tex); + } + + parent.AddChild(button); + + // Ensure that the button that transitions the menu to the associated category layer + // is visible in the main radial container (as these all start with Visible = false) + foreach (var child in main.Children) + { + var castChild = child as RadialMenuTextureButton; + + if (castChild is not RadialMenuTextureButton) + continue; + + if (castChild.TargetLayer == proto.Category) + { + castChild.Visible = true; + break; + } + } + } + + // Set up menu actions + foreach (var child in Children) + AddRCDMenuButtonOnClickActions(child); + + OnChildAdded += AddRCDMenuButtonOnClickActions; + + SendRCDSystemMessageAction += bui.SendRCDSystemMessage; + } + + private void AddRCDMenuButtonOnClickActions(Control control) + { + var radialContainer = control as RadialContainer; + + if (radialContainer == null) + return; + + foreach (var child in radialContainer.Children) + { + var castChild = child as RCDMenuButton; + + if (castChild == null) + continue; + + castChild.OnButtonUp += _ => + { + SendRCDSystemMessageAction?.Invoke(castChild.ProtoId); + Close(); + }; + } + } +} + +public sealed class RCDMenuButton : RadialMenuTextureButton +{ + public ProtoId ProtoId { get; set; } + + public RCDMenuButton() + { + + } +} diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs new file mode 100644 index 00000000000..a37dbcecf8c --- /dev/null +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -0,0 +1,49 @@ +using Content.Shared.RCD; +using Content.Shared.RCD.Components; +using JetBrains.Annotations; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Shared.Prototypes; + +namespace Content.Client.RCD; + +[UsedImplicitly] +public sealed class RCDMenuBoundUserInterface : BoundUserInterface +{ + [Dependency] private readonly IClyde _displayManager = default!; + [Dependency] private readonly IInputManager _inputManager = default!; + + private RCDMenu? _menu; + + public RCDMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + IoCManager.InjectDependencies(this); + } + + protected override void Open() + { + base.Open(); + + _menu = new(Owner, this); + _menu.OnClose += Close; + + // Open the menu, centered on the mouse + var vpSize = _displayManager.ScreenSize; + _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); + } + + public void SendRCDSystemMessage(ProtoId protoId) + { + // A predicted message cannot be used here as the RCD UI is closed immediately + // after this message is sent, which will stop the server from receiving it + SendMessage(new RCDSystemMessage(protoId)); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) return; + + _menu?.Dispose(); + } +} diff --git a/Content.Client/Shuttles/UI/MapScreen.xaml.cs b/Content.Client/Shuttles/UI/MapScreen.xaml.cs index 8430699bae1..225d1be14e5 100644 --- a/Content.Client/Shuttles/UI/MapScreen.xaml.cs +++ b/Content.Client/Shuttles/UI/MapScreen.xaml.cs @@ -5,6 +5,7 @@ using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; using Content.Shared.Shuttles.UI.MapObjects; +using Content.Shared.Timing; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface; @@ -38,16 +39,11 @@ public sealed partial class MapScreen : BoxContainer private EntityUid? _shuttleEntity; private FTLState _state; - private float _ftlDuration; + private StartEndTime _ftlTime; private List _beacons = new(); private List _exclusions = new(); - /// - /// When the next FTL state change happens. - /// - private TimeSpan _nextFtlTime; - private TimeSpan _nextPing; private TimeSpan _pingCooldown = TimeSpan.FromSeconds(3); private TimeSpan _nextMapDequeue; @@ -114,8 +110,7 @@ public void UpdateState(ShuttleMapInterfaceState state) _beacons = state.Destinations; _exclusions = state.Exclusions; _state = state.FTLState; - _ftlDuration = state.FTLDuration; - _nextFtlTime = _timing.CurTime + TimeSpan.FromSeconds(_ftlDuration); + _ftlTime = state.FTLTime; MapRadar.InFtl = true; MapFTLState.Text = Loc.GetString($"shuttle-console-ftl-state-{_state.ToString()}"); @@ -511,20 +506,8 @@ protected override void FrameUpdate(FrameEventArgs args) MapRebuildButton.Disabled = false; } - var ftlDiff = (float) (_nextFtlTime - _timing.CurTime).TotalSeconds; - - float ftlRatio; - - if (_ftlDuration.Equals(0f)) - { - ftlRatio = 1f; - } - else - { - ftlRatio = Math.Clamp(1f - (ftlDiff / _ftlDuration), 0f, 1f); - } - - FTLBar.Value = ftlRatio; + var progress = _ftlTime.ProgressAt(curTime); + FTLBar.Value = float.IsFinite(progress) ? progress : 1; } protected override void Draw(DrawingHandleScreen handle) diff --git a/Content.Client/Store/Ui/StoreBoundUserInterface.cs b/Content.Client/Store/Ui/StoreBoundUserInterface.cs index af5bb4f49be..7bb236db48f 100644 --- a/Content.Client/Store/Ui/StoreBoundUserInterface.cs +++ b/Content.Client/Store/Ui/StoreBoundUserInterface.cs @@ -1,22 +1,27 @@ using Content.Shared.Store; using JetBrains.Annotations; -using Robust.Client.GameObjects; using System.Linq; -using System.Threading; -using Serilog; -using Timer = Robust.Shared.Timing.Timer; +using Robust.Shared.Prototypes; namespace Content.Client.Store.Ui; [UsedImplicitly] public sealed class StoreBoundUserInterface : BoundUserInterface { + private IPrototypeManager _prototypeManager = default!; + [ViewVariables] private StoreMenu? _menu; [ViewVariables] private string _windowName = Loc.GetString("store-ui-default-title"); + [ViewVariables] + private string _search = ""; + + [ViewVariables] + private HashSet _listings = new(); + public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } @@ -49,6 +54,12 @@ protected override void Open() SendMessage(new StoreRequestUpdateInterfaceMessage()); }; + _menu.SearchTextUpdated += (_, search) => + { + _search = search.Trim().ToLowerInvariant(); + UpdateListingsWithSearchFilter(); + }; + _menu.OnRefundAttempt += (_) => { SendMessage(new StoreRequestRefundMessage()); @@ -64,11 +75,14 @@ protected override void UpdateState(BoundUserInterfaceState state) switch (state) { case StoreUpdateState msg: + // start-backmen: bank _menu.SetCanBuyFromBank(IoCManager.Resolve().HasComponent(Owner)); // backmen: currency - _menu.UpdateBalance(msg.Balance); - _menu.PopulateStoreCategoryButtons(msg.Listings); + // end-backmen: bank + + _listings = msg.Listings; - _menu.UpdateListing(msg.Listings.ToList()); + _menu.UpdateBalance(msg.Balance); + UpdateListingsWithSearchFilter(); _menu.SetFooterVisibility(msg.ShowFooter); _menu.UpdateRefund(msg.AllowRefund); break; @@ -90,4 +104,19 @@ protected override void Dispose(bool disposing) _menu?.Close(); _menu?.Dispose(); } + + private void UpdateListingsWithSearchFilter() + { + if (_menu == null) + return; + + var filteredListings = new HashSet(_listings); + if (!string.IsNullOrEmpty(_search)) + { + filteredListings.RemoveWhere(listingData => !ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search) && + !ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search)); + } + _menu.PopulateStoreCategoryButtons(filteredListings); + _menu.UpdateListing(filteredListings.ToList()); + } } diff --git a/Content.Client/Store/Ui/StoreMenu.xaml b/Content.Client/Store/Ui/StoreMenu.xaml index 4b38352a44a..fc4cbe444fc 100644 --- a/Content.Client/Store/Ui/StoreMenu.xaml +++ b/Content.Client/Store/Ui/StoreMenu.xaml @@ -28,7 +28,8 @@ HorizontalAlignment="Right" Text="Refund" /> - + + diff --git a/Content.Client/Store/Ui/StoreMenu.xaml.cs b/Content.Client/Store/Ui/StoreMenu.xaml.cs index c465e0e392a..7f997a115b9 100644 --- a/Content.Client/Store/Ui/StoreMenu.xaml.cs +++ b/Content.Client/Store/Ui/StoreMenu.xaml.cs @@ -1,5 +1,4 @@ using System.Linq; -using System.Threading; using Content.Client.Actions; using Content.Client.GameTicking.Managers; using Content.Client.Message; @@ -27,6 +26,7 @@ public sealed partial class StoreMenu : DefaultWindow private StoreWithdrawWindow? _withdrawWindow; + public event EventHandler? SearchTextUpdated; public event Action? OnListingButtonPressed; public event Action? OnCategoryButtonPressed; public event Action? OnWithdrawAttempt; @@ -46,6 +46,7 @@ public StoreMenu(string name) WithdrawButton.OnButtonDown += OnWithdrawButtonDown; RefreshButton.OnButtonDown += OnRefreshButtonDown; RefundButton.OnButtonDown += OnRefundButtonDown; + SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text); if (Window != null) Window.Title = name; @@ -68,7 +69,7 @@ public void UpdateBalance(Dictionary balance) (type.Key, type.Value), type => _prototypeManager.Index(type.Key)); var balanceStr = string.Empty; - foreach (var ((type, amount),proto) in currency) + foreach (var ((_, amount), proto) in currency) { balanceStr += Loc.GetString("store-ui-balance-display", ("amount", amount), ("currency", Loc.GetString(proto.DisplayName, ("amount", 1)))); @@ -90,7 +91,6 @@ public void UpdateListing(List listings) { var sorted = listings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum()); - // should probably chunk these out instead. to-do if this clogs the internet tubes. // maybe read clients prototypes instead? ClearListings(); @@ -138,8 +138,8 @@ private void AddListingGui(ListingData listing) if (!listing.Categories.Contains(CurrentCategory)) return; - var listingName = Loc.GetString(listing.Name); - var listingDesc = Loc.GetString(listing.Description); + var listingName = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager); + var listingDesc = ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listing, _prototypeManager); var listingPrice = listing.Cost; var canBuy = CanBuyListing(Balance, listingPrice); @@ -153,12 +153,6 @@ private void AddListingGui(ListingData listing) { if (texture == null) texture = spriteSys.GetPrototypeIcon(listing.ProductEntity).Default; - - var proto = _prototypeManager.Index(listing.ProductEntity); - if (listingName == string.Empty) - listingName = proto.Name; - if (listingDesc == string.Empty) - listingDesc = proto.Description; } else if (listing.ProductAction != null) { @@ -252,13 +246,16 @@ public void PopulateStoreCategoryButtons(HashSet listings) allCategories = allCategories.OrderBy(c => c.Priority).ToList(); + // This will reset the Current Category selection if nothing matches the search. + if (allCategories.All(category => category.ID != CurrentCategory)) + CurrentCategory = string.Empty; + if (CurrentCategory == string.Empty && allCategories.Count > 0) CurrentCategory = allCategories.First().ID; - if (allCategories.Count <= 1) - return; - CategoryListContainer.Children.Clear(); + if (allCategories.Count < 1) + return; foreach (var proto in allCategories) { diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index 426af1616ec..2c7a1873a36 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -290,7 +290,7 @@ public StyleNano(IResourceCache resCache) : base(resCache) var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); var topButtonBase = new StyleBoxTexture { - Texture = buttonTex, + Texture = buttonTex, }; topButtonBase.SetPatchMargin(StyleBox.Margin.All, 10); topButtonBase.SetPadding(StyleBox.Margin.All, 0); @@ -298,19 +298,19 @@ public StyleNano(IResourceCache resCache) : base(resCache) var topButtonOpenRight = new StyleBoxTexture(topButtonBase) { - Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))), + Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))), }; topButtonOpenRight.SetPatchMargin(StyleBox.Margin.Right, 0); var topButtonOpenLeft = new StyleBoxTexture(topButtonBase) { - Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))), + Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))), }; topButtonOpenLeft.SetPatchMargin(StyleBox.Margin.Left, 0); var topButtonSquare = new StyleBoxTexture(topButtonBase) { - Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))), + Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))), }; topButtonSquare.SetPatchMargin(StyleBox.Margin.Horizontal, 0); @@ -368,9 +368,9 @@ public StyleNano(IResourceCache resCache) : base(resCache) }; tabContainerPanel.SetPatchMargin(StyleBox.Margin.All, 2); - var tabContainerBoxActive = new StyleBoxFlat {BackgroundColor = new Color(64, 64, 64)}; + var tabContainerBoxActive = new StyleBoxFlat { BackgroundColor = new Color(64, 64, 64) }; tabContainerBoxActive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5); - var tabContainerBoxInactive = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 32)}; + var tabContainerBoxInactive = new StyleBoxFlat { BackgroundColor = new Color(32, 32, 32) }; tabContainerBoxInactive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5); var progressBarBackground = new StyleBoxFlat @@ -409,21 +409,21 @@ public StyleNano(IResourceCache resCache) : base(resCache) // Placeholder var placeholderTexture = resCache.GetTexture("/Textures/Interface/Nano/placeholder.png"); - var placeholder = new StyleBoxTexture {Texture = placeholderTexture}; + var placeholder = new StyleBoxTexture { Texture = placeholderTexture }; placeholder.SetPatchMargin(StyleBox.Margin.All, 19); placeholder.SetExpandMargin(StyleBox.Margin.All, -5); placeholder.Mode = StyleBoxTexture.StretchMode.Tile; - var itemListBackgroundSelected = new StyleBoxFlat {BackgroundColor = new Color(75, 75, 86)}; + var itemListBackgroundSelected = new StyleBoxFlat { BackgroundColor = new Color(75, 75, 86) }; itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Vertical, 2); itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); - var itemListItemBackgroundDisabled = new StyleBoxFlat {BackgroundColor = new Color(10, 10, 12)}; + var itemListItemBackgroundDisabled = new StyleBoxFlat { BackgroundColor = new Color(10, 10, 12) }; itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Vertical, 2); itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); - var itemListItemBackground = new StyleBoxFlat {BackgroundColor = new Color(55, 55, 68)}; + var itemListItemBackground = new StyleBoxFlat { BackgroundColor = new Color(55, 55, 68) }; itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 2); itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); - var itemListItemBackgroundTransparent = new StyleBoxFlat {BackgroundColor = Color.Transparent}; + var itemListItemBackgroundTransparent = new StyleBoxFlat { BackgroundColor = Color.Transparent }; itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Vertical, 2); itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); @@ -489,9 +489,9 @@ public StyleNano(IResourceCache resCache) : base(resCache) sliderForeBox.SetPatchMargin(StyleBox.Margin.All, 12); sliderGrabBox.SetPatchMargin(StyleBox.Margin.All, 12); - var sliderFillGreen = new StyleBoxTexture(sliderFillBox) {Modulate = Color.LimeGreen}; - var sliderFillRed = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Red}; - var sliderFillBlue = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Blue}; + var sliderFillGreen = new StyleBoxTexture(sliderFillBox) { Modulate = Color.LimeGreen }; + var sliderFillRed = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Red }; + var sliderFillBlue = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Blue }; var sliderFillWhite = new StyleBoxTexture(sliderFillBox) { Modulate = Color.White }; var boxFont13 = resCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13); @@ -1468,6 +1468,25 @@ public StyleNano(IResourceCache resCache) : base(resCache) Element