Add chess (and mostly just tabletop backend stuff) (#4429)
* 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 <comedian_vs_clown@hotmail.com>
@@ -275,6 +275,7 @@ namespace Content.Client.Entry
|
|||||||
"BatteryCharger",
|
"BatteryCharger",
|
||||||
"SpawnItemsOnUse",
|
"SpawnItemsOnUse",
|
||||||
"AmbientOnPowered",
|
"AmbientOnPowered",
|
||||||
|
"TabletopGame"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
280
Content.Client/Tabletop/TabletopSystem.cs
Normal file
@@ -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<TabletopSystem>();
|
||||||
|
|
||||||
|
SubscribeNetworkEvent<TabletopPlayEvent>(OnTabletopPlay);
|
||||||
|
SubscribeLocalEvent<TabletopDraggableComponent, ComponentHandleState>(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<TabletopDraggableComponent>(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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs when the player presses the "Play Game" verb on a tabletop game.
|
||||||
|
/// Opens a viewport where they can then play the game.
|
||||||
|
/// </summary>
|
||||||
|
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<EyeComponent>(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<TabletopDraggableComponent>(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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start dragging an entity in a specific viewport.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="draggedEntity">The entity that we start dragging.</param>
|
||||||
|
/// <param name="viewport">The viewport in which we are dragging.</param>
|
||||||
|
private void StartDragging(IEntity draggedEntity, ScalingViewport viewport)
|
||||||
|
{
|
||||||
|
RaiseNetworkEvent(new TabletopDraggingPlayerChangedEvent(draggedEntity.Uid, _playerManager.LocalPlayer?.UserId));
|
||||||
|
|
||||||
|
if (draggedEntity.TryGetComponent<AppearanceComponent>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop dragging the entity.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="broadcast">Whether to tell other clients that we stopped dragging.</param>
|
||||||
|
private void StopDragging(bool broadcast = true)
|
||||||
|
{
|
||||||
|
// Set the dragging player on the component to noone
|
||||||
|
if (broadcast && _draggedEntity != null && _draggedEntity.HasComponent<TabletopDraggableComponent>())
|
||||||
|
{
|
||||||
|
RaiseNetworkEvent(new TabletopDraggingPlayerChangedEvent(_draggedEntity.Uid, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
_draggedEntity = null;
|
||||||
|
_viewport = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clamps coordinates within a viewport. ONLY WORKS FOR 90 DEGREE ROTATIONS!
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="coordinates">The coordinates to be clamped.</param>
|
||||||
|
/// <param name="viewport">The viewport to clamp the coordinates to.</param>
|
||||||
|
/// <returns>Coordinates clamped to the viewport.</returns>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Content.Client/Tabletop/UI/TabletopWindow.xaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<SS14Window xmlns="https://spacestation14.io"
|
||||||
|
xmlns:viewport="clr-namespace:Content.Client.Viewport">
|
||||||
|
<viewport:ScalingViewport Name="ScalingVp" MouseFilter="Stop" RenderScaleMode="CeilInt">
|
||||||
|
<Button Name="FlipButton"
|
||||||
|
Text="{ Loc 'tabletop-chess-flip' }"
|
||||||
|
MinSize="60 30"
|
||||||
|
MaxSize="60 30"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Top" />
|
||||||
|
</viewport:ScalingViewport>
|
||||||
|
</SS14Window>
|
||||||
38
Content.Client/Tabletop/UI/TabletopWindow.xaml.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
|
||||||
|
namespace Content.Client.Tabletop.UI
|
||||||
|
{
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public partial class TabletopWindow : SS14Window
|
||||||
|
{
|
||||||
|
public TabletopWindow(IEye? eye, Vector2i size)
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
ScalingVp.Eye = eye;
|
||||||
|
ScalingVp.ViewportSize = size;
|
||||||
|
|
||||||
|
FlipButton.OnButtonUp += Flip;
|
||||||
|
OpenCentered();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Flip(BaseButton.ButtonEventArgs args)
|
||||||
|
{
|
||||||
|
// Flip the view 180 degrees
|
||||||
|
if (ScalingVp.Eye is { } eye)
|
||||||
|
{
|
||||||
|
eye.Rotation = eye.Rotation.Opposite();
|
||||||
|
|
||||||
|
// Flip alignmento of the button
|
||||||
|
FlipButton.HorizontalAlignment = FlipButton.HorizontalAlignment == HAlignment.Right
|
||||||
|
? HAlignment.Left
|
||||||
|
: HAlignment.Right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Content.Shared.Tabletop;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
|
||||||
|
namespace Content.Client.Tabletop.Visualizers
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public class TabletopItemVisualizer : AppearanceVisualizer
|
||||||
|
{
|
||||||
|
public override void OnChangeData(AppearanceComponent appearance)
|
||||||
|
{
|
||||||
|
if (!appearance.Owner.TryGetComponent<ISpriteComponent>(out var sprite))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: maybe this can work more nicely, by maybe only having to set the item to "being dragged", and have
|
||||||
|
// the appearance handle the rest
|
||||||
|
if (appearance.TryGetData<Vector2>(TabletopItemVisuals.Scale, out var scale))
|
||||||
|
{
|
||||||
|
sprite.Scale = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appearance.TryGetData<int>(TabletopItemVisuals.DrawDepth, out var drawDepth))
|
||||||
|
{
|
||||||
|
sprite.DrawDepth = drawDepth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -347,7 +347,7 @@ namespace Content.Server.Interaction
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify user has a hand, and find what object he is currently holding in his active hand
|
// Verify user has a hand, and find what object they are currently holding in their active hand
|
||||||
if (!user.TryGetComponent<IHandsComponent>(out var hands))
|
if (!user.TryGetComponent<IHandsComponent>(out var hands))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -393,11 +393,11 @@ namespace Content.Server.Interaction
|
|||||||
|
|
||||||
private bool ValidateInteractAndFace(IEntity user, EntityCoordinates coordinates)
|
private bool ValidateInteractAndFace(IEntity user, EntityCoordinates coordinates)
|
||||||
{
|
{
|
||||||
// Verify user is on the same map as the entity he clicked on
|
// Verify user is on the same map as the entity they clicked on
|
||||||
if (coordinates.GetMapId(_entityManager) != user.Transform.MapID)
|
if (coordinates.GetMapId(_entityManager) != user.Transform.MapID)
|
||||||
{
|
{
|
||||||
Logger.WarningS("system.interaction",
|
Logger.WarningS("system.interaction",
|
||||||
$"User entity named {user.Name} clicked on a map he isn't located on");
|
$"User entity named {user.Name} clicked on a map they aren't located on");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,7 +873,7 @@ namespace Content.Server.Interaction
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify user has a hand, and find what object he is currently holding in his active hand
|
// Verify user has a hand, and find what object they are currently holding in their active hand
|
||||||
if (user.TryGetComponent<IHandsComponent>(out var hands))
|
if (user.TryGetComponent<IHandsComponent>(out var hands))
|
||||||
{
|
{
|
||||||
var item = hands.GetActiveHand?.Owner;
|
var item = hands.GetActiveHand?.Owner;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Content.Shared.Tabletop.Components;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Players;
|
||||||
|
using Robust.Shared.ViewVariables;
|
||||||
|
using static Content.Shared.Tabletop.SharedTabletopSystem;
|
||||||
|
|
||||||
|
namespace Content.Server.Tabletop.Components
|
||||||
|
{
|
||||||
|
[RegisterComponent]
|
||||||
|
[ComponentReference(typeof(SharedTabletopDraggableComponent))]
|
||||||
|
public class TabletopDraggableComponent : SharedTabletopDraggableComponent
|
||||||
|
{
|
||||||
|
private NetUserId? _draggingPlayer;
|
||||||
|
|
||||||
|
// The player dragging the piece
|
||||||
|
[ViewVariables]
|
||||||
|
public NetUserId? DraggingPlayer
|
||||||
|
{
|
||||||
|
get => _draggingPlayer;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_draggingPlayer = value;
|
||||||
|
Dirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Content.Server/Tabletop/Components/TabletopGameComponent.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Content.Shared.ActionBlocker;
|
||||||
|
using Content.Shared.Verbs;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
|
||||||
|
namespace Content.Server.Tabletop.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A component that makes an object playable as a tabletop game.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public class TabletopGameComponent : Component
|
||||||
|
{
|
||||||
|
public override string Name => "TabletopGame";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A verb that allows the player to start playing a tabletop game.
|
||||||
|
/// </summary>
|
||||||
|
[Verb]
|
||||||
|
public class PlayVerb : Verb<TabletopGameComponent>
|
||||||
|
{
|
||||||
|
protected override void GetData(IEntity user, TabletopGameComponent component, VerbData data)
|
||||||
|
{
|
||||||
|
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
|
||||||
|
{
|
||||||
|
data.Visibility = VerbVisibility.Invisible;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Text = Loc.GetString("tabletop-verb-play-game");
|
||||||
|
data.IconTexture = "/Textures/Interface/VerbIcons/die.svg.192dpi.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Activate(IEntity user, TabletopGameComponent component)
|
||||||
|
{
|
||||||
|
EntitySystem.Get<TabletopSystem>().OpenTable(user, component.Owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
Content.Server/Tabletop/TabletopSession.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.Tabletop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A struct for storing data about a running tabletop game.
|
||||||
|
/// </summary>
|
||||||
|
public struct TabletopSession
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The map ID associated with this tabletop game session.
|
||||||
|
/// </summary>
|
||||||
|
public MapId MapId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The set of players currently playing this tabletop game.
|
||||||
|
/// </summary>
|
||||||
|
private readonly HashSet<IPlayerSession> _currentPlayers;
|
||||||
|
|
||||||
|
/// <param name="mapId">The map ID associated with this tabletop game.</param>
|
||||||
|
public TabletopSession(MapId mapId)
|
||||||
|
{
|
||||||
|
MapId = mapId;
|
||||||
|
_currentPlayers = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the given player is currently playing this tabletop game.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPlaying(IPlayerSession playerSession)
|
||||||
|
{
|
||||||
|
return _currentPlayers.Contains(playerSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store that this player has started playing this tabletop game. If the player was already playing, nothing
|
||||||
|
/// happens.
|
||||||
|
/// </summary>
|
||||||
|
public void StartPlaying(IPlayerSession playerSession)
|
||||||
|
{
|
||||||
|
_currentPlayers.Add(playerSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store that this player has stopped playing this tabletop game. If the player was not playing, nothing
|
||||||
|
/// happens.
|
||||||
|
/// </summary>
|
||||||
|
public void StopPlaying(IPlayerSession playerSession)
|
||||||
|
{
|
||||||
|
_currentPlayers.Remove(playerSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
Content.Server/Tabletop/TabletopSystem.Chess.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.Tabletop
|
||||||
|
{
|
||||||
|
public partial class TabletopSystem
|
||||||
|
{
|
||||||
|
private void SetupChessBoard(MapId mapId)
|
||||||
|
{
|
||||||
|
var chessboard = EntityManager.SpawnEntity("ChessBoardTabletop", new MapCoordinates(-1, 0, mapId));
|
||||||
|
chessboard.Transform.Anchored = true;
|
||||||
|
|
||||||
|
SpawnPieces(new MapCoordinates(-4.5f, 3.5f, mapId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnPieces(MapCoordinates topLeft, float separation = 1f)
|
||||||
|
{
|
||||||
|
var (mapId, x, y) = topLeft;
|
||||||
|
|
||||||
|
// Spawn all black pieces
|
||||||
|
SpawnPiecesRow("Black", topLeft, separation);
|
||||||
|
SpawnPawns("Black", new MapCoordinates(x, y - separation, mapId) , separation);
|
||||||
|
|
||||||
|
// Spawn all white pieces
|
||||||
|
SpawnPawns("White", new MapCoordinates(x, y - 6 * separation, mapId) , separation);
|
||||||
|
SpawnPiecesRow("White", new MapCoordinates(x, y - 7 * separation, mapId), separation);
|
||||||
|
|
||||||
|
// Extra queens
|
||||||
|
EntityManager.SpawnEntity("BlackQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 3 * separation, mapId));
|
||||||
|
EntityManager.SpawnEntity("WhiteQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 4 * separation, mapId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: refactor to load FEN instead
|
||||||
|
private void SpawnPiecesRow(string color, MapCoordinates left, float separation = 1f)
|
||||||
|
{
|
||||||
|
const string piecesRow = "rnbqkbnr";
|
||||||
|
|
||||||
|
var (mapId, x, y) = left;
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
switch (piecesRow[i])
|
||||||
|
{
|
||||||
|
case 'r':
|
||||||
|
EntityManager.SpawnEntity(color + "Rook", new MapCoordinates(x + i * separation, y, mapId));
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
EntityManager.SpawnEntity(color + "Knight", new MapCoordinates(x + i * separation, y, mapId));
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
EntityManager.SpawnEntity(color + "Bishop", new MapCoordinates(x + i * separation, y, mapId));
|
||||||
|
break;
|
||||||
|
case 'q':
|
||||||
|
EntityManager.SpawnEntity(color + "Queen", new MapCoordinates(x + i * separation, y, mapId));
|
||||||
|
break;
|
||||||
|
case 'k':
|
||||||
|
EntityManager.SpawnEntity(color + "King", new MapCoordinates(x + i * separation, y, mapId));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: refactor to load FEN instead
|
||||||
|
private void SpawnPawns(string color, MapCoordinates left, float separation = 1f)
|
||||||
|
{
|
||||||
|
var (mapId, x, y) = left;
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
EntityManager.SpawnEntity(color + "Pawn", new MapCoordinates(x + i * separation, y, mapId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
195
Content.Server/Tabletop/TabletopSystem.cs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.Tabletop.Components;
|
||||||
|
using Content.Shared.Tabletop;
|
||||||
|
using Content.Shared.Tabletop.Events;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
|
||||||
|
|
||||||
|
namespace Content.Server.Tabletop
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public partial class TabletopSystem : SharedTabletopSystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly ViewSubscriberSystem _viewSubscriberSystem = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All tabletop games currently in progress. Sessions are associated with an entity UID, which acts as a
|
||||||
|
/// key, such that an entity can only have one running tabletop game session.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<EntityUid, TabletopSession> _gameSessions = new();
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
SubscribeNetworkEvent<TabletopMoveEvent>(OnTabletopMove);
|
||||||
|
SubscribeNetworkEvent<TabletopDraggingPlayerChangedEvent>(OnDraggingPlayerChanged);
|
||||||
|
SubscribeNetworkEvent<TabletopStopPlayingEvent>(OnStopPlaying);
|
||||||
|
SubscribeLocalEvent<TabletopGameComponent, ComponentShutdown>(OnGameShutdown);
|
||||||
|
SubscribeLocalEvent<TabletopDraggableComponent, ComponentGetState>(GetCompState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetCompState(EntityUid uid, TabletopDraggableComponent component, ref ComponentGetState args)
|
||||||
|
{
|
||||||
|
args.State = new TabletopDraggableComponentState(component.DraggingPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For a specific user, create a table if it does not exist yet and let the user open a UI window to play it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user entity for which to open the window.</param>
|
||||||
|
/// <param name="table">The entity with which the tabletop game session will be associated.</param>
|
||||||
|
public void OpenTable(IEntity user, IEntity table)
|
||||||
|
{
|
||||||
|
if (user.PlayerSession() is not { } playerSession) return;
|
||||||
|
|
||||||
|
// Make sure we have a session, and add the player to it
|
||||||
|
var session = EnsureSession(table.Uid);
|
||||||
|
session.StartPlaying(playerSession);
|
||||||
|
|
||||||
|
// Create a camera for the user to use
|
||||||
|
// TODO: set correct coordinates, depending on the piece the game was started from
|
||||||
|
IEntity camera = CreateCamera(user, new MapCoordinates(0, 0, session.MapId));
|
||||||
|
|
||||||
|
// Tell the client to open a viewport for the tabletop game
|
||||||
|
// TODO: use actual title/size from prototype, for now we assume its chess
|
||||||
|
RaiseNetworkEvent(new TabletopPlayEvent(table.Uid, camera.Uid, "Chess", (274 + 64, 274)), playerSession.ConnectedClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a session associated to this entity UID, if it does not already exist, and return it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uid">The entity UID to ensure a session for.</param>
|
||||||
|
/// <returns>The created/stored tabletop game session.</returns>
|
||||||
|
private TabletopSession EnsureSession(EntityUid uid)
|
||||||
|
{
|
||||||
|
// We already have a session, return it
|
||||||
|
// TODO: if tables are connected, treat them as a single entity
|
||||||
|
if (_gameSessions.ContainsKey(uid))
|
||||||
|
{
|
||||||
|
return _gameSessions[uid];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session does not exist for this entity yet, create a map and create a session
|
||||||
|
var mapId = _mapManager.CreateMap();
|
||||||
|
|
||||||
|
// Tabletop maps do not need lighting, turn it off
|
||||||
|
var mapComponent = _mapManager.GetMapEntity(mapId).GetComponent<IMapComponent>();
|
||||||
|
mapComponent.LightingEnabled = false;
|
||||||
|
mapComponent.Dirty();
|
||||||
|
|
||||||
|
_gameSessions.Add(uid, new TabletopSession(mapId));
|
||||||
|
var session = _gameSessions[uid];
|
||||||
|
|
||||||
|
// Since this is the first time opening this session, set up the game
|
||||||
|
// TODO: don't assume we're playing chess
|
||||||
|
SetupChessBoard(session.MapId);
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Event handlers
|
||||||
|
|
||||||
|
// Move an entity which is dragged by the user, but check if they are allowed to do so and to these coordinates
|
||||||
|
private void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.SenderSession as IPlayerSession is not { AttachedEntity: { } playerEntity } playerSession) return;
|
||||||
|
|
||||||
|
// Check if player is actually playing at this table
|
||||||
|
if (!_gameSessions.TryGetValue(msg.TableUid, out var tableUid) ||
|
||||||
|
!tableUid.IsPlaying(playerSession)) return;
|
||||||
|
|
||||||
|
// Return if can not see table or stunned/no hands
|
||||||
|
if (!EntityManager.TryGetEntity(msg.TableUid, out var table)) return;
|
||||||
|
if (!CanSeeTable(playerEntity, table) || StunnedOrNoHands(playerEntity)) return;
|
||||||
|
|
||||||
|
// Check if moved entity exists and has tabletop draggable component
|
||||||
|
if (!EntityManager.TryGetEntity(msg.MovedEntityUid, out var movedEntity)) return;
|
||||||
|
if (!ComponentManager.HasComponent<TabletopDraggableComponent>(movedEntity.Uid)) return;
|
||||||
|
|
||||||
|
// TODO: some permission system, disallow movement if you're not permitted to move the item
|
||||||
|
|
||||||
|
// Move the entity and dirty it (we use the map ID from the entity so noone can try to be funny and move the item to another map)
|
||||||
|
var transform = ComponentManager.GetComponent<ITransformComponent>(movedEntity.Uid);
|
||||||
|
var entityCoordinates = new EntityCoordinates(_mapManager.GetMapEntityId(transform.MapID), msg.Coordinates.Position);
|
||||||
|
transform.Coordinates = entityCoordinates;
|
||||||
|
movedEntity.Dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDraggingPlayerChanged(TabletopDraggingPlayerChangedEvent msg)
|
||||||
|
{
|
||||||
|
var draggedEntity = EntityManager.GetEntity(msg.DraggedEntityUid);
|
||||||
|
|
||||||
|
if (!draggedEntity.TryGetComponent<TabletopDraggableComponent>(out var draggableComponent)) return;
|
||||||
|
|
||||||
|
draggableComponent.DraggingPlayer = msg.DraggingPlayer;
|
||||||
|
|
||||||
|
if (!draggedEntity.TryGetComponent<AppearanceComponent>(out var appearance)) return;
|
||||||
|
|
||||||
|
if (draggableComponent.DraggingPlayer != null)
|
||||||
|
{
|
||||||
|
appearance.SetData(TabletopItemVisuals.Scale, new Vector2(1.25f, 1.25f));
|
||||||
|
appearance.SetData(TabletopItemVisuals.DrawDepth, (int) DrawDepth.Items + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
appearance.SetData(TabletopItemVisuals.Scale, Vector2.One);
|
||||||
|
appearance.SetData(TabletopItemVisuals.DrawDepth, (int) DrawDepth.Items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStopPlaying(TabletopStopPlayingEvent msg, EntitySessionEventArgs args)
|
||||||
|
{
|
||||||
|
if (_gameSessions.ContainsKey(msg.TableUid) && args.SenderSession as IPlayerSession is { } playerSession)
|
||||||
|
{
|
||||||
|
_gameSessions[msg.TableUid].StopPlaying(playerSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: needs to be refactored such that the corresponding entity on the table gets removed, instead of the whole map
|
||||||
|
private void OnGameShutdown(EntityUid uid, TabletopGameComponent component, ComponentShutdown args)
|
||||||
|
{
|
||||||
|
if (!_gameSessions.ContainsKey(uid)) return;
|
||||||
|
|
||||||
|
// Delete the map and remove it from the list of sessions
|
||||||
|
_mapManager.DeleteMap(_gameSessions[uid].MapId);
|
||||||
|
_gameSessions.Remove(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Utility
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a camera entity for a user to control, and add the user to the view subscribers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user entity to create this camera for and add to the view subscribers.</param>
|
||||||
|
/// <param name="coordinates">The map coordinates to spawn this camera at.</param>
|
||||||
|
// TODO: this can probably be generalized into a "CctvSystem" or whatever
|
||||||
|
private IEntity CreateCamera(IEntity user, MapCoordinates coordinates)
|
||||||
|
{
|
||||||
|
// Spawn an empty entity at the coordinates
|
||||||
|
var camera = EntityManager.SpawnEntity(null, coordinates);
|
||||||
|
|
||||||
|
// Add an eye component and disable FOV
|
||||||
|
var eyeComponent = camera.EnsureComponent<EyeComponent>();
|
||||||
|
eyeComponent.DrawFov = false;
|
||||||
|
|
||||||
|
// Add the user to the view subscribers. If there is no player session, just skip this step
|
||||||
|
if (user.PlayerSession() is { } playerSession)
|
||||||
|
{
|
||||||
|
_viewSubscriberSystem.AddViewSubscriber(camera.Uid, playerSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
return camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
|
namespace Content.Shared.Tabletop.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Allows an entity to be dragged around by the mouse. The position is updated for all player while dragging.
|
||||||
|
/// </summary>
|
||||||
|
[NetworkedComponent]
|
||||||
|
public abstract class SharedTabletopDraggableComponent : Component
|
||||||
|
{
|
||||||
|
public override string Name => "TabletopDraggable";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Tabletop.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Event to tell other clients that we are dragging this item. Necessery to handle multiple users
|
||||||
|
/// trying to move a single item at the same time.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class TabletopDraggingPlayerChangedEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The UID of the entity being dragged.
|
||||||
|
/// </summary>
|
||||||
|
public EntityUid DraggedEntityUid;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The NetUserID of the player that is now dragging the item.
|
||||||
|
/// </summary>
|
||||||
|
public NetUserId? DraggingPlayer;
|
||||||
|
|
||||||
|
public TabletopDraggingPlayerChangedEvent(EntityUid draggedEntityUid, NetUserId? draggingPlayer)
|
||||||
|
{
|
||||||
|
DraggedEntityUid = draggedEntityUid;
|
||||||
|
DraggingPlayer = draggingPlayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Content.Shared/Tabletop/Events/TabletopMoveEvent.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Shared.Tabletop.Components;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Tabletop.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An event that is sent to the server every so often by the client to tell where an entity with a
|
||||||
|
/// <see cref="SharedTabletopDraggableComponent"/> has been moved.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class TabletopMoveEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The UID of the entity being moved.
|
||||||
|
/// </summary>
|
||||||
|
public EntityUid MovedEntityUid { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The new coordinates of the entity being moved.
|
||||||
|
/// </summary>
|
||||||
|
public MapCoordinates Coordinates { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The UID of the table the entity is being moved on.
|
||||||
|
/// </summary>
|
||||||
|
public EntityUid TableUid { get; }
|
||||||
|
|
||||||
|
public TabletopMoveEvent(EntityUid movedEntityUid, MapCoordinates coordinates, EntityUid tableUid)
|
||||||
|
{
|
||||||
|
MovedEntityUid = movedEntityUid;
|
||||||
|
Coordinates = coordinates;
|
||||||
|
TableUid = tableUid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Content.Shared/Tabletop/Events/TabletopPlayEvent.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Tabletop.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An event sent by the server to the client to tell the client to open a tabletop game window.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class TabletopPlayEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public EntityUid TableUid;
|
||||||
|
public EntityUid CameraUid;
|
||||||
|
public string Title;
|
||||||
|
public Vector2i Size;
|
||||||
|
|
||||||
|
public TabletopPlayEvent(EntityUid tableUid, EntityUid cameraUid, string title, Vector2i size)
|
||||||
|
{
|
||||||
|
TableUid = tableUid;
|
||||||
|
CameraUid = cameraUid;
|
||||||
|
Title = title;
|
||||||
|
Size = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Content.Shared/Tabletop/Events/TabletopStopPlayingEvent.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Tabletop.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An event ot tell the server that we have stopped playing this tabletop game.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class TabletopStopPlayingEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The entity UID of the table associated with this tabletop game.
|
||||||
|
/// </summary>
|
||||||
|
public EntityUid TableUid;
|
||||||
|
|
||||||
|
public TabletopStopPlayingEvent(EntityUid tableUid)
|
||||||
|
{
|
||||||
|
TableUid = tableUid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
Content.Shared/Tabletop/SharedTabletopSystem.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Shared.Hands.Components;
|
||||||
|
using Content.Shared.Interaction.Helpers;
|
||||||
|
using Content.Shared.MobState.Components;
|
||||||
|
using Content.Shared.Stunnable;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Tabletop
|
||||||
|
{
|
||||||
|
public abstract class SharedTabletopSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class TabletopDraggableComponentState : ComponentState
|
||||||
|
{
|
||||||
|
public NetUserId? DraggingPlayer;
|
||||||
|
|
||||||
|
public TabletopDraggableComponentState(NetUserId? draggingPlayer)
|
||||||
|
{
|
||||||
|
DraggingPlayer = draggingPlayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Utility
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the table exists, is in range and the player is alive.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="playerEntity">The player entity to check.</param>
|
||||||
|
/// <param name="table">The table entity to check.</param>
|
||||||
|
protected static bool CanSeeTable(IEntity playerEntity, IEntity? table)
|
||||||
|
{
|
||||||
|
if (table?.Transform.Parent?.Owner is not { } parent)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parent.HasComponent<MapComponent>() && !parent.HasComponent<IMapGridComponent>())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alive = playerEntity.TryGetComponent<SharedMobStateComponent>(out var mob) && mob.IsAlive();
|
||||||
|
var inRange = playerEntity.InRangeUnobstructed(table);
|
||||||
|
|
||||||
|
return alive && inRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static bool StunnedOrNoHands(IEntity playerEntity)
|
||||||
|
{
|
||||||
|
var stunned = playerEntity.TryGetComponent<SharedStunnableComponent>(out var stun) &&
|
||||||
|
stun.Stunned;
|
||||||
|
var hasHand = playerEntity.TryGetComponent<SharedHandsComponent>(out var handsComponent) &&
|
||||||
|
handsComponent.Hands.Count > 0;
|
||||||
|
|
||||||
|
return stunned || !hasHand;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Content.Shared/Tabletop/TabletopItemVisuals.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Tabletop
|
||||||
|
{
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum TabletopItemVisuals : byte
|
||||||
|
{
|
||||||
|
Scale,
|
||||||
|
DrawDepth
|
||||||
|
}
|
||||||
|
}
|
||||||
5
Resources/Locale/en-US/tabletop/tabletop.ftl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
## TabletopGameComponent
|
||||||
|
tabletop-verb-play-game = Play Game
|
||||||
|
|
||||||
|
## Chess
|
||||||
|
tabletop-chess-flip = Flip
|
||||||
147
Resources/Prototypes/Entities/Objects/Fun/Tabletop/chess.yml
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Chessboard item (normal in game item you can hold in your hand)
|
||||||
|
- type: entity
|
||||||
|
parent: BaseItem
|
||||||
|
id: ChessBoard
|
||||||
|
name: chessboard
|
||||||
|
description: A chessboard. Pieces included!
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chessboard.rsi
|
||||||
|
state: chessboard
|
||||||
|
- type: TabletopGame
|
||||||
|
|
||||||
|
# Chessboard tabletop item (item only visible in tabletop game)
|
||||||
|
- type: entity
|
||||||
|
id: ChessBoardTabletop
|
||||||
|
name: chessboard
|
||||||
|
abstract: true
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chessboard_tabletop.rsi
|
||||||
|
state: chessboard_tabletop
|
||||||
|
noRot: false
|
||||||
|
drawdepth: FloorTiles
|
||||||
|
|
||||||
|
## Chess pieces
|
||||||
|
- type: entity
|
||||||
|
id: BaseChessPiece
|
||||||
|
parent: BaseItem
|
||||||
|
abstract: true
|
||||||
|
components:
|
||||||
|
- type: TabletopDraggable
|
||||||
|
- type: Sprite
|
||||||
|
netsync: false
|
||||||
|
noRot: true
|
||||||
|
- type: Appearance
|
||||||
|
visuals:
|
||||||
|
- type: TabletopItemVisualizer
|
||||||
|
|
||||||
|
# White pieces
|
||||||
|
- type: entity
|
||||||
|
id: WhiteKing
|
||||||
|
name: white king
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: w_king
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: WhiteQueen
|
||||||
|
name: white queen
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: w_queen
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: WhiteRook
|
||||||
|
name: white rook
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: w_rook
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: WhiteBishop
|
||||||
|
name: white bishop
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: w_bishop
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: WhiteKnight
|
||||||
|
name: white knight
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: w_knight
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: WhitePawn
|
||||||
|
name: white pawn
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: w_pawn
|
||||||
|
|
||||||
|
# Black pieces
|
||||||
|
- type: entity
|
||||||
|
id: BlackKing
|
||||||
|
name: black king
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: b_king
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: BlackQueen
|
||||||
|
name: black queen
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: b_queen
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: BlackRook
|
||||||
|
name: black rook
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: b_rook
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: BlackBishop
|
||||||
|
name: black bishop
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: b_bishop
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: BlackKnight
|
||||||
|
name: black knight
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: b_knight
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: BlackPawn
|
||||||
|
name: black pawn
|
||||||
|
parent: BaseChessPiece
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Fun/Tabletop/chess_pieces.rsi
|
||||||
|
state: b_pawn
|
||||||
79
Resources/Textures/Interface/VerbIcons/die.svg
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 8.4666665 8.4666665"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||||
|
sodipodi:docname="die.svg"
|
||||||
|
inkscape:export-filename="C:\Users\vince\Documents\GitKraken\space-wizards\space-station-14\Resources\Textures\Interface\VerbIcons\die.svg.192dpi.png"
|
||||||
|
inkscape:export-xdpi="192"
|
||||||
|
inkscape:export-ydpi="192">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="11.313709"
|
||||||
|
inkscape:cx="11.175023"
|
||||||
|
inkscape:cy="18.632593"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="true"
|
||||||
|
units="px"
|
||||||
|
objecttolerance="20"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
scale-x="1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid5090"
|
||||||
|
originx="0"
|
||||||
|
originy="-3.4933333e-006" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-288.53334)">
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.90235829;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
d="M 8 4 C 5.7840003 4 4 5.7840252 4 8 L 4 24 C 4 26.216013 5.7840003 28 8 28 L 24 28 C 26.216 28 28 26.216013 28 24 L 28 8 C 28 5.7840252 26.216 4 24 4 L 8 4 z M 10.5 8 A 2.4999999 2.4999999 0 0 1 13 10.5 A 2.4999999 2.4999999 0 0 1 10.5 13 A 2.4999999 2.4999999 0 0 1 8 10.5 A 2.4999999 2.4999999 0 0 1 10.5 8 z M 21.5 8 A 2.4999999 2.4999999 0 0 1 24 10.5 A 2.4999999 2.4999999 0 0 1 21.5 13 A 2.4999999 2.4999999 0 0 1 19 10.5 A 2.4999999 2.4999999 0 0 1 21.5 8 z M 16 13.5 A 2.4999999 2.4999999 0 0 1 18.5 16 A 2.4999999 2.4999999 0 0 1 16 18.5 A 2.4999999 2.4999999 0 0 1 13.5 16 A 2.4999999 2.4999999 0 0 1 16 13.5 z M 10.5 19 A 2.4999999 2.4999999 0 0 1 13 21.5 A 2.4999999 2.4999999 0 0 1 10.5 24 A 2.4999999 2.4999999 0 0 1 8 21.5 A 2.4999999 2.4999999 0 0 1 10.5 19 z M 21.5 19 A 2.4999999 2.4999999 0 0 1 24 21.5 A 2.4999999 2.4999999 0 0 1 21.5 24 A 2.4999999 2.4999999 0 0 1 19 21.5 A 2.4999999 2.4999999 0 0 1 21.5 19 z "
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,0,288.53334)"
|
||||||
|
id="rect5088" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
BIN
Resources/Textures/Interface/VerbIcons/die.svg.192dpi.png
Normal file
|
After Width: | Height: | Size: 755 B |
@@ -0,0 +1,2 @@
|
|||||||
|
sample:
|
||||||
|
filter: true
|
||||||
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 466 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 426 B |
|
After Width: | Height: | Size: 405 B |
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"license": "CC-BY-SA-3.0",
|
||||||
|
"copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/3edffc96061f135b836bc353ee29ad9ab220fa54",
|
||||||
|
"size": {
|
||||||
|
"x": 32,
|
||||||
|
"y": 32
|
||||||
|
},
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"name": "w_pawn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "w_rook"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "w_knight"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "w_bishop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "w_king"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "w_queen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "b_pawn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "b_rook"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "b_knight"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "b_bishop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "b_king"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "b_queen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 466 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 426 B |
|
After Width: | Height: | Size: 405 B |
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"license": "CC-BY-SA-3.0",
|
||||||
|
"copyright": "Visne",
|
||||||
|
"size": {
|
||||||
|
"x": 18,
|
||||||
|
"y": 18
|
||||||
|
},
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"name": "chessboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"license": "CC-BY-SA-3.0",
|
||||||
|
"copyright": "Stanbery Trask#5343, Visne",
|
||||||
|
"size": {
|
||||||
|
"x": 274,
|
||||||
|
"y": 274
|
||||||
|
},
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"name": "chessboard_tabletop"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||