From 973926fc9aba8158825c527479c15aec68f69130 Mon Sep 17 00:00:00 2001 From: Visne <39844191+Visne@users.noreply.github.com> Date: Mon, 13 Sep 2021 11:58:44 +0200 Subject: [PATCH] Add chess (and mostly just tabletop backend stuff) (#4429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add draggable tabletop component * Use EntityCoordinates instead * Don't send coordinates every frame * Add chessboard + verb WIP * Add documentation, verb networking works now * Work so far Need PVS refactor before being able to continue Current code is broken * viewsubscriber magic * yes * Fix map creation * Add chess pieces, attempt prediction * Add chess sprites and yml * Clamping + other stuff * fix * stuff * StopDragging() StartDragging() * add piece grabbing * Refactor dragging player to seperate event * 🤣 Who did this 🤣💯👌 * 📮 sussy 📮 * Update chessboard sprite, scale piece while dragging * yes * ye * y * Close tabletop window when player dies * Make interaction check more sane * Fix funny behaviour when stunned * Add icon * Fix rsi * Make time passed check more accurate Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Use EyeManager.PixelsPerMeter Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Add missing import * Move viewport properties to XAML * Make shared system and component abstract * Use built in EntityManager * Use RaiseNetworkEvent instead of SendSystemNetworkMessage * Cache ViewSubscriberSystem * Move unnecessary code to prototype * Delete map on component shutdown instead of round restart * Make documentation match rest of codebase * Use ComponentManager instead of TryGetComponent * Use TryGetComponent instead of GetComponent * Add nullspace check to ClampPositionToViewport() * Set world pos instead of local pos * Improve server side verification * Use visualizer * Add netsync: false to sprites using visualizer * Close window when chessboard is picked up * Update to master * Fix bug when opening window while another is opened * Use ComponentManager * Use TryGetValue Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: metalgearsloth --- Content.Client/Entry/IgnoredComponents.cs | 1 + .../Components/TabletopDraggableComponent.cs | 16 + Content.Client/Tabletop/TabletopSystem.cs | 280 ++++++++++++++++++ .../Tabletop/UI/TabletopWindow.xaml | 11 + .../Tabletop/UI/TabletopWindow.xaml.cs | 38 +++ .../Visualizers/TabletopItemVisualizer.cs | 31 ++ .../Interaction/InteractionSystem.cs | 8 +- .../Components/TabletopDraggableComponent.cs | 28 ++ .../Components/TabletopGameComponent.cs | 40 +++ Content.Server/Tabletop/TabletopSession.cs | 55 ++++ .../Tabletop/TabletopSystem.Chess.cs | 73 +++++ Content.Server/Tabletop/TabletopSystem.cs | 195 ++++++++++++ .../SharedTabletopDraggableComponent.cs | 14 + .../TabletopDraggingPlayerChangedEvent.cs | 31 ++ .../Tabletop/Events/TabletopMoveEvent.cs | 38 +++ .../Tabletop/Events/TabletopPlayEvent.cs | 27 ++ .../Events/TabletopStopPlayingEvent.cs | 23 ++ .../Tabletop/SharedTabletopSystem.cs | 62 ++++ .../Tabletop/TabletopItemVisuals.cs | 12 + Resources/Locale/en-US/tabletop/tabletop.ftl | 5 + .../Entities/Objects/Fun/Tabletop/chess.yml | 147 +++++++++ .../Textures/Interface/VerbIcons/die.svg | 79 +++++ .../Interface/VerbIcons/die.svg.192dpi.png | Bin 0 -> 755 bytes .../VerbIcons/die.svg.192dpi.png.yml | 2 + .../Tabletop/chess_pieces.rsi/b_bishop.png | Bin 0 -> 432 bytes .../Fun/Tabletop/chess_pieces.rsi/b_king.png | Bin 0 -> 435 bytes .../Tabletop/chess_pieces.rsi/b_knight.png | Bin 0 -> 466 bytes .../Fun/Tabletop/chess_pieces.rsi/b_pawn.png | Bin 0 -> 442 bytes .../Fun/Tabletop/chess_pieces.rsi/b_queen.png | Bin 0 -> 426 bytes .../Fun/Tabletop/chess_pieces.rsi/b_rook.png | Bin 0 -> 405 bytes .../Fun/Tabletop/chess_pieces.rsi/meta.json | 47 +++ .../Tabletop/chess_pieces.rsi/w_bishop.png | Bin 0 -> 432 bytes .../Fun/Tabletop/chess_pieces.rsi/w_king.png | Bin 0 -> 435 bytes .../Tabletop/chess_pieces.rsi/w_knight.png | Bin 0 -> 466 bytes .../Fun/Tabletop/chess_pieces.rsi/w_pawn.png | Bin 0 -> 442 bytes .../Fun/Tabletop/chess_pieces.rsi/w_queen.png | Bin 0 -> 426 bytes .../Fun/Tabletop/chess_pieces.rsi/w_rook.png | Bin 0 -> 405 bytes .../Tabletop/chessboard.rsi/chessboard.png | Bin 0 -> 285 bytes .../Fun/Tabletop/chessboard.rsi/meta.json | 14 + .../chessboard_tabletop.png | Bin 0 -> 1865 bytes .../chessboard_tabletop.rsi/meta.json | 14 + 41 files changed, 1287 insertions(+), 4 deletions(-) create mode 100644 Content.Client/Tabletop/Components/TabletopDraggableComponent.cs create mode 100644 Content.Client/Tabletop/TabletopSystem.cs create mode 100644 Content.Client/Tabletop/UI/TabletopWindow.xaml create mode 100644 Content.Client/Tabletop/UI/TabletopWindow.xaml.cs create mode 100644 Content.Client/Tabletop/Visualizers/TabletopItemVisualizer.cs create mode 100644 Content.Server/Tabletop/Components/TabletopDraggableComponent.cs create mode 100644 Content.Server/Tabletop/Components/TabletopGameComponent.cs create mode 100644 Content.Server/Tabletop/TabletopSession.cs create mode 100644 Content.Server/Tabletop/TabletopSystem.Chess.cs create mode 100644 Content.Server/Tabletop/TabletopSystem.cs create mode 100644 Content.Shared/Tabletop/Components/SharedTabletopDraggableComponent.cs create mode 100644 Content.Shared/Tabletop/Events/TabletopDraggingPlayerChangedEvent.cs create mode 100644 Content.Shared/Tabletop/Events/TabletopMoveEvent.cs create mode 100644 Content.Shared/Tabletop/Events/TabletopPlayEvent.cs create mode 100644 Content.Shared/Tabletop/Events/TabletopStopPlayingEvent.cs create mode 100644 Content.Shared/Tabletop/SharedTabletopSystem.cs create mode 100644 Content.Shared/Tabletop/TabletopItemVisuals.cs create mode 100644 Resources/Locale/en-US/tabletop/tabletop.ftl create mode 100644 Resources/Prototypes/Entities/Objects/Fun/Tabletop/chess.yml create mode 100644 Resources/Textures/Interface/VerbIcons/die.svg create mode 100644 Resources/Textures/Interface/VerbIcons/die.svg.192dpi.png create mode 100644 Resources/Textures/Interface/VerbIcons/die.svg.192dpi.png.yml create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/b_bishop.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/b_king.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/b_knight.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/b_pawn.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/b_queen.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/b_rook.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/meta.json create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/w_bishop.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/w_king.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/w_knight.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/w_pawn.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/w_queen.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chess_pieces.rsi/w_rook.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chessboard.rsi/chessboard.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chessboard.rsi/meta.json create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chessboard_tabletop.rsi/chessboard_tabletop.png create mode 100644 Resources/Textures/Objects/Fun/Tabletop/chessboard_tabletop.rsi/meta.json 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 @@ + + +