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>
This commit is contained in:
Visne
2021-09-13 11:58:44 +02:00
committed by GitHub
parent 19551047c8
commit 973926fc9a
41 changed files with 1287 additions and 4 deletions

View File

@@ -275,6 +275,7 @@ namespace Content.Client.Entry
"BatteryCharger",
"SpawnItemsOnUse",
"AmbientOnPowered",
"TabletopGame"
};
}
}

View File

@@ -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;
}
}

View 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
}
}

View 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>

View 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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -347,7 +347,7 @@ namespace Content.Server.Interaction
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))
return;
@@ -393,11 +393,11 @@ namespace Content.Server.Interaction
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)
{
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;
}
@@ -873,7 +873,7 @@ namespace Content.Server.Interaction
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))
{
var item = hands.GetActiveHand?.Owner;

View File

@@ -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();
}
}
}
}

View 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);
}
}
}
}

View 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);
}
}
}

View 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));
}
}
}
}

View 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
}
}

View File

@@ -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";
}
}

View File

@@ -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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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
}
}

View File

@@ -0,0 +1,12 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.Tabletop
{
[Serializable, NetSerializable]
public enum TabletopItemVisuals : byte
{
Scale,
DrawDepth
}
}

View File

@@ -0,0 +1,5 @@
## TabletopGameComponent
tabletop-verb-play-game = Play Game
## Chess
tabletop-chess-flip = Flip

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

View File

@@ -0,0 +1,2 @@
sample:
filter: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -0,0 +1,14 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Visne",
"size": {
"x": 18,
"y": 18
},
"states": [
{
"name": "chessboard"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -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"
}
]
}