mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
Table top prediction (#44348)
* TabletopSystem-move-to-shared * final refactor * Update TabletopHologramComponent.cs * Update AdminVerbSystem.Smites.cs * merge * line * prediction fix * Fix the scrunkly * Also noticed this --------- Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
parent
ef367d2ea1
commit
95a5f7c7de
27 changed files with 882 additions and 905 deletions
|
|
@ -27,13 +27,12 @@ namespace Content.Client.Tabletop
|
|||
[Dependency] private IPlayerManager _playerManager = default!;
|
||||
[Dependency] private IGameTiming _gameTiming = default!;
|
||||
[Dependency] private AppearanceSystem _appearance = default!;
|
||||
[Dependency] private SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private SharedTransformSystem _transform = default!;
|
||||
[Dependency] private SpriteSystem _sprite = 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 EntityUid? _draggedEntity; // Entity being dragged
|
||||
private ScalingViewport? _viewport; // Viewport currently being used
|
||||
private BaseWindow? _window; // Current open tabletop window (only allow one at a time)
|
||||
|
|
@ -60,8 +59,11 @@ namespace Content.Client.Tabletop
|
|||
StopDragging(false);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
if (!_gameTiming.IsFirstTimePredicted)
|
||||
return;
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
|
|
@ -101,17 +103,10 @@ namespace Content.Client.Tabletop
|
|||
var clampedCoords = ClampPositionToViewport(coords, _viewport);
|
||||
if (clampedCoords.Equals(MapCoordinates.Nullspace)) return;
|
||||
|
||||
// Move the entity locally every update
|
||||
_transformSystem.SetWorldPosition(_draggedEntity.Value, clampedCoords.Position);
|
||||
|
||||
// Increment total time passed
|
||||
_timePassed += frameTime;
|
||||
|
||||
// Only send new position to server when Delay is reached
|
||||
if (_timePassed >= Delay && _table != null)
|
||||
if (_table != null)
|
||||
{
|
||||
RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), clampedCoords, GetNetEntity(_table.Value)));
|
||||
_timePassed -= Delay;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,7 +255,7 @@ namespace Content.Client.Tabletop
|
|||
// Set the dragging player on the component to noone
|
||||
if (broadcast && _draggedEntity != null && HasComp<TabletopDraggableComponent>(_draggedEntity.Value))
|
||||
{
|
||||
RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), Transforms.GetMapCoordinates(_draggedEntity.Value), GetNetEntity(_table!.Value)));
|
||||
RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), _transform.GetMapCoordinates(_draggedEntity.Value), GetNetEntity(_table!.Value)));
|
||||
RaisePredictiveEvent(new TabletopDraggingPlayerChangedEvent(GetNetEntity(_draggedEntity.Value), false));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,9 @@ using Content.Server.Speech.Components;
|
|||
using Content.Shared.Speech.Components;
|
||||
using Content.Server.Storage.EntitySystems;
|
||||
using Content.Server.Tabletop;
|
||||
using Content.Server.Tabletop.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Components;
|
||||
using Content.Shared.Administration.Systems;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Body.Components;
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Content.Server.Tabletop.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A component that makes an object playable as a tabletop game.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(TabletopSystem))]
|
||||
public sealed partial class TabletopGameComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The localized name of the board. Shown in the UI.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId BoardName { get; private set; } = "tabletop-default-board-name";
|
||||
|
||||
/// <summary>
|
||||
/// The type of method used to set up a tabletop.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public TabletopSetup Setup { get; private set; } = new TabletopChessSetup();
|
||||
|
||||
/// <summary>
|
||||
/// The size of the viewport being opened. Must match the board dimensions otherwise you'll get the space parallax (unless that's what you want).
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Vector2i Size { get; private set; } = (300, 300);
|
||||
|
||||
/// <summary>
|
||||
/// The zoom of the viewport camera.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Vector2 CameraZoom { get; private set; } = Vector2.One;
|
||||
|
||||
/// <summary>
|
||||
/// The specific session of this tabletop.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public TabletopSession? Session { get; set; } = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
namespace Content.Server.Tabletop.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Component for marking an entity as currently playing a tabletop.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(TabletopSystem))]
|
||||
public sealed partial class TabletopGamerComponent : Component
|
||||
{
|
||||
[DataField("tabletop")]
|
||||
public EntityUid Tabletop { get; set; } = EntityUid.Invalid;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Tabletop
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopBackgammonSetup : TabletopSetup
|
||||
{
|
||||
|
||||
[DataField("whitePiecePrototype")]
|
||||
public string WhitePiecePrototype { get; private set; } = "WhiteTabletopPiece";
|
||||
|
||||
[DataField("blackPiecePrototype")]
|
||||
public string BlackPiecePrototype { get; private set; } = "BlackTabletopPiece";
|
||||
public override void SetupTabletop(TabletopSession session, IEntityManager entityManager)
|
||||
{
|
||||
var board = entityManager.SpawnEntity(BoardPrototype, session.Position);
|
||||
|
||||
const float borderLengthX = 7.35f; //BORDER
|
||||
const float borderLengthY = 5.60f; //BORDER
|
||||
|
||||
const float boardDistanceX = 1.25f;
|
||||
const float pieceDistanceY = 0.80f;
|
||||
|
||||
float GetXPosition(float distanceFromSide, bool isLeftSide)
|
||||
{
|
||||
var pos = borderLengthX - (distanceFromSide * boardDistanceX);
|
||||
return isLeftSide ? -pos : pos;
|
||||
}
|
||||
|
||||
float GetYPosition(float positionNumber, bool isTop)
|
||||
{
|
||||
var pos = borderLengthY - (pieceDistanceY * positionNumber);
|
||||
return isTop ? pos : -pos;
|
||||
}
|
||||
|
||||
void AddPieces(
|
||||
float distanceFromSide,
|
||||
int numberOfPieces,
|
||||
bool isBlackPiece,
|
||||
bool isTop,
|
||||
bool isLeftSide)
|
||||
{
|
||||
for (int i = 0; i < numberOfPieces; i++)
|
||||
{
|
||||
session.Entities.Add(entityManager.SpawnEntity(isBlackPiece ? BlackPiecePrototype : WhitePiecePrototype, session.Position.Offset(GetXPosition(distanceFromSide, isLeftSide), GetYPosition(i, isTop))));
|
||||
}
|
||||
}
|
||||
|
||||
// Top left
|
||||
AddPieces(0, 5, true, true, true);
|
||||
// top middle left
|
||||
AddPieces(4, 3, false, true, true);
|
||||
// top middle right
|
||||
AddPieces(5, 5, false, true, false);
|
||||
// top far right
|
||||
AddPieces(0, 2, true, true, false);
|
||||
// bottom left
|
||||
AddPieces(0, 5, false, false, true);
|
||||
// bottom middle left
|
||||
AddPieces(4, 3, true, false, true);
|
||||
// bottom middle right
|
||||
AddPieces(5, 5, true, false, false);
|
||||
// bottom far right
|
||||
AddPieces(0, 2, false, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Tabletop
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopCheckerSetup : TabletopSetup
|
||||
{
|
||||
|
||||
[DataField("prototypePieceWhite", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string PrototypePieceWhite = default!;
|
||||
|
||||
[DataField("prototypeCrownWhite", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string PrototypeCrownWhite = default!;
|
||||
|
||||
[DataField("prototypePieceBlack", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string PrototypePieceBlack = default!;
|
||||
|
||||
[DataField("prototypeCrownBlack", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string PrototypeCrownBlack = default!;
|
||||
|
||||
public override void SetupTabletop(TabletopSession session, IEntityManager entityManager)
|
||||
{
|
||||
session.Entities.Add(
|
||||
entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(-1, 0))
|
||||
);
|
||||
|
||||
SpawnPieces(session, entityManager, session.Position.Offset(-4.5f, 3.5f));
|
||||
}
|
||||
|
||||
private void SpawnPieces(TabletopSession session, IEntityManager entityManager, MapCoordinates left)
|
||||
{
|
||||
static float GetOffset(float offset) => offset * 1f /* separation */;
|
||||
|
||||
Span<EntityUid> pieces = stackalloc EntityUid[42];
|
||||
var pieceIndex = 0;
|
||||
|
||||
// Pieces
|
||||
for (var offsetY = 0; offsetY < 3; offsetY++)
|
||||
{
|
||||
var checker = offsetY % 2;
|
||||
|
||||
for (var offsetX = 0; offsetX < 8; offsetX += 2)
|
||||
{
|
||||
// Prevents an extra piece on the middle row
|
||||
if (checker + offsetX > 8) continue;
|
||||
|
||||
pieces[pieceIndex] = entityManager.SpawnEntity(
|
||||
PrototypePieceBlack,
|
||||
left.Offset(GetOffset(offsetX + (1 - checker)), GetOffset(offsetY * -1))
|
||||
);
|
||||
pieces[pieceIndex] = entityManager.SpawnEntity(
|
||||
PrototypePieceWhite,
|
||||
left.Offset(GetOffset(offsetX + checker), GetOffset(offsetY - 7))
|
||||
);
|
||||
pieceIndex += 2;
|
||||
}
|
||||
}
|
||||
|
||||
const int NumCrowns = 3;
|
||||
const float Overlap = 0.25f;
|
||||
const float xOffset = 9f / 32;
|
||||
const float xOffsetBlack = 9 + xOffset;
|
||||
const float xOffsetWhite = 8 + xOffset;
|
||||
|
||||
// Crowns
|
||||
for (var i = 0; i < NumCrowns; i++)
|
||||
{
|
||||
var step = -(Overlap * i);
|
||||
pieces[pieceIndex] = entityManager.SpawnEntity(
|
||||
PrototypeCrownBlack,
|
||||
left.Offset(GetOffset(xOffsetBlack), GetOffset(step))
|
||||
);
|
||||
pieces[pieceIndex + 1] = entityManager.SpawnEntity(
|
||||
PrototypeCrownWhite,
|
||||
left.Offset(GetOffset(xOffsetWhite), GetOffset(step))
|
||||
);
|
||||
pieceIndex += 2;
|
||||
}
|
||||
|
||||
// Spares
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var step = -((Overlap * (NumCrowns + 2)) + (Overlap * i));
|
||||
pieces[pieceIndex] = entityManager.SpawnEntity(
|
||||
PrototypePieceBlack,
|
||||
left.Offset(GetOffset(xOffsetBlack), GetOffset(step))
|
||||
);
|
||||
pieces[pieceIndex] = entityManager.SpawnEntity(
|
||||
PrototypePieceWhite,
|
||||
left.Offset(GetOffset(xOffsetWhite), GetOffset(step))
|
||||
);
|
||||
pieceIndex += 2;
|
||||
}
|
||||
|
||||
for (var i = 0; i < pieces.Length; i++)
|
||||
{
|
||||
session.Entities.Add(pieces[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Tabletop
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopChessSetup : TabletopSetup
|
||||
{
|
||||
|
||||
// TODO: Un-hardcode the rest of entity prototype IDs, probably.
|
||||
|
||||
public override void SetupTabletop(TabletopSession session, IEntityManager entityManager)
|
||||
{
|
||||
var chessboard = entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(-1, 0));
|
||||
|
||||
session.Entities.Add(chessboard);
|
||||
|
||||
SpawnPieces(session, entityManager, session.Position.Offset(-4.5f, 3.5f));
|
||||
}
|
||||
|
||||
private void SpawnPieces(TabletopSession session, IEntityManager entityManager, MapCoordinates topLeft, float separation = 1f)
|
||||
{
|
||||
var (mapId, x, y) = topLeft;
|
||||
|
||||
// Spawn all black pieces
|
||||
SpawnPiecesRow(session, entityManager, "Black", topLeft, separation);
|
||||
SpawnPawns(session, entityManager, "Black", new MapCoordinates(x, y - separation, mapId) , separation);
|
||||
|
||||
// Spawn all white pieces
|
||||
SpawnPawns(session, entityManager, "White", new MapCoordinates(x, y - 6 * separation, mapId) , separation);
|
||||
SpawnPiecesRow(session, entityManager, "White", new MapCoordinates(x, y - 7 * separation, mapId), separation);
|
||||
|
||||
// Extra queens
|
||||
EntityUid tempQualifier = entityManager.SpawnEntity("BlackQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 3 * separation, mapId));
|
||||
session.Entities.Add(tempQualifier);
|
||||
EntityUid tempQualifier1 = entityManager.SpawnEntity("WhiteQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 4 * separation, mapId));
|
||||
session.Entities.Add(tempQualifier1);
|
||||
}
|
||||
|
||||
// TODO: refactor to load FEN instead
|
||||
private void SpawnPiecesRow(TabletopSession session, IEntityManager entityManager, 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':
|
||||
EntityUid tempQualifier = entityManager.SpawnEntity(color + "Rook", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier);
|
||||
break;
|
||||
case 'n':
|
||||
EntityUid tempQualifier1 = entityManager.SpawnEntity(color + "Knight", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier1);
|
||||
break;
|
||||
case 'b':
|
||||
EntityUid tempQualifier2 = entityManager.SpawnEntity(color + "Bishop", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier2);
|
||||
break;
|
||||
case 'q':
|
||||
EntityUid tempQualifier3 = entityManager.SpawnEntity(color + "Queen", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier3);
|
||||
break;
|
||||
case 'k':
|
||||
EntityUid tempQualifier4 = entityManager.SpawnEntity(color + "King", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactor to load FEN instead
|
||||
private void SpawnPawns(TabletopSession session, IEntityManager entityManager, string color, MapCoordinates left, float separation = 1f)
|
||||
{
|
||||
var (mapId, x, y) = left;
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
EntityUid tempQualifier = entityManager.SpawnEntity(color + "Pawn", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Tabletop
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopEmptySetup : TabletopSetup
|
||||
{
|
||||
public override void SetupTabletop(TabletopSession session, IEntityManager entityManager)
|
||||
{
|
||||
var board = entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(0, 0));
|
||||
session.Entities.Add(board);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Tabletop
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopParchisSetup : TabletopSetup
|
||||
{
|
||||
|
||||
[DataField("redPiecePrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string RedPiecePrototype { get; private set; } = "RedTabletopPiece";
|
||||
|
||||
[DataField("greenPiecePrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string GreenPiecePrototype { get; private set; } = "GreenTabletopPiece";
|
||||
|
||||
[DataField("yellowPiecePrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string YellowPiecePrototype { get; private set; } = "YellowTabletopPiece";
|
||||
|
||||
[DataField("bluePiecePrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string BluePiecePrototype { get; private set; } = "BlueTabletopPiece";
|
||||
|
||||
public override void SetupTabletop(TabletopSession session, IEntityManager entityManager)
|
||||
{
|
||||
var board = entityManager.SpawnEntity(BoardPrototype, session.Position);
|
||||
|
||||
const float x1 = 6.25f;
|
||||
const float x2 = 4.25f;
|
||||
|
||||
const float y1 = 6.25f;
|
||||
const float y2 = 4.25f;
|
||||
|
||||
var center = session.Position;
|
||||
|
||||
// Red pieces.
|
||||
EntityUid tempQualifier = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y1));
|
||||
session.Entities.Add(tempQualifier);
|
||||
EntityUid tempQualifier1 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y2));
|
||||
session.Entities.Add(tempQualifier1);
|
||||
EntityUid tempQualifier2 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y1));
|
||||
session.Entities.Add(tempQualifier2);
|
||||
EntityUid tempQualifier3 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y2));
|
||||
session.Entities.Add(tempQualifier3);
|
||||
|
||||
// Green pieces.
|
||||
EntityUid tempQualifier4 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y1));
|
||||
session.Entities.Add(tempQualifier4);
|
||||
EntityUid tempQualifier5 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y2));
|
||||
session.Entities.Add(tempQualifier5);
|
||||
EntityUid tempQualifier6 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y1));
|
||||
session.Entities.Add(tempQualifier6);
|
||||
EntityUid tempQualifier7 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y2));
|
||||
session.Entities.Add(tempQualifier7);
|
||||
|
||||
// Yellow pieces.
|
||||
EntityUid tempQualifier8 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y1));
|
||||
session.Entities.Add(tempQualifier8);
|
||||
EntityUid tempQualifier9 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y2));
|
||||
session.Entities.Add(tempQualifier9);
|
||||
EntityUid tempQualifier10 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y1));
|
||||
session.Entities.Add(tempQualifier10);
|
||||
EntityUid tempQualifier11 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y2));
|
||||
session.Entities.Add(tempQualifier11);
|
||||
|
||||
// Blue pieces.
|
||||
EntityUid tempQualifier12 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y1));
|
||||
session.Entities.Add(tempQualifier12);
|
||||
EntityUid tempQualifier13 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y2));
|
||||
session.Entities.Add(tempQualifier13);
|
||||
EntityUid tempQualifier14 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y1));
|
||||
session.Entities.Add(tempQualifier14);
|
||||
EntityUid tempQualifier15 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y2));
|
||||
session.Entities.Add(tempQualifier15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
using System.Numerics;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Tabletop
|
||||
{
|
||||
/// <summary>
|
||||
/// A class for storing data about a running tabletop game.
|
||||
/// </summary>
|
||||
public sealed class TabletopSession
|
||||
{
|
||||
/// <summary>
|
||||
/// The center position of this session.
|
||||
/// </summary>
|
||||
public readonly MapCoordinates Position;
|
||||
|
||||
/// <summary>
|
||||
/// The set of players currently playing this tabletop game.
|
||||
/// </summary>
|
||||
public readonly Dictionary<ICommonSession, TabletopSessionPlayerData> Players = new();
|
||||
|
||||
/// <summary>
|
||||
/// All entities bound to this session. If you create an entity for this session, you have to add it here.
|
||||
/// </summary>
|
||||
public readonly HashSet<EntityUid> Entities = new();
|
||||
|
||||
public TabletopSession(MapId tabletopMap, Vector2 position)
|
||||
{
|
||||
Position = new MapCoordinates(position, tabletopMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
namespace Content.Server.Tabletop
|
||||
{
|
||||
/// <summary>
|
||||
/// A class that stores per-player data for tabletops.
|
||||
/// </summary>
|
||||
public sealed class TabletopSessionPlayerData
|
||||
{
|
||||
public EntityUid Camera { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Tabletop
|
||||
{
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract partial class TabletopSetup
|
||||
{
|
||||
/// <summary>
|
||||
/// Method for setting up a tabletop. Use this to spawn the board and pieces, etc.
|
||||
/// Make sure you add every entity you create to the Entities hashset in the session.
|
||||
/// </summary>
|
||||
/// <param name="session">Tabletop session to set up. You'll want to grab the tabletop center position here for spawning entities.</param>
|
||||
/// <param name="entityManager">Dependency that can be used for spawning entities.</param>
|
||||
public abstract void SetupTabletop(TabletopSession session, IEntityManager entityManager);
|
||||
|
||||
[DataField("boardPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string BoardPrototype = default!;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,152 +1,143 @@
|
|||
using System.Numerics;
|
||||
using Content.Server.Tabletop.Components;
|
||||
using Content.Shared.Tabletop;
|
||||
using Content.Shared.Tabletop.Components;
|
||||
using Content.Shared.Tabletop.Events;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Tabletop
|
||||
namespace Content.Server.Tabletop;
|
||||
|
||||
public sealed partial class TabletopSystem
|
||||
{
|
||||
public sealed partial class TabletopSystem
|
||||
/// <summary>
|
||||
/// Ensures that a <see cref="TabletopSession"/> exists on a <see cref="TabletopGameComponent"/>.
|
||||
/// Creates it and sets it up if it doesn't.
|
||||
/// </summary>
|
||||
/// <param name="tabletop">The tabletop game in question.</param>
|
||||
/// <returns>The session for the given tabletop game.</returns>
|
||||
public TabletopSession EnsureSession(TabletopGameComponent tabletop)
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures that a <see cref="TabletopSession"/> exists on a <see cref="TabletopGameComponent"/>.
|
||||
/// Creates it and sets it up if it doesn't.
|
||||
/// </summary>
|
||||
/// <param name="tabletop">The tabletop game in question.</param>
|
||||
/// <returns>The session for the given tabletop game.</returns>
|
||||
public TabletopSession EnsureSession(TabletopGameComponent tabletop)
|
||||
// We already have a session, return it.
|
||||
// TODO: if tables are connected, treat them as a single entity. This can be done by sharing the session.
|
||||
if (tabletop.Session != null)
|
||||
return tabletop.Session;
|
||||
|
||||
// We make sure that the tabletop map exists before continuing.
|
||||
EnsureTabletopMap();
|
||||
|
||||
// Create new session.
|
||||
var session = new TabletopSession(TabletopMap, GetNextTabletopPosition());
|
||||
tabletop.Session = session;
|
||||
|
||||
// Since this is the first time opening this session, set up the game.
|
||||
tabletop.Setup.SetupTabletop(session, EntityManager);
|
||||
|
||||
Log.Info($"Created tabletop session number {tabletop} at position {session.Position}.");
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up a tabletop game session, deleting every entity in it.
|
||||
/// </summary>
|
||||
/// <param name="uid">The UID of the tabletop game entity.</param>
|
||||
public void CleanupSession(EntityUid uid)
|
||||
{
|
||||
if (!TryComp(uid, out TabletopGameComponent? tabletop))
|
||||
return;
|
||||
|
||||
if (tabletop.Session is not { } session)
|
||||
return;
|
||||
|
||||
foreach (var (player, _) in session.Players)
|
||||
CloseSessionFor(player, uid);
|
||||
|
||||
foreach (var euid in session.Entities)
|
||||
QueueDel(euid);
|
||||
|
||||
tabletop.Session = null;
|
||||
}
|
||||
|
||||
public override void OpenSessionFor(ICommonSession player, EntityUid uid)
|
||||
{
|
||||
if (!TryComp(uid, out TabletopGameComponent? tabletop) || player.AttachedEntity is not { Valid: true } attachedEntity)
|
||||
return;
|
||||
|
||||
// Make sure we have a session, and add the player to it if not added already.
|
||||
var session = EnsureSession(tabletop);
|
||||
|
||||
if (session.Players.ContainsKey(player))
|
||||
return;
|
||||
|
||||
if (TryComp(attachedEntity, out TabletopGamerComponent? gamer))
|
||||
CloseSessionFor(player, gamer.Tabletop, false);
|
||||
|
||||
// Set the entity as an absolute GAMER.
|
||||
EnsureComp<TabletopGamerComponent>(attachedEntity).Tabletop = uid;
|
||||
|
||||
// Create a camera for the gamer to use.
|
||||
var camera = CreateCamera(tabletop, player);
|
||||
|
||||
session.Players[player] = new TabletopSessionPlayerData { Camera = camera };
|
||||
|
||||
// Tell the gamer to open a viewport for the tabletop game.
|
||||
RaiseNetworkEvent(new TabletopPlayEvent(GetNetEntity(uid), GetNetEntity(camera), Loc.GetString(tabletop.BoardName), tabletop.Size), player.Channel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a player from a tabletop game session, and sends them a message so their tabletop window is closed.
|
||||
/// </summary>
|
||||
/// <param name="player">The player in question.</param>
|
||||
/// <param name="uid">The UID of the tabletop game entity.</param>
|
||||
/// <param name="removeGamerComponent">Whether to remove the <see cref="TabletopGamerComponent"/> from the player's attached entity.</param>
|
||||
public void CloseSessionFor(ICommonSession player, EntityUid uid, bool removeGamerComponent = true)
|
||||
{
|
||||
if (!TryComp(uid, out TabletopGameComponent? tabletop) || tabletop.Session is not { } session)
|
||||
return;
|
||||
|
||||
if (!session.Players.TryGetValue(player, out var data))
|
||||
return;
|
||||
|
||||
if (removeGamerComponent && player.AttachedEntity is { } attachedEntity && TryComp(attachedEntity, out TabletopGamerComponent? gamer))
|
||||
{
|
||||
// We already have a session, return it
|
||||
// TODO: if tables are connected, treat them as a single entity. This can be done by sharing the session.
|
||||
if (tabletop.Session != null)
|
||||
return tabletop.Session;
|
||||
// We invalidate this to prevent an infinite feedback from removing the component.
|
||||
gamer.Tabletop = EntityUid.Invalid;
|
||||
|
||||
// We make sure that the tabletop map exists before continuing.
|
||||
EnsureTabletopMap();
|
||||
|
||||
// Create new session.
|
||||
var session = new TabletopSession(TabletopMap, GetNextTabletopPosition());
|
||||
tabletop.Session = session;
|
||||
|
||||
// Since this is the first time opening this session, set up the game
|
||||
tabletop.Setup.SetupTabletop(session, EntityManager);
|
||||
|
||||
Log.Info($"Created tabletop session number {tabletop} at position {session.Position}.");
|
||||
|
||||
return session;
|
||||
// You stop being a gamer.......
|
||||
RemComp<TabletopGamerComponent>(attachedEntity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up a tabletop game session, deleting every entity in it.
|
||||
/// </summary>
|
||||
/// <param name="uid">The UID of the tabletop game entity.</param>
|
||||
public void CleanupSession(EntityUid uid)
|
||||
{
|
||||
if (!TryComp(uid, out TabletopGameComponent? tabletop))
|
||||
return;
|
||||
session.Players.Remove(player);
|
||||
session.Entities.Remove(data.Camera);
|
||||
|
||||
if (tabletop.Session is not { } session)
|
||||
return;
|
||||
// Deleting the view subscriber automatically cleans up subscriptions, no need to do anything else.
|
||||
QueueDel(data.Camera);
|
||||
}
|
||||
|
||||
foreach (var (player, _) in session.Players)
|
||||
{
|
||||
CloseSessionFor(player, uid);
|
||||
}
|
||||
/// <summary>
|
||||
/// A helper method that creates a camera for a specified player, in a tabletop game session.
|
||||
/// </summary>
|
||||
/// <param name="tabletop">The tabletop game component in question.</param>
|
||||
/// <param name="player">The player in question.</param>
|
||||
/// <param name="offset">An offset from the tabletop position for the camera. Zero by default.</param>
|
||||
/// <returns>The UID of the camera entity.</returns>
|
||||
private EntityUid CreateCamera(TabletopGameComponent tabletop, ICommonSession player, Vector2 offset = default)
|
||||
{
|
||||
DebugTools.AssertNotNull(tabletop.Session);
|
||||
|
||||
foreach (var euid in session.Entities)
|
||||
{
|
||||
QueueDel(euid);
|
||||
}
|
||||
var session = tabletop.Session!;
|
||||
|
||||
tabletop.Session = null;
|
||||
}
|
||||
// Spawn an empty entity at the coordinates.
|
||||
var camera = EntityManager.SpawnEntity(null, session.Position.Offset(offset));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a player to a tabletop game session, sending a message so the tabletop window opens on their end.
|
||||
/// </summary>
|
||||
/// <param name="player">The player session in question.</param>
|
||||
/// <param name="uid">The UID of the tabletop game entity.</param>
|
||||
public void OpenSessionFor(ICommonSession player, EntityUid uid)
|
||||
{
|
||||
if (!TryComp(uid, out TabletopGameComponent? tabletop) || player.AttachedEntity is not {Valid: true} attachedEntity)
|
||||
return;
|
||||
// Add an eye component and disable FOV.
|
||||
var eyeComponent = EnsureComp<EyeComponent>(camera);
|
||||
_eye.SetDrawFov(camera, false, eyeComponent);
|
||||
_eye.SetZoom(camera, tabletop.CameraZoom, eyeComponent);
|
||||
|
||||
// Make sure we have a session, and add the player to it if not added already.
|
||||
var session = EnsureSession(tabletop);
|
||||
// Add the user to the view subscribers. If there is no player session, just skip this step.
|
||||
_viewSubscriberSystem.AddViewSubscriber(camera, player);
|
||||
|
||||
if (session.Players.ContainsKey(player))
|
||||
return;
|
||||
|
||||
if(TryComp(attachedEntity, out TabletopGamerComponent? gamer))
|
||||
CloseSessionFor(player, gamer.Tabletop, false);
|
||||
|
||||
// Set the entity as an absolute GAMER.
|
||||
EnsureComp<TabletopGamerComponent>(attachedEntity).Tabletop = uid;
|
||||
|
||||
// Create a camera for the gamer to use
|
||||
var camera = CreateCamera(tabletop, player);
|
||||
|
||||
session.Players[player] = new TabletopSessionPlayerData { Camera = camera };
|
||||
|
||||
// Tell the gamer to open a viewport for the tabletop game
|
||||
RaiseNetworkEvent(new TabletopPlayEvent(GetNetEntity(uid), GetNetEntity(camera), Loc.GetString(tabletop.BoardName), tabletop.Size), player.Channel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a player from a tabletop game session, and sends them a message so their tabletop window is closed.
|
||||
/// </summary>
|
||||
/// <param name="player">The player in question.</param>
|
||||
/// <param name="uid">The UID of the tabletop game entity.</param>
|
||||
/// <param name="removeGamerComponent">Whether to remove the <see cref="TabletopGamerComponent"/> from the player's attached entity.</param>
|
||||
public void CloseSessionFor(ICommonSession player, EntityUid uid, bool removeGamerComponent = true)
|
||||
{
|
||||
if (!TryComp(uid, out TabletopGameComponent? tabletop) || tabletop.Session is not { } session)
|
||||
return;
|
||||
|
||||
if (!session.Players.TryGetValue(player, out var data))
|
||||
return;
|
||||
|
||||
if(removeGamerComponent && player.AttachedEntity is {} attachedEntity && TryComp(attachedEntity, out TabletopGamerComponent? gamer))
|
||||
{
|
||||
// We invalidate this to prevent an infinite feedback from removing the component.
|
||||
gamer.Tabletop = EntityUid.Invalid;
|
||||
|
||||
// You stop being a gamer.......
|
||||
RemComp<TabletopGamerComponent>(attachedEntity);
|
||||
}
|
||||
|
||||
session.Players.Remove(player);
|
||||
session.Entities.Remove(data.Camera);
|
||||
|
||||
// Deleting the view subscriber automatically cleans up subscriptions, no need to do anything else.
|
||||
QueueDel(data.Camera);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A helper method that creates a camera for a specified player, in a tabletop game session.
|
||||
/// </summary>
|
||||
/// <param name="tabletop">The tabletop game component in question.</param>
|
||||
/// <param name="player">The player in question.</param>
|
||||
/// <param name="offset">An offset from the tabletop position for the camera. Zero by default.</param>
|
||||
/// <returns>The UID of the camera entity.</returns>
|
||||
private EntityUid CreateCamera(TabletopGameComponent tabletop, ICommonSession player, Vector2 offset = default)
|
||||
{
|
||||
DebugTools.AssertNotNull(tabletop.Session);
|
||||
|
||||
var session = tabletop.Session!;
|
||||
|
||||
// Spawn an empty entity at the coordinates
|
||||
var camera = EntityManager.SpawnEntity(null, session.Position.Offset(offset));
|
||||
|
||||
// Add an eye component and disable FOV
|
||||
var eyeComponent = EnsureComp<EyeComponent>(camera);
|
||||
_eye.SetDrawFov(camera, false, eyeComponent);
|
||||
_eye.SetZoom(camera, tabletop.CameraZoom, eyeComponent);
|
||||
|
||||
// Add the user to the view subscribers. If there is no player session, just skip this step
|
||||
_viewSubscriberSystem.AddViewSubscriber(camera, player);
|
||||
|
||||
return camera;
|
||||
}
|
||||
return camera;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,202 +1,105 @@
|
|||
using Content.Server.Hands.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Tabletop.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Hands.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Tabletop;
|
||||
using Content.Shared.Tabletop.Components;
|
||||
using Content.Shared.Tabletop.Events;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Tabletop
|
||||
namespace Content.Server.Tabletop;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopSystem : SharedTabletopSystem
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopSystem : SharedTabletopSystem
|
||||
[Dependency] private EyeSystem _eye = default!;
|
||||
[Dependency] private SharedMapSystem _map = default!;
|
||||
[Dependency] private ViewSubscriberSystem _viewSubscriberSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private SharedMapSystem _map = default!;
|
||||
[Dependency] private EyeSystem _eye = default!;
|
||||
[Dependency] private HandsSystem _hands = default!;
|
||||
[Dependency] private ViewSubscriberSystem _viewSubscriberSystem = default!;
|
||||
[Dependency] private PopupSystem _popupSystem = default!;
|
||||
[Dependency] private IConfigurationManager _cfg = default!;
|
||||
base.Initialize();
|
||||
|
||||
public override void Initialize()
|
||||
SubscribeNetworkEvent<TabletopStopPlayingEvent>(OnStopPlaying);
|
||||
|
||||
SubscribeLocalEvent<TabletopGameComponent, ActivateInWorldEvent>(OnTabletopActivate);
|
||||
SubscribeLocalEvent<TabletopGameComponent, ComponentShutdown>(OnGameShutdown);
|
||||
SubscribeLocalEvent<TabletopGamerComponent, PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<TabletopGamerComponent, ComponentShutdown>(OnGamerShutdown);
|
||||
|
||||
InitializeMap();
|
||||
}
|
||||
|
||||
protected override void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession is not { } playerSession)
|
||||
return;
|
||||
|
||||
if (!TryComp(GetEntity(msg.TableUid), out TabletopGameComponent? tabletop) || tabletop.Session is not { } session)
|
||||
return;
|
||||
|
||||
// Check if player is actually playing at this table.
|
||||
if (!session.Players.ContainsKey(playerSession))
|
||||
return;
|
||||
|
||||
base.OnTabletopMove(msg, args);
|
||||
}
|
||||
|
||||
private void OnTabletopActivate(Entity<TabletopGameComponent> ent, ref ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
// Check that a player is attached to the entity.
|
||||
if (!TryComp(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
OpenSessionFor(actor.PlayerSession, ent.Owner);
|
||||
}
|
||||
|
||||
private void OnGameShutdown(Entity<TabletopGameComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
CleanupSession(ent.Owner);
|
||||
}
|
||||
|
||||
private void OnStopPlaying(TabletopStopPlayingEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
CloseSessionFor(args.SenderSession, GetEntity(msg.TableUid));
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(Entity<TabletopGamerComponent> ent, ref PlayerDetachedEvent args)
|
||||
{
|
||||
if (ent.Comp.Tabletop.IsValid())
|
||||
CloseSessionFor(args.Player, ent.Comp.Tabletop);
|
||||
}
|
||||
|
||||
private void OnGamerShutdown(Entity<TabletopGamerComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
if (!TryComp(ent.Owner, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
if (ent.Comp.Tabletop.IsValid())
|
||||
CloseSessionFor(actor.PlayerSession, ent.Comp.Tabletop);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<TabletopGamerComponent>();
|
||||
while (query.MoveNext(out var uid, out var gamer))
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<TabletopStopPlayingEvent>(OnStopPlaying);
|
||||
SubscribeLocalEvent<TabletopGameComponent, ActivateInWorldEvent>(OnTabletopActivate);
|
||||
SubscribeLocalEvent<TabletopGameComponent, ComponentShutdown>(OnGameShutdown);
|
||||
SubscribeLocalEvent<TabletopGamerComponent, PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<TabletopGamerComponent, ComponentShutdown>(OnGamerShutdown);
|
||||
SubscribeLocalEvent<TabletopGameComponent, GetVerbsEvent<ActivationVerb>>(AddPlayGameVerb);
|
||||
SubscribeLocalEvent<TabletopGameComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
if (!Exists(gamer.Tabletop))
|
||||
continue;
|
||||
|
||||
SubscribeNetworkEvent<TabletopRequestTakeOut>(OnTabletopRequestTakeOut);
|
||||
|
||||
InitializeMap();
|
||||
}
|
||||
|
||||
private void OnTabletopRequestTakeOut(TabletopRequestTakeOut msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession is not { } playerSession)
|
||||
return;
|
||||
|
||||
var table = GetEntity(msg.TableUid);
|
||||
|
||||
if (!TryComp(table, out TabletopGameComponent? tabletop) || tabletop.Session is not { } session)
|
||||
return;
|
||||
|
||||
if (!msg.Entity.IsValid())
|
||||
return;
|
||||
|
||||
var entity = GetEntity(msg.Entity);
|
||||
|
||||
if (!TryComp(entity, out TabletopHologramComponent? hologram))
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("tabletop-error-remove-non-hologram"), table, args.SenderSession);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player is actually playing at this table
|
||||
if (!session.Players.ContainsKey(playerSession))
|
||||
return;
|
||||
|
||||
// Find the entity, remove it from the session and set it's position to the tabletop
|
||||
session.Entities.TryGetValue(entity, out var result);
|
||||
session.Entities.Remove(result);
|
||||
QueueDel(result);
|
||||
}
|
||||
|
||||
private void OnInteractUsing(EntityUid uid, TabletopGameComponent component, InteractUsingEvent args)
|
||||
{
|
||||
if (!_cfg.GetCVar(CCVars.GameTabletopPlace))
|
||||
return;
|
||||
|
||||
if (!TryComp(args.User, out HandsComponent? hands))
|
||||
return;
|
||||
|
||||
if (component.Session is not { } session)
|
||||
return;
|
||||
|
||||
if (!_hands.TryGetActiveItem(uid, out var handEnt))
|
||||
return;
|
||||
|
||||
if (!TryComp<ItemComponent>(handEnt, out var item))
|
||||
return;
|
||||
|
||||
var meta = MetaData(handEnt.Value);
|
||||
var protoId = meta.EntityPrototype?.ID;
|
||||
|
||||
var hologram = Spawn(protoId, session.Position.Offset(-1, 0));
|
||||
|
||||
// Make sure the entity can be dragged and can be removed, move it into the board game world and add it to the Entities hashmap
|
||||
EnsureComp<TabletopDraggableComponent>(hologram);
|
||||
EnsureComp<TabletopHologramComponent>(hologram);
|
||||
session.Entities.Add(hologram);
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("tabletop-added-piece"), uid, args.User);
|
||||
}
|
||||
|
||||
protected override void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession is not { } playerSession)
|
||||
return;
|
||||
|
||||
if (!TryComp(GetEntity(msg.TableUid), out TabletopGameComponent? tabletop) || tabletop.Session is not { } session)
|
||||
return;
|
||||
|
||||
// Check if player is actually playing at this table
|
||||
if (!session.Players.ContainsKey(playerSession))
|
||||
return;
|
||||
|
||||
base.OnTabletopMove(msg, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a verb that allows the player to start playing a tabletop game.
|
||||
/// </summary>
|
||||
private void AddPlayGameVerb(EntityUid uid, TabletopGameComponent component, GetVerbsEvent<ActivationVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
|
||||
if (!TryComp(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
var playVerb = new ActivationVerb()
|
||||
{
|
||||
Text = Loc.GetString("tabletop-verb-play-game"),
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/die.svg.192dpi.png")),
|
||||
Act = () => OpenSessionFor(actor.PlayerSession, uid)
|
||||
};
|
||||
|
||||
args.Verbs.Add(playVerb);
|
||||
}
|
||||
|
||||
private void OnTabletopActivate(EntityUid uid, TabletopGameComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
// Check that a player is attached to the entity.
|
||||
if (!TryComp(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
OpenSessionFor(actor.PlayerSession, uid);
|
||||
}
|
||||
|
||||
private void OnGameShutdown(EntityUid uid, TabletopGameComponent component, ComponentShutdown args)
|
||||
{
|
||||
CleanupSession(uid);
|
||||
}
|
||||
|
||||
private void OnStopPlaying(TabletopStopPlayingEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
CloseSessionFor(args.SenderSession, GetEntity(msg.TableUid));
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(EntityUid uid, TabletopGamerComponent component, PlayerDetachedEvent args)
|
||||
{
|
||||
if(component.Tabletop.IsValid())
|
||||
CloseSessionFor(args.Player, component.Tabletop);
|
||||
}
|
||||
|
||||
private void OnGamerShutdown(EntityUid uid, TabletopGamerComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (!TryComp(uid, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
if(component.Tabletop.IsValid())
|
||||
CloseSessionFor(actor.PlayerSession, component.Tabletop);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<TabletopGamerComponent>();
|
||||
while (query.MoveNext(out var uid, out var gamer))
|
||||
{
|
||||
if (!Exists(gamer.Tabletop))
|
||||
continue;
|
||||
|
||||
if (!TryComp(uid, out ActorComponent? actor))
|
||||
{
|
||||
RemComp<TabletopGamerComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (actor.PlayerSession.Status != SessionStatus.InGame || !CanSeeTable(uid, gamer.Tabletop))
|
||||
CloseSessionFor(actor.PlayerSession, gamer.Tabletop);
|
||||
RemCompDeferred<TabletopGamerComponent>(uid);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (actor.PlayerSession.Status != SessionStatus.InGame || !CanSeeTable(uid, gamer.Tabletop))
|
||||
CloseSessionFor(actor.PlayerSession, gamer.Tabletop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
42
Content.Shared/Tabletop/Components/TabletopGameComponent.cs
Normal file
42
Content.Shared/Tabletop/Components/TabletopGameComponent.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System.Numerics;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Tabletop.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A component that makes an object playable as a tabletop game.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[Access(typeof(SharedTabletopSystem))]
|
||||
public sealed partial class TabletopGameComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The localized name of the board. Shown in the UI.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId BoardName { get; private set; } = "tabletop-default-board-name";
|
||||
|
||||
/// <summary>
|
||||
/// The type of method used to set up a tabletop.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public TabletopSetup Setup { get; private set; } = new TabletopChessSetup();
|
||||
|
||||
/// <summary>
|
||||
/// The size of the viewport being opened. Must match the board dimensions otherwise you'll get the space parallax (unless that's what you want).
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Vector2i Size { get; private set; } = (300, 300);
|
||||
|
||||
/// <summary>
|
||||
/// The zoom of the viewport camera.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Vector2 CameraZoom { get; private set; } = Vector2.One;
|
||||
|
||||
/// <summary>
|
||||
/// The specific session of this tabletop.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public TabletopSession? Session { get; set; } = null;
|
||||
}
|
||||
14
Content.Shared/Tabletop/Components/TabletopGamerComponent.cs
Normal file
14
Content.Shared/Tabletop/Components/TabletopGamerComponent.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Tabletop.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Component for marking an entity as currently playing a tabletop.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[Access(typeof(SharedTabletopSystem))]
|
||||
public sealed partial class TabletopGamerComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public EntityUid Tabletop = EntityUid.Invalid;
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
namespace Content.Server.Tabletop;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Tabletop.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for tracking pieces that are simply "holograms" shown on the tabletop
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class TabletopHologramComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class TabletopHologramComponent : Component;
|
||||
13
Content.Shared/Tabletop/SharedTabletopSystem.Session.cs
Normal file
13
Content.Shared/Tabletop/SharedTabletopSystem.Session.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Shared.Tabletop;
|
||||
|
||||
public abstract partial class SharedTabletopSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a player to a tabletop game session, sending a message so the tabletop window opens on their end.
|
||||
/// </summary>
|
||||
/// <param name="player">The player session in question.</param>
|
||||
/// <param name="uid">The UID of the tabletop game entity.</param>
|
||||
public virtual void OpenSessionFor(ICommonSession player, EntityUid uid) { }
|
||||
}
|
||||
|
|
@ -1,123 +1,216 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Tabletop.Components;
|
||||
using Content.Shared.Tabletop.Events;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Tabletop
|
||||
namespace Content.Shared.Tabletop;
|
||||
|
||||
public abstract partial class SharedTabletopSystem : EntitySystem
|
||||
{
|
||||
public abstract partial class SharedTabletopSystem : EntitySystem
|
||||
[Dependency] private ActionBlockerSystem _actionBlocker = default!;
|
||||
[Dependency] private IConfigurationManager _cfg = default!;
|
||||
[Dependency] private SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private SharedHandsSystem _hands = default!;
|
||||
[Dependency] private SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private SharedMapSystem _map = default!;
|
||||
[Dependency] private SharedPopupSystem _popup = default!;
|
||||
[Dependency] private SharedTransformSystem _transforms = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] protected ActionBlockerSystem ActionBlockerSystem = default!;
|
||||
[Dependency] private SharedInteractionSystem _interactionSystem = default!;
|
||||
[Dependency] private SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private SharedMapSystem _mapSystem = default!;
|
||||
[Dependency] protected SharedTransformSystem Transforms = default!;
|
||||
base.Initialize();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeAllEvent<TabletopDraggingPlayerChangedEvent>(OnDraggingPlayerChanged);
|
||||
SubscribeAllEvent<TabletopMoveEvent>(OnTabletopMove);
|
||||
}
|
||||
SubscribeAllEvent<TabletopDraggingPlayerChangedEvent>(OnDraggingPlayerChanged);
|
||||
|
||||
/// <summary>
|
||||
/// Move an entity which is dragged by the user, but check if they are allowed to do so and to these coordinates
|
||||
/// </summary>
|
||||
protected virtual void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession is not { AttachedEntity: { } playerEntity } playerSession)
|
||||
return;
|
||||
SubscribeLocalEvent<TabletopGameComponent, GetVerbsEvent<ActivationVerb>>(AddPlayGameVerb);
|
||||
SubscribeLocalEvent<TabletopGameComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
|
||||
var table = GetEntity(msg.TableUid);
|
||||
var moved = GetEntity(msg.MovedEntityUid);
|
||||
SubscribeNetworkEvent<TabletopRequestTakeOut>(OnTabletopRequestTakeOut);
|
||||
|
||||
if (!CanSeeTable(playerEntity, table) || !CanDrag(playerEntity, moved, out _))
|
||||
return;
|
||||
|
||||
// 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 = Comp<TransformComponent>(moved);
|
||||
Transforms.SetParent(moved, transform, _mapSystem.GetMapOrInvalid(transform.MapID));
|
||||
Transforms.SetLocalPositionNoLerp(moved, msg.Coordinates.Position, transform);
|
||||
}
|
||||
|
||||
private void OnDraggingPlayerChanged(TabletopDraggingPlayerChangedEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var dragged = GetEntity(msg.DraggedEntityUid);
|
||||
|
||||
if (!TryComp(dragged, out TabletopDraggableComponent? draggableComponent))
|
||||
return;
|
||||
|
||||
draggableComponent.DraggingPlayer = msg.IsDragging ? args.SenderSession.UserId : null;
|
||||
Dirty(dragged, draggableComponent);
|
||||
|
||||
if (!TryComp(dragged, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
if (draggableComponent.DraggingPlayer != null)
|
||||
{
|
||||
_appearance.SetData(dragged, TabletopItemVisuals.Scale, new Vector2(1.25f, 1.25f), appearance);
|
||||
_appearance.SetData(dragged, TabletopItemVisuals.DrawDepth, (int) DrawDepth.DrawDepth.Items + 1, appearance);
|
||||
}
|
||||
else
|
||||
{
|
||||
_appearance.SetData(dragged, TabletopItemVisuals.Scale, Vector2.One, appearance);
|
||||
_appearance.SetData(dragged, TabletopItemVisuals.DrawDepth, (int) DrawDepth.DrawDepth.Items, appearance);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class TabletopDraggableComponentState : ComponentState
|
||||
{
|
||||
public NetUserId? DraggingPlayer;
|
||||
|
||||
public TabletopDraggableComponentState(NetUserId? draggingPlayer)
|
||||
{
|
||||
DraggingPlayer = draggingPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class TabletopRequestTakeOut : EntityEventArgs
|
||||
{
|
||||
public NetEntity Entity;
|
||||
public NetEntity TableUid;
|
||||
}
|
||||
|
||||
#region Utility
|
||||
|
||||
/// <summary>
|
||||
/// Whether the table exists, and the player can interact with it.
|
||||
/// </summary>
|
||||
/// <param name="playerEntity">The player entity to check.</param>
|
||||
/// <param name="table">The table entity to check.</param>
|
||||
protected bool CanSeeTable(EntityUid playerEntity, EntityUid? table)
|
||||
{
|
||||
// Table may have been deleted, hence TryComp
|
||||
if (!TryComp(table, out MetaDataComponent? meta)
|
||||
|| meta.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| (meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _interactionSystem.InRangeUnobstructed(playerEntity, table.Value) && ActionBlockerSystem.CanInteract(playerEntity, table);
|
||||
}
|
||||
|
||||
protected bool CanDrag(EntityUid playerEntity, EntityUid target, [NotNullWhen(true)] out TabletopDraggableComponent? draggable)
|
||||
{
|
||||
if (!TryComp(target, out draggable))
|
||||
return false;
|
||||
|
||||
// CanSeeTable checks interaction action blockers. So no need to check them here.
|
||||
// If this ever changes, so that ghosts can spectate games, then the check needs to be moved here.
|
||||
|
||||
return TryComp(playerEntity, out HandsComponent? hands) && hands.Hands.Count > 0;
|
||||
}
|
||||
#endregion
|
||||
SubscribeAllEvent<TabletopMoveEvent>(OnTabletopMove);
|
||||
}
|
||||
|
||||
private void OnTabletopRequestTakeOut(TabletopRequestTakeOut msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession is not { } playerSession)
|
||||
return;
|
||||
|
||||
var table = GetEntity(msg.TableUid);
|
||||
|
||||
if (!TryComp(table, out TabletopGameComponent? tabletop) || tabletop.Session is not { } session)
|
||||
return;
|
||||
|
||||
if (!msg.Entity.IsValid())
|
||||
return;
|
||||
|
||||
var entity = GetEntity(msg.Entity);
|
||||
|
||||
if (!HasComp<TabletopHologramComponent>(entity))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("tabletop-error-remove-non-hologram"), table, args.SenderSession);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player is actually playing at this table.
|
||||
if (!session.Players.ContainsKey(playerSession))
|
||||
return;
|
||||
|
||||
// Find the entity, remove it from the session and set it's position to the tabletop.
|
||||
session.Entities.TryGetValue(entity, out var result);
|
||||
session.Entities.Remove(result);
|
||||
PredictedQueueDel(result);
|
||||
}
|
||||
|
||||
private void OnInteractUsing(Entity<TabletopGameComponent> ent, ref InteractUsingEvent args)
|
||||
{
|
||||
if (!_cfg.GetCVar(CCVars.GameTabletopPlace))
|
||||
return;
|
||||
|
||||
if (!HasComp<HandsComponent>(args.User))
|
||||
return;
|
||||
|
||||
if (ent.Comp.Session is not { } session)
|
||||
return;
|
||||
|
||||
if (!_hands.TryGetActiveItem(ent.Owner, out var handEnt))
|
||||
return;
|
||||
|
||||
if (!HasComp<ItemComponent>(handEnt))
|
||||
return;
|
||||
|
||||
var meta = MetaData(handEnt.Value);
|
||||
var protoId = meta.EntityPrototype?.ID;
|
||||
|
||||
var hologram = EntityManager.PredictedSpawn(protoId, session.Position.Offset(-1, 0));
|
||||
|
||||
// Make sure the entity can be dragged and can be removed, move it into the board game world and add it to the Entities hashmap.
|
||||
EnsureComp<TabletopDraggableComponent>(hologram);
|
||||
EnsureComp<TabletopHologramComponent>(hologram);
|
||||
session.Entities.Add(hologram);
|
||||
|
||||
_popup.PopupClient(Loc.GetString("tabletop-added-piece"), ent.Owner, args.User);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a verb that allows the player to start playing a tabletop game.
|
||||
/// </summary>
|
||||
private void AddPlayGameVerb(Entity<TabletopGameComponent> ent, ref GetVerbsEvent<ActivationVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
|
||||
if (!TryComp(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
var playVerb = new ActivationVerb()
|
||||
{
|
||||
Text = Loc.GetString("tabletop-verb-play-game"),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/die.svg.192dpi.png")),
|
||||
Act = () => OpenSessionFor(actor.PlayerSession, ent.Owner)
|
||||
};
|
||||
|
||||
args.Verbs.Add(playVerb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move an entity which is dragged by the user, but check if they are allowed to do so and to these coordinates.
|
||||
/// </summary>
|
||||
protected virtual void OnTabletopMove(TabletopMoveEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession is not { AttachedEntity: { } playerEntity })
|
||||
return;
|
||||
|
||||
var table = GetEntity(msg.TableUid);
|
||||
var moved = GetEntity(msg.MovedEntityUid);
|
||||
|
||||
if (!CanSeeTable(playerEntity, table) || !CanDrag(playerEntity, moved, out _))
|
||||
return;
|
||||
|
||||
// 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 = Comp<TransformComponent>(moved);
|
||||
_transforms.SetParent(moved, transform, _map.GetMapOrInvalid(transform.MapID));
|
||||
_transforms.SetLocalPosition(moved, msg.Coordinates.Position, transform);
|
||||
}
|
||||
|
||||
private void OnDraggingPlayerChanged(TabletopDraggingPlayerChangedEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var dragged = GetEntity(msg.DraggedEntityUid);
|
||||
|
||||
if (!TryComp(dragged, out TabletopDraggableComponent? draggableComponent))
|
||||
return;
|
||||
|
||||
draggableComponent.DraggingPlayer = msg.IsDragging ? args.SenderSession.UserId : null;
|
||||
Dirty(dragged, draggableComponent);
|
||||
|
||||
if (!TryComp(dragged, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
if (draggableComponent.DraggingPlayer != null)
|
||||
{
|
||||
_appearance.SetData(dragged, TabletopItemVisuals.Scale, new Vector2(1.25f, 1.25f), appearance);
|
||||
_appearance.SetData(dragged, TabletopItemVisuals.DrawDepth, (int)DrawDepth.DrawDepth.Items + 1, appearance);
|
||||
}
|
||||
else
|
||||
{
|
||||
_appearance.SetData(dragged, TabletopItemVisuals.Scale, Vector2.One, appearance);
|
||||
_appearance.SetData(dragged, TabletopItemVisuals.DrawDepth, (int)DrawDepth.DrawDepth.Items, appearance);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class TabletopDraggableComponentState(NetUserId? draggingPlayer) : ComponentState
|
||||
{
|
||||
public NetUserId? DraggingPlayer = draggingPlayer;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class TabletopRequestTakeOut : EntityEventArgs
|
||||
{
|
||||
public NetEntity Entity;
|
||||
public NetEntity TableUid;
|
||||
}
|
||||
|
||||
#region Utility
|
||||
|
||||
/// <summary>
|
||||
/// Whether the table exists, and the player can interact with it.
|
||||
/// </summary>
|
||||
/// <param name="playerEntity">The player entity to check.</param>
|
||||
/// <param name="table">The table entity to check.</param>
|
||||
protected bool CanSeeTable(EntityUid playerEntity, EntityUid? table)
|
||||
{
|
||||
// Table may have been deleted, hence TryComp.
|
||||
if (!TryComp(table, out MetaDataComponent? meta)
|
||||
|| meta.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| (meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _interaction.InRangeUnobstructed(playerEntity, table.Value) && _actionBlocker.CanInteract(playerEntity, table);
|
||||
}
|
||||
|
||||
protected bool CanDrag(EntityUid playerEntity, EntityUid target, [NotNullWhen(true)] out TabletopDraggableComponent? draggable)
|
||||
{
|
||||
if (!TryComp(target, out draggable))
|
||||
return false;
|
||||
|
||||
// CanSeeTable checks interaction action blockers. So no need to check them here.
|
||||
// If this ever changes, so that ghosts can spectate games, then the check needs to be moved here.
|
||||
return TryComp(playerEntity, out HandsComponent? hands) && hands.Hands.Count > 0;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
66
Content.Shared/Tabletop/TabletopBackgammonSetup.cs
Normal file
66
Content.Shared/Tabletop/TabletopBackgammonSetup.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Shared.Tabletop;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopBackgammonSetup : TabletopSetup
|
||||
{
|
||||
[DataField]
|
||||
public string WhitePiecePrototype { get; private set; } = "WhiteTabletopPiece";
|
||||
|
||||
[DataField]
|
||||
public string BlackPiecePrototype { get; private set; } = "BlackTabletopPiece";
|
||||
|
||||
public override void SetupTabletop(TabletopSession session, IEntityManager entityManager)
|
||||
{
|
||||
var board = entityManager.SpawnEntity(BoardPrototype, session.Position);
|
||||
|
||||
const float borderLengthX = 7.35f; //BORDER
|
||||
const float borderLengthY = 5.60f; //BORDER
|
||||
|
||||
const float boardDistanceX = 1.25f;
|
||||
const float pieceDistanceY = 0.80f;
|
||||
|
||||
float GetXPosition(float distanceFromSide, bool isLeftSide)
|
||||
{
|
||||
var pos = borderLengthX - distanceFromSide * boardDistanceX;
|
||||
return isLeftSide ? -pos : pos;
|
||||
}
|
||||
|
||||
float GetYPosition(float positionNumber, bool isTop)
|
||||
{
|
||||
var pos = borderLengthY - pieceDistanceY * positionNumber;
|
||||
return isTop ? pos : -pos;
|
||||
}
|
||||
|
||||
void AddPieces(
|
||||
float distanceFromSide,
|
||||
int numberOfPieces,
|
||||
bool isBlackPiece,
|
||||
bool isTop,
|
||||
bool isLeftSide)
|
||||
{
|
||||
for (var i = 0; i < numberOfPieces; i++)
|
||||
{
|
||||
session.Entities.Add(entityManager.SpawnEntity(isBlackPiece ? BlackPiecePrototype : WhitePiecePrototype, session.Position.Offset(GetXPosition(distanceFromSide, isLeftSide), GetYPosition(i, isTop))));
|
||||
}
|
||||
}
|
||||
|
||||
// top left
|
||||
AddPieces(0, 5, true, true, true);
|
||||
// top middle left
|
||||
AddPieces(4, 3, false, true, true);
|
||||
// top middle right
|
||||
AddPieces(5, 5, false, true, false);
|
||||
// top far right
|
||||
AddPieces(0, 2, true, true, false);
|
||||
// bottom left
|
||||
AddPieces(0, 5, false, false, true);
|
||||
// bottom middle left
|
||||
AddPieces(4, 3, true, false, true);
|
||||
// bottom middle right
|
||||
AddPieces(5, 5, true, false, false);
|
||||
// bottom far right
|
||||
AddPieces(0, 2, false, false, false);
|
||||
}
|
||||
}
|
||||
102
Content.Shared/Tabletop/TabletopCheckerSetup.cs
Normal file
102
Content.Shared/Tabletop/TabletopCheckerSetup.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared.Tabletop;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopCheckerSetup : TabletopSetup
|
||||
{
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string PrototypePieceWhite = default!;
|
||||
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string PrototypeCrownWhite = default!;
|
||||
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string PrototypePieceBlack = default!;
|
||||
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string PrototypeCrownBlack = default!;
|
||||
|
||||
public override void SetupTabletop(TabletopSession session, IEntityManager entityManager)
|
||||
{
|
||||
session.Entities.Add(
|
||||
entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(-1, 0))
|
||||
);
|
||||
|
||||
SpawnPieces(session, entityManager, session.Position.Offset(-4.5f, 3.5f));
|
||||
}
|
||||
|
||||
private void SpawnPieces(TabletopSession session, IEntityManager entityManager, MapCoordinates left)
|
||||
{
|
||||
static float GetOffset(float offset) => offset * 1f /* separation */;
|
||||
|
||||
var pieces = new EntityUid[42];
|
||||
var pieceIndex = 0;
|
||||
|
||||
// Pieces.
|
||||
for (var offsetY = 0; offsetY < 3; offsetY++)
|
||||
{
|
||||
var checker = offsetY % 2;
|
||||
|
||||
for (var offsetX = 0; offsetX < 8; offsetX += 2)
|
||||
{
|
||||
// Prevents an extra piece on the middle row.
|
||||
if (checker + offsetX > 8) continue;
|
||||
|
||||
pieces[pieceIndex] = entityManager.SpawnEntity(
|
||||
PrototypePieceBlack,
|
||||
left.Offset(GetOffset(offsetX + (1 - checker)), GetOffset(offsetY * -1))
|
||||
);
|
||||
pieces[pieceIndex] = entityManager.SpawnEntity(
|
||||
PrototypePieceWhite,
|
||||
left.Offset(GetOffset(offsetX + checker), GetOffset(offsetY - 7))
|
||||
);
|
||||
pieceIndex += 2;
|
||||
}
|
||||
}
|
||||
|
||||
const int numCrowns = 3;
|
||||
const float overlap = 0.25f;
|
||||
const float xOffset = 9f / 32;
|
||||
const float xOffsetBlack = 9 + xOffset;
|
||||
const float xOffsetWhite = 8 + xOffset;
|
||||
|
||||
// Crowns.
|
||||
for (var i = 0; i < numCrowns; i++)
|
||||
{
|
||||
var step = -(overlap * i);
|
||||
pieces[pieceIndex] = entityManager.SpawnEntity(
|
||||
PrototypeCrownBlack,
|
||||
left.Offset(GetOffset(xOffsetBlack), GetOffset(step))
|
||||
);
|
||||
pieces[pieceIndex + 1] = entityManager.SpawnEntity(
|
||||
PrototypeCrownWhite,
|
||||
left.Offset(GetOffset(xOffsetWhite), GetOffset(step))
|
||||
);
|
||||
pieceIndex += 2;
|
||||
}
|
||||
|
||||
// Spares.
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var step = -(overlap * (numCrowns + 2) + overlap * i);
|
||||
pieces[pieceIndex] = entityManager.SpawnEntity(
|
||||
PrototypePieceBlack,
|
||||
left.Offset(GetOffset(xOffsetBlack), GetOffset(step))
|
||||
);
|
||||
pieces[pieceIndex] = entityManager.SpawnEntity(
|
||||
PrototypePieceWhite,
|
||||
left.Offset(GetOffset(xOffsetWhite), GetOffset(step))
|
||||
);
|
||||
pieceIndex += 2;
|
||||
}
|
||||
|
||||
for (var i = 0; i < pieces.Length; i++)
|
||||
{
|
||||
session.Entities.Add(pieces[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Content.Shared/Tabletop/TabletopChessSetup.cs
Normal file
85
Content.Shared/Tabletop/TabletopChessSetup.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Tabletop;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopChessSetup : TabletopSetup
|
||||
{
|
||||
// TODO: Un-hardcode the rest of entity prototype IDs, probably.
|
||||
|
||||
public override void SetupTabletop(TabletopSession session, IEntityManager entityManager)
|
||||
{
|
||||
var chessboard = entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(-1, 0));
|
||||
|
||||
session.Entities.Add(chessboard);
|
||||
|
||||
SpawnPieces(session, entityManager, session.Position.Offset(-4.5f, 3.5f));
|
||||
}
|
||||
|
||||
private void SpawnPieces(TabletopSession session, IEntityManager entityManager, MapCoordinates topLeft, float separation = 1f)
|
||||
{
|
||||
var (mapId, x, y) = topLeft;
|
||||
|
||||
// Spawn all black pieces.
|
||||
SpawnPiecesRow(session, entityManager, "Black", topLeft, separation);
|
||||
SpawnPawns(session, entityManager, "Black", new MapCoordinates(x, y - separation, mapId), separation);
|
||||
|
||||
// Spawn all white pieces.
|
||||
SpawnPawns(session, entityManager, "White", new MapCoordinates(x, y - 6 * separation, mapId), separation);
|
||||
SpawnPiecesRow(session, entityManager, "White", new MapCoordinates(x, y - 7 * separation, mapId), separation);
|
||||
|
||||
// Extra queens.
|
||||
var tempQualifier = entityManager.SpawnEntity("BlackQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 3 * separation, mapId));
|
||||
session.Entities.Add(tempQualifier);
|
||||
var tempQualifier1 = entityManager.SpawnEntity("WhiteQueen", new MapCoordinates(x + 9 * separation + 9f / 32, y - 4 * separation, mapId));
|
||||
session.Entities.Add(tempQualifier1);
|
||||
}
|
||||
|
||||
// TODO: refactor to load FEN instead
|
||||
private void SpawnPiecesRow(TabletopSession session, IEntityManager entityManager, string color, MapCoordinates left, float separation = 1f)
|
||||
{
|
||||
const string piecesRow = "rnbqkbnr";
|
||||
|
||||
var (mapId, x, y) = left;
|
||||
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
switch (piecesRow[i])
|
||||
{
|
||||
case 'r':
|
||||
var tempQualifier = entityManager.SpawnEntity(color + "Rook", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier);
|
||||
break;
|
||||
case 'n':
|
||||
var tempQualifier1 = entityManager.SpawnEntity(color + "Knight", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier1);
|
||||
break;
|
||||
case 'b':
|
||||
var tempQualifier2 = entityManager.SpawnEntity(color + "Bishop", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier2);
|
||||
break;
|
||||
case 'q':
|
||||
var tempQualifier3 = entityManager.SpawnEntity(color + "Queen", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier3);
|
||||
break;
|
||||
case 'k':
|
||||
var tempQualifier4 = entityManager.SpawnEntity(color + "King", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactor to load FEN instead
|
||||
private void SpawnPawns(TabletopSession session, IEntityManager entityManager, string color, MapCoordinates left, float separation = 1f)
|
||||
{
|
||||
var (mapId, x, y) = left;
|
||||
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var tempQualifier = entityManager.SpawnEntity(color + "Pawn", new MapCoordinates(x + i * separation, y, mapId));
|
||||
session.Entities.Add(tempQualifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Content.Shared/Tabletop/TabletopEmptySetup.cs
Normal file
13
Content.Shared/Tabletop/TabletopEmptySetup.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Shared.Tabletop;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopEmptySetup : TabletopSetup
|
||||
{
|
||||
public override void SetupTabletop(TabletopSession session, IEntityManager entityManager)
|
||||
{
|
||||
var board = entityManager.SpawnEntity(BoardPrototype, session.Position.Offset(0, 0));
|
||||
session.Entities.Add(board);
|
||||
}
|
||||
}
|
||||
74
Content.Shared/Tabletop/TabletopParchisSetup.cs
Normal file
74
Content.Shared/Tabletop/TabletopParchisSetup.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared.Tabletop;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TabletopParchisSetup : TabletopSetup
|
||||
{
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string RedPiecePrototype = "RedTabletopPiece";
|
||||
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string GreenPiecePrototype = "GreenTabletopPiece";
|
||||
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string YellowPiecePrototype = "YellowTabletopPiece";
|
||||
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string BluePiecePrototype = "BlueTabletopPiece";
|
||||
|
||||
public override void SetupTabletop(TabletopSession session, IEntityManager entityManager)
|
||||
{
|
||||
var board = entityManager.SpawnEntity(BoardPrototype, session.Position);
|
||||
|
||||
const float x1 = 6.25f;
|
||||
const float x2 = 4.25f;
|
||||
|
||||
const float y1 = 6.25f;
|
||||
const float y2 = 4.25f;
|
||||
|
||||
var center = session.Position;
|
||||
|
||||
// Red pieces.
|
||||
var tempQualifier = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y1));
|
||||
session.Entities.Add(tempQualifier);
|
||||
var tempQualifier1 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x1, -y2));
|
||||
session.Entities.Add(tempQualifier1);
|
||||
var tempQualifier2 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y1));
|
||||
session.Entities.Add(tempQualifier2);
|
||||
var tempQualifier3 = entityManager.SpawnEntity(RedPiecePrototype, center.Offset(-x2, -y2));
|
||||
session.Entities.Add(tempQualifier3);
|
||||
|
||||
// Green pieces.
|
||||
var tempQualifier4 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y1));
|
||||
session.Entities.Add(tempQualifier4);
|
||||
var tempQualifier5 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x1, -y2));
|
||||
session.Entities.Add(tempQualifier5);
|
||||
var tempQualifier6 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y1));
|
||||
session.Entities.Add(tempQualifier6);
|
||||
var tempQualifier7 = entityManager.SpawnEntity(GreenPiecePrototype, center.Offset(x2, -y2));
|
||||
session.Entities.Add(tempQualifier7);
|
||||
|
||||
// Yellow pieces.
|
||||
var tempQualifier8 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y1));
|
||||
session.Entities.Add(tempQualifier8);
|
||||
var tempQualifier9 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x1, y2));
|
||||
session.Entities.Add(tempQualifier9);
|
||||
var tempQualifier10 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y1));
|
||||
session.Entities.Add(tempQualifier10);
|
||||
var tempQualifier11 = entityManager.SpawnEntity(YellowPiecePrototype, center.Offset(x2, y2));
|
||||
session.Entities.Add(tempQualifier11);
|
||||
|
||||
// Blue pieces.
|
||||
var tempQualifier12 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y1));
|
||||
session.Entities.Add(tempQualifier12);
|
||||
var tempQualifier13 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x1, y2));
|
||||
session.Entities.Add(tempQualifier13);
|
||||
var tempQualifier14 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y1));
|
||||
session.Entities.Add(tempQualifier14);
|
||||
var tempQualifier15 = entityManager.SpawnEntity(BluePiecePrototype, center.Offset(-x2, y2));
|
||||
session.Entities.Add(tempQualifier15);
|
||||
}
|
||||
}
|
||||
26
Content.Shared/Tabletop/TabletopSession.cs
Normal file
26
Content.Shared/Tabletop/TabletopSession.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using System.Numerics;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Shared.Tabletop;
|
||||
|
||||
/// <summary>
|
||||
/// A class for storing data about a running tabletop game.
|
||||
/// </summary>
|
||||
public sealed class TabletopSession(MapId tabletopMap, Vector2 position)
|
||||
{
|
||||
/// <summary>
|
||||
/// The center position of this session.
|
||||
/// </summary>
|
||||
public readonly MapCoordinates Position = new(position, tabletopMap);
|
||||
|
||||
/// <summary>
|
||||
/// The set of players currently playing this tabletop game.
|
||||
/// </summary>
|
||||
public readonly Dictionary<ICommonSession, TabletopSessionPlayerData> Players = [];
|
||||
|
||||
/// <summary>
|
||||
/// All entities bound to this session. If you create an entity for this session, you have to add it here.
|
||||
/// </summary>
|
||||
public readonly HashSet<EntityUid> Entities = [];
|
||||
}
|
||||
9
Content.Shared/Tabletop/TabletopSessionPlayerData.cs
Normal file
9
Content.Shared/Tabletop/TabletopSessionPlayerData.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace Content.Shared.Tabletop;
|
||||
|
||||
/// <summary>
|
||||
/// A class that stores per-player data for tabletops.
|
||||
/// </summary>
|
||||
public sealed class TabletopSessionPlayerData
|
||||
{
|
||||
public EntityUid Camera { get; set; }
|
||||
}
|
||||
19
Content.Shared/Tabletop/TabletopSetup.cs
Normal file
19
Content.Shared/Tabletop/TabletopSetup.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared.Tabletop;
|
||||
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract partial class TabletopSetup
|
||||
{
|
||||
/// <summary>
|
||||
/// Method for setting up a tabletop. Use this to spawn the board and pieces, etc.
|
||||
/// Make sure you add every entity you create to the Entities hashset in the session.
|
||||
/// </summary>
|
||||
/// <param name="session">Tabletop session to set up. You'll want to grab the tabletop center position here for spawning entities.</param>
|
||||
/// <param name="entityManager">Dependency that can be used for spawning entities.</param>
|
||||
public abstract void SetupTabletop(TabletopSession session, IEntityManager entityManager);
|
||||
|
||||
[DataField]
|
||||
public EntProtoId BoardPrototype;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue