diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index cb9a5e9853..38bcbe940d 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -275,6 +275,7 @@ namespace Content.Client.Entry "BatteryCharger", "SpawnItemsOnUse", "AmbientOnPowered", + "TabletopGame" }; } } diff --git a/Content.Client/Tabletop/Components/TabletopDraggableComponent.cs b/Content.Client/Tabletop/Components/TabletopDraggableComponent.cs new file mode 100644 index 0000000000..7b11c0d5e4 --- /dev/null +++ b/Content.Client/Tabletop/Components/TabletopDraggableComponent.cs @@ -0,0 +1,16 @@ +using Content.Shared.Tabletop.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Network; +using Robust.Shared.ViewVariables; + +namespace Content.Client.Tabletop.Components +{ + [RegisterComponent] + [ComponentReference(typeof(SharedTabletopDraggableComponent))] + public class TabletopDraggableComponent : SharedTabletopDraggableComponent + { + // The player dragging the piece + [ViewVariables] + public NetUserId? DraggingPlayer; + } +} diff --git a/Content.Client/Tabletop/TabletopSystem.cs b/Content.Client/Tabletop/TabletopSystem.cs new file mode 100644 index 0000000000..bbd42de706 --- /dev/null +++ b/Content.Client/Tabletop/TabletopSystem.cs @@ -0,0 +1,280 @@ +using Content.Client.Tabletop.Components; +using Content.Client.Tabletop.UI; +using Content.Client.Viewport; +using Content.Shared.Tabletop; +using Content.Shared.Tabletop.Events; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Input; +using Robust.Shared.Input.Binding; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using DrawDepth = Content.Shared.DrawDepth.DrawDepth; + +namespace Content.Client.Tabletop +{ + [UsedImplicitly] + public class TabletopSystem : SharedTabletopSystem + { + [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IUserInterfaceManager _uiManger = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + // Time in seconds to wait until sending the location of a dragged entity to the server again + private const float Delay = 1f / 10; // 10 Hz + + private float _timePassed; // Time passed since last update sent to the server. + private IEntity? _draggedEntity; // Entity being dragged + private ScalingViewport? _viewport; // Viewport currently being used + private SS14Window? _window; // Current open tabletop window (only allow one at a time) + private IEntity? _table; // The table entity of the currently open game session + + public override void Initialize() + { + CommandBinds.Builder + .Bind(EngineKeyFunctions.Use, new PointerInputCmdHandler(OnUse, false)) + .Register(); + + SubscribeNetworkEvent(OnTabletopPlay); + SubscribeLocalEvent(HandleComponentState); + } + + public override void Update(float frameTime) + { + // If there is no player entity, return + if (_playerManager.LocalPlayer is not { ControlledEntity: { } playerEntity }) return; + + if (StunnedOrNoHands(playerEntity)) + { + StopDragging(); + } + + if (!CanSeeTable(playerEntity, _table)) + { + StopDragging(); + _window?.Close(); + return; + } + + // If no entity is being dragged or no viewport is clicked, return + if (_draggedEntity == null || _viewport == null) return; + + // Make sure the dragged entity has a draggable component + if (!_draggedEntity.TryGetComponent(out var draggableComponent)) return; + + // If the dragged entity has another dragging player, drop the item + // This should happen if the local player is dragging an item, and another player grabs it out of their hand + if (draggableComponent.DraggingPlayer != null && + draggableComponent.DraggingPlayer != _playerManager.LocalPlayer?.Session.UserId) + { + StopDragging(false); + return; + } + + // Map mouse position to EntityCoordinates + var coords = _viewport.ScreenToMap(_inputManager.MouseScreenPosition.Position); + + // Clamp coordinates to viewport + var clampedCoords = ClampPositionToViewport(coords, _viewport); + if (clampedCoords.Equals(MapCoordinates.Nullspace)) return; + + // Move the entity locally every update + _draggedEntity.Transform.WorldPosition = clampedCoords.Position; + + // Increment total time passed + _timePassed += frameTime; + + // Only send new position to server when Delay is reached + if (_timePassed >= Delay && _table != null) + { + RaiseNetworkEvent(new TabletopMoveEvent(_draggedEntity.Uid, clampedCoords, _table.Uid)); + _timePassed -= Delay; + } + } + + #region Event handlers + + /// + /// Runs when the player presses the "Play Game" verb on a tabletop game. + /// Opens a viewport where they can then play the game. + /// + private void OnTabletopPlay(TabletopPlayEvent msg) + { + // Close the currently opened window, if it exists + _window?.Close(); + + _table = EntityManager.GetEntity(msg.TableUid); + + // Get the camera entity that the server has created for us + var camera = EntityManager.GetEntity(msg.CameraUid); + + if (!ComponentManager.TryGetComponent(camera.Uid, out var eyeComponent)) + { + // If there is no eye, print error and do not open any window + Logger.Error("Camera entity does not have eye component!"); + return; + } + + // Create a window to contain the viewport + _window = new TabletopWindow(eyeComponent.Eye, (msg.Size.X, msg.Size.Y)) + { + MinWidth = 500, + MinHeight = 436, + Title = msg.Title + }; + + _window.OnClose += OnWindowClose; + } + + private void HandleComponentState(EntityUid uid, TabletopDraggableComponent component, ref ComponentHandleState args) + { + if (args.Current is not TabletopDraggableComponentState state) return; + + component.DraggingPlayer = state.DraggingPlayer; + } + + private void OnWindowClose() + { + if (_table != null) + { + RaiseNetworkEvent(new TabletopStopPlayingEvent(_table.Uid)); + } + + StopDragging(); + _window = null; + } + + private bool OnUse(in PointerInputCmdHandler.PointerInputCmdArgs args) + { + return args.State switch + { + BoundKeyState.Down => OnMouseDown(args), + BoundKeyState.Up => OnMouseUp(args), + _ => false + }; + } + + private bool OnMouseDown(in PointerInputCmdHandler.PointerInputCmdArgs args) + { + // Return if no player entity + if (_playerManager.LocalPlayer is not { ControlledEntity: { } playerEntity }) return false; + + // Return if can not see table or stunned/no hands + if (!CanSeeTable(playerEntity, _table) || StunnedOrNoHands(playerEntity)) + { + return false; + } + + // Set the entity being dragged and the viewport under the mouse + if (!EntityManager.TryGetEntity(args.EntityUid, out var draggedEntity)) + { + return false; + } + + // Make sure that entity can be dragged + if (!ComponentManager.HasComponent(draggedEntity.Uid)) + { + return false; + } + + // Try to get the viewport under the cursor + if (_uiManger.MouseGetControl(args.ScreenCoordinates) as ScalingViewport is not { } viewport) + { + return false; + } + + StartDragging(draggedEntity, viewport); + return true; + } + + private bool OnMouseUp(in PointerInputCmdHandler.PointerInputCmdArgs args) + { + StopDragging(); + return false; + } + + #endregion + + #region Utility + + /// + /// Start dragging an entity in a specific viewport. + /// + /// The entity that we start dragging. + /// The viewport in which we are dragging. + private void StartDragging(IEntity draggedEntity, ScalingViewport viewport) + { + RaiseNetworkEvent(new TabletopDraggingPlayerChangedEvent(draggedEntity.Uid, _playerManager.LocalPlayer?.UserId)); + + if (draggedEntity.TryGetComponent(out var appearance)) + { + appearance.SetData(TabletopItemVisuals.Scale, new Vector2(1.25f, 1.25f)); + appearance.SetData(TabletopItemVisuals.DrawDepth, (int) DrawDepth.Items + 1); + } + + _draggedEntity = draggedEntity; + _viewport = viewport; + } + + /// + /// Stop dragging the entity. + /// + /// Whether to tell other clients that we stopped dragging. + private void StopDragging(bool broadcast = true) + { + // Set the dragging player on the component to noone + if (broadcast && _draggedEntity != null && _draggedEntity.HasComponent()) + { + RaiseNetworkEvent(new TabletopDraggingPlayerChangedEvent(_draggedEntity.Uid, null)); + } + + _draggedEntity = null; + _viewport = null; + } + + /// + /// Clamps coordinates within a viewport. ONLY WORKS FOR 90 DEGREE ROTATIONS! + /// + /// The coordinates to be clamped. + /// The viewport to clamp the coordinates to. + /// Coordinates clamped to the viewport. + private static MapCoordinates ClampPositionToViewport(MapCoordinates coordinates, ScalingViewport viewport) + { + if (coordinates == MapCoordinates.Nullspace) return MapCoordinates.Nullspace; + + var eye = viewport.Eye; + if (eye == null) return MapCoordinates.Nullspace; + + var size = (Vector2) viewport.ViewportSize / EyeManager.PixelsPerMeter; // Convert to tiles instead of pixels + var eyePosition = eye.Position.Position; + var eyeRotation = eye.Rotation; + var eyeScale = eye.Scale; + + var min = (eyePosition - size / 2) / eyeScale; + var max = (eyePosition + size / 2) / eyeScale; + + // If 90/270 degrees rotated, flip X and Y + if (MathHelper.CloseTo(eyeRotation.Degrees % 180d, 90d) || MathHelper.CloseTo(eyeRotation.Degrees % 180d, -90d)) + { + (min.Y, min.X) = (min.X, min.Y); + (max.Y, max.X) = (max.X, max.Y); + } + + var clampedPosition = Vector2.Clamp(coordinates.Position, min, max); + + // Use the eye's map ID, we don't want anything moving to a different map! + return new MapCoordinates(clampedPosition, eye.Position.MapId); + } + + #endregion + } +} diff --git a/Content.Client/Tabletop/UI/TabletopWindow.xaml b/Content.Client/Tabletop/UI/TabletopWindow.xaml new file mode 100644 index 0000000000..1e247b6a2b --- /dev/null +++ b/Content.Client/Tabletop/UI/TabletopWindow.xaml @@ -0,0 +1,11 @@ + + +