ECS Arcade Machines (#16791)

This commit is contained in:
TemporalOroboros
2023-06-15 04:25:25 -07:00
committed by GitHub
parent b72ab3b00c
commit bc3f42d822
23 changed files with 2116 additions and 1521 deletions

View File

@@ -0,0 +1,256 @@
using Content.Shared.Arcade;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Arcade.BlockGame;
public sealed partial class BlockGame
{
// note: field is 10(0 -> 9) wide and 20(0 -> 19) high
/// <summary>
/// Whether the given position is above the bottom of the playfield.
/// </summary>
private bool LowerBoundCheck(Vector2i position)
{
return position.Y < 20;
}
/// <summary>
/// Whether the given position is horizontally positioned within the playfield.
/// </summary>
private bool BorderCheck(Vector2i position)
{
return position.X >= 0 && position.X < 10;
}
/// <summary>
/// Whether the given position is currently occupied by a piece.
/// Yes this is on O(n) collision check, it works well enough.
/// </summary>
private bool ClearCheck(Vector2i position)
{
return _field.All(block => !position.Equals(block.Position));
}
/// <summary>
/// Whether a block can be dropped into the given position.
/// </summary>
private bool DropCheck(Vector2i position)
{
return LowerBoundCheck(position) && ClearCheck(position);
}
/// <summary>
/// Whether a block can be moved horizontally into the given position.
/// </summary>
private bool MoveCheck(Vector2i position)
{
return BorderCheck(position) && ClearCheck(position);
}
/// <summary>
/// Whether a block can be rotated into the given position.
/// </summary>
private bool RotateCheck(Vector2i position)
{
return BorderCheck(position) && LowerBoundCheck(position) && ClearCheck(position);
}
/// <summary>
/// The set of blocks that have landed in the field.
/// </summary>
private readonly List<BlockGameBlock> _field = new();
/// <summary>
/// The current pool of pickable pieces.
/// Refreshed when a piece is requested while empty.
/// Ensures that the player is given an even spread of pieces by making picked pieces unpickable until the rest are picked.
/// </summary>
private List<BlockGamePieceType> _blockGamePiecesBuffer = new();
/// <summary>
/// Gets a random piece from the pool of pickable pieces. (<see cref="_blockGamePiecesBuffer"/>)
/// </summary>
private BlockGamePiece GetRandomBlockGamePiece(IRobustRandom random)
{
if (_blockGamePiecesBuffer.Count == 0)
{
_blockGamePiecesBuffer = _allBlockGamePieces.ToList();
}
var chosenPiece = random.Pick(_blockGamePiecesBuffer);
_blockGamePiecesBuffer.Remove(chosenPiece);
return BlockGamePiece.GetPiece(chosenPiece);
}
/// <summary>
/// The piece that is currently falling and controllable by the player.
/// </summary>
private BlockGamePiece CurrentPiece
{
get => _internalCurrentPiece;
set
{
_internalCurrentPiece = value;
UpdateFieldUI();
}
}
private BlockGamePiece _internalCurrentPiece = default!;
/// <summary>
/// The position of the falling piece.
/// </summary>
private Vector2i _currentPiecePosition;
/// <summary>
/// The rotation of the falling piece.
/// </summary>
private BlockGamePieceRotation _currentRotation;
/// <summary>
/// The amount of time (in seconds) between piece steps.
/// Decreased by a constant amount per level.
/// Decreased heavily by soft dropping the current piece (holding down).
/// </summary>
private float Speed => Math.Max(0.03f, (_softDropPressed ? SoftDropModifier : 1f) - 0.03f * Level);
/// <summary>
/// The base amount of time between piece steps while softdropping.
/// </summary>
private const float SoftDropModifier = 0.1f;
/// <summary>
/// Attempts to rotate the falling piece to a new rotation.
/// </summary>
private void TrySetRotation(BlockGamePieceRotation rotation)
{
if (!_running)
return;
if (!CurrentPiece.CanSpin)
return;
if (!CurrentPiece.Positions(_currentPiecePosition, rotation)
.All(RotateCheck))
return;
_currentRotation = rotation;
UpdateFieldUI();
}
/// <summary>
/// The next piece that will be dispensed.
/// </summary>
private BlockGamePiece NextPiece
{
get => _internalNextPiece;
set
{
_internalNextPiece = value;
SendNextPieceUpdate();
}
}
private BlockGamePiece _internalNextPiece = default!;
/// <summary>
/// The piece the player has chosen to hold in reserve.
/// </summary>
private BlockGamePiece? HeldPiece
{
get => _internalHeldPiece;
set
{
_internalHeldPiece = value;
SendHoldPieceUpdate();
}
}
private BlockGamePiece? _internalHeldPiece = null;
/// <summary>
/// Prevents the player from holding the currently falling piece if true.
/// Set true when a piece is held and set false when a new piece is created.
/// Exists to prevent the player from swapping between two pieces forever and never actually letting the block fall.
/// </summary>
private bool _holdBlock = false;
/// <summary>
/// The number of lines that have been cleared in the current level.
/// Automatically advances the game to the next level if enough lines are cleared.
/// </summary>
private int ClearedLines
{
get => _clearedLines;
set
{
_clearedLines = value;
if (_clearedLines < LevelRequirement)
return;
_clearedLines -= LevelRequirement;
Level++;
}
}
private int _clearedLines = 0;
/// <summary>
/// The number of lines that must be cleared to advance to the next level.
/// </summary>
private int LevelRequirement => Math.Min(100, Math.Max(Level * 10 - 50, 10));
/// <summary>
/// The current level of the game.
/// Effects the movement speed of the active piece.
/// </summary>
private int Level
{
get => _internalLevel;
set
{
if (_internalLevel == value)
return;
_internalLevel = value;
SendLevelUpdate();
}
}
private int _internalLevel = 0;
/// <summary>
/// The total number of points accumulated in the current game.
/// </summary>
private int Points
{
get => _internalPoints;
set
{
if (_internalPoints == value)
return;
_internalPoints = value;
SendPointsUpdate();
}
}
private int _internalPoints = 0;
/// <summary>
/// Setter for the setter for the number of points accumulated in the current game.
/// </summary>
private void AddPoints(int amount)
{
if (amount == 0)
return;
Points += amount;
}
/// <summary>
/// Where the current game has placed amongst the leaderboard.
/// </summary>
private ArcadeSystem.HighScorePlacement? _highScorePlacement = null;
}

View File

@@ -0,0 +1,238 @@
using Content.Shared.Arcade;
using System.Linq;
namespace Content.Server.Arcade.BlockGame;
public sealed partial class BlockGame
{
/// <summary>
/// The set of types of game pieces that exist.
/// Used as templates when creating pieces for the game.
/// </summary>
private readonly BlockGamePieceType[] _allBlockGamePieces;
/// <summary>
/// The set of types of game pieces that exist.
/// Used to generate the templates used when creating pieces for the game.
/// </summary>
private enum BlockGamePieceType
{
I,
L,
LInverted,
S,
SInverted,
T,
O
}
/// <summary>
/// The set of possible rotations for the game pieces.
/// </summary>
private enum BlockGamePieceRotation
{
North,
East,
South,
West
}
/// <summary>
/// A static extension for the rotations that allows rotating through the possible rotations.
/// </summary>
private static BlockGamePieceRotation Next(BlockGamePieceRotation rotation, bool inverted)
{
return rotation switch
{
BlockGamePieceRotation.North => inverted ? BlockGamePieceRotation.West : BlockGamePieceRotation.East,
BlockGamePieceRotation.East => inverted ? BlockGamePieceRotation.North : BlockGamePieceRotation.South,
BlockGamePieceRotation.South => inverted ? BlockGamePieceRotation.East : BlockGamePieceRotation.West,
BlockGamePieceRotation.West => inverted ? BlockGamePieceRotation.South : BlockGamePieceRotation.North,
_ => throw new ArgumentOutOfRangeException(nameof(rotation), rotation, null)
};
}
/// <summary>
/// A static extension for the rotations that allows rotating through the possible rotations.
/// </summary>
private struct BlockGamePiece
{
/// <summary>
/// Where all of the blocks that make up this piece are located relative to the origin of the piece.
/// </summary>
public Vector2i[] Offsets;
/// <summary>
/// The color of all of the blocks that make up this piece.
/// </summary>
private BlockGameBlock.BlockGameBlockColor _gameBlockColor;
/// <summary>
/// Whether or not the block should be able to rotate about its origin.
/// </summary>
public bool CanSpin;
/// <summary>
/// Generates a list of the positions of each block comprising this game piece in worldspace.
/// </summary>
/// <param name="center">The position of the game piece in worldspace.</param>
/// <param name="rotation">The rotation of the game piece in worldspace.</param>
public readonly Vector2i[] Positions(Vector2i center, BlockGamePieceRotation rotation)
{
return RotatedOffsets(rotation).Select(v => center + v).ToArray();
}
/// <summary>
/// Gets the relative position of each block comprising this piece given a rotation.
/// </summary>
/// <param name="rotation">The rotation to be applied to the local position of the blocks in this piece.</param>
private readonly Vector2i[] RotatedOffsets(BlockGamePieceRotation rotation)
{
var rotatedOffsets = (Vector2i[]) Offsets.Clone();
//until i find a better algo
var amount = rotation switch
{
BlockGamePieceRotation.North => 0,
BlockGamePieceRotation.East => 1,
BlockGamePieceRotation.South => 2,
BlockGamePieceRotation.West => 3,
_ => 0
};
for (var i = 0; i < amount; i++)
{
for (var j = 0; j < rotatedOffsets.Length; j++)
{
rotatedOffsets[j] = rotatedOffsets[j].Rotate90DegreesAsOffset();
}
}
return rotatedOffsets;
}
/// <summary>
/// Gets a list of all of the blocks comprising this piece in worldspace.
/// </summary>
/// <param name="center">The position of the game piece in worldspace.</param>
/// <param name="rotation">The rotation of the game piece in worldspace.</param>
public readonly BlockGameBlock[] Blocks(Vector2i center, BlockGamePieceRotation rotation)
{
var positions = Positions(center, rotation);
var result = new BlockGameBlock[positions.Length];
var i = 0;
foreach (var position in positions)
{
result[i++] = position.ToBlockGameBlock(_gameBlockColor);
}
return result;
}
/// <summary>
/// Gets a list of all of the blocks comprising this piece in worldspace.
/// Used to generate the held piece/next piece preview images.
/// </summary>
public readonly BlockGameBlock[] BlocksForPreview()
{
var xOffset = 0;
var yOffset = 0;
foreach (var offset in Offsets)
{
if (offset.X < xOffset)
xOffset = offset.X;
if (offset.Y < yOffset)
yOffset = offset.Y;
}
return Blocks(new Vector2i(-xOffset, -yOffset), BlockGamePieceRotation.North);
}
/// <summary>
/// Generates a game piece for a given type of game piece.
/// See <see cref="BlockGamePieceType"/> for the available options.
/// </summary>
/// <param name="type">The type of game piece to generate.</param>
public static BlockGamePiece GetPiece(BlockGamePieceType type)
{
//switch statement, hardcoded offsets
return type switch
{
BlockGamePieceType.I => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(0, 1), new Vector2i(0, 2),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.LightBlue,
CanSpin = true
},
BlockGamePieceType.L => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(0, 1), new Vector2i(1, 1),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Orange,
CanSpin = true
},
BlockGamePieceType.LInverted => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(-1, 1),
new Vector2i(0, 1),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Blue,
CanSpin = true
},
BlockGamePieceType.S => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1), new Vector2i(1, -1), new Vector2i(-1, 0),
new Vector2i(0, 0),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Green,
CanSpin = true
},
BlockGamePieceType.SInverted => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(-1, -1), new Vector2i(0, -1), new Vector2i(0, 0),
new Vector2i(1, 0),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Red,
CanSpin = true
},
BlockGamePieceType.T => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1),
new Vector2i(-1, 0), new Vector2i(0, 0), new Vector2i(1, 0),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Purple,
CanSpin = true
},
BlockGamePieceType.O => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, -1), new Vector2i(1, -1), new Vector2i(0, 0),
new Vector2i(1, 0),
},
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Yellow,
CanSpin = false
},
_ => new BlockGamePiece
{
Offsets = new[]
{
new Vector2i(0, 0)
}
},
};
}
}
}

View File

@@ -0,0 +1,364 @@
using Content.Shared.Arcade;
using Robust.Server.Player;
using System.Linq;
namespace Content.Server.Arcade.BlockGame;
public sealed partial class BlockGame
{
/// <summary>
/// How often to check the currently pressed inputs for whether to move the active piece horizontally.
/// </summary>
private const float PressCheckSpeed = 0.08f;
/// <summary>
/// Whether the left button is pressed.
/// Moves the active piece left if true.
/// </summary>
private bool _leftPressed = false;
/// <summary>
/// How long the left button has been pressed.
/// </summary>
private float _accumulatedLeftPressTime = 0f;
/// <summary>
/// Whether the right button is pressed.
/// Moves the active piece right if true.
/// </summary>
private bool _rightPressed = false;
/// <summary>
/// How long the right button has been pressed.
/// </summary>
private float _accumulatedRightPressTime = 0f;
/// <summary>
/// Whether the down button is pressed.
/// Speeds up how quickly the active piece falls if true.
/// </summary>
private bool _softDropPressed = false;
/// <summary>
/// Handles user input.
/// </summary>
/// <param name="action">The action to current player has prompted.</param>
public void ProcessInput(BlockGamePlayerAction action)
{
if (_running)
{
switch (action)
{
case BlockGamePlayerAction.StartLeft:
_leftPressed = true;
break;
case BlockGamePlayerAction.StartRight:
_rightPressed = true;
break;
case BlockGamePlayerAction.Rotate:
TrySetRotation(Next(_currentRotation, false));
break;
case BlockGamePlayerAction.CounterRotate:
TrySetRotation(Next(_currentRotation, true));
break;
case BlockGamePlayerAction.SoftdropStart:
_softDropPressed = true;
if (_accumulatedFieldFrameTime > Speed)
_accumulatedFieldFrameTime = Speed; //to prevent jumps
break;
case BlockGamePlayerAction.Harddrop:
PerformHarddrop();
break;
case BlockGamePlayerAction.Hold:
HoldPiece();
break;
}
}
switch (action)
{
case BlockGamePlayerAction.EndLeft:
_leftPressed = false;
break;
case BlockGamePlayerAction.EndRight:
_rightPressed = false;
break;
case BlockGamePlayerAction.SoftdropEnd:
_softDropPressed = false;
break;
case BlockGamePlayerAction.Pause:
_running = false;
SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, Started));
break;
case BlockGamePlayerAction.Unpause:
if (!_gameOver && Started)
{
_running = true;
SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game));
}
break;
case BlockGamePlayerAction.ShowHighscores:
_running = false;
SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Highscores, Started));
break;
}
}
/// <summary>
/// Handle moving the active game piece in response to user input.
/// </summary>
/// <param name="frameTime">The amount of time the current game tick covers.</param>
private void InputTick(float frameTime)
{
var anythingChanged = false;
if (_leftPressed)
{
_accumulatedLeftPressTime += frameTime;
while (_accumulatedLeftPressTime >= PressCheckSpeed)
{
if (CurrentPiece.Positions(_currentPiecePosition.AddToX(-1), _currentRotation)
.All(MoveCheck))
{
_currentPiecePosition = _currentPiecePosition.AddToX(-1);
anythingChanged = true;
}
_accumulatedLeftPressTime -= PressCheckSpeed;
}
}
if (_rightPressed)
{
_accumulatedRightPressTime += frameTime;
while (_accumulatedRightPressTime >= PressCheckSpeed)
{
if (CurrentPiece.Positions(_currentPiecePosition.AddToX(1), _currentRotation)
.All(MoveCheck))
{
_currentPiecePosition = _currentPiecePosition.AddToX(1);
anythingChanged = true;
}
_accumulatedRightPressTime -= PressCheckSpeed;
}
}
if (anythingChanged)
UpdateFieldUI();
}
/// <summary>
/// Handles sending a message to all players/spectators.
/// </summary>
/// <param name="message">The message to broadcase to all players/spectators.</param>
private void SendMessage(BoundUserInterfaceMessage message)
{
if (_uiSystem.TryGetUi(_owner, BlockGameUiKey.Key, out var bui))
_uiSystem.SendUiMessage(bui, message);
}
/// <summary>
/// Handles sending a message to a specific player/spectator.
/// </summary>
/// <param name="message">The message to send to a specific player/spectator.</param>
/// <param name="session">The target recipient.</param>
private void SendMessage(BoundUserInterfaceMessage message, IPlayerSession session)
{
if (_uiSystem.TryGetUi(_owner, BlockGameUiKey.Key, out var bui))
_uiSystem.TrySendUiMessage(bui, message, session);
}
/// <summary>
/// Handles sending the current state of the game to a player that has just opened the UI.
/// </summary>
/// <param name="session">The target recipient.</param>
public void UpdateNewPlayerUI(IPlayerSession session)
{
if (_gameOver)
{
SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement), session);
return;
}
if (Paused)
SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, Started), session);
else
SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game, Started), session);
FullUpdate(session);
}
/// <summary>
/// Handles broadcasting the full player-visible game state to everyone who can see the game.
/// </summary>
private void FullUpdate()
{
UpdateFieldUI();
SendHoldPieceUpdate();
SendNextPieceUpdate();
SendLevelUpdate();
SendPointsUpdate();
SendHighscoreUpdate();
}
/// <summary>
/// Handles broadcasting the full player-visible game state to a specific player/spectator.
/// </summary>
/// <param name="session">The target recipient.</param>
private void FullUpdate(IPlayerSession session)
{
UpdateFieldUI(session);
SendNextPieceUpdate(session);
SendHoldPieceUpdate(session);
SendLevelUpdate(session);
SendPointsUpdate(session);
SendHighscoreUpdate(session);
}
/// <summary>
/// Handles broadcasting the current location of all of the blocks in the playfield + the active piece to all spectators.
/// </summary>
public void UpdateFieldUI()
{
if (!Started)
return;
var computedField = ComputeField();
SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField));
}
/// <summary>
/// Handles broadcasting the current location of all of the blocks in the playfield + the active piece to a specific player/spectator.
/// </summary>
/// <param name="session">The target recipient.</param>
public void UpdateFieldUI(IPlayerSession session)
{
if (!Started)
return;
var computedField = ComputeField();
SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField), session);
}
/// <summary>
/// Generates the set of blocks to send to viewers.
/// </summary>
public List<BlockGameBlock> ComputeField()
{
var result = new List<BlockGameBlock>();
result.AddRange(_field);
result.AddRange(CurrentPiece.Blocks(_currentPiecePosition, _currentRotation));
var dropGhostPosition = _currentPiecePosition;
while (CurrentPiece.Positions(dropGhostPosition.AddToY(1), _currentRotation)
.All(DropCheck))
{
dropGhostPosition = dropGhostPosition.AddToY(1);
}
if (dropGhostPosition != _currentPiecePosition)
{
var blox = CurrentPiece.Blocks(dropGhostPosition, _currentRotation);
for (var i = 0; i < blox.Length; i++)
{
result.Add(new BlockGameBlock(blox[i].Position, BlockGameBlock.ToGhostBlockColor(blox[i].GameBlockColor)));
}
}
return result;
}
/// <summary>
/// Broadcasts the state of the next queued piece to all viewers.
/// </summary>
private void SendNextPieceUpdate()
{
SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(NextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock));
}
/// <summary>
/// Broadcasts the state of the next queued piece to a specific viewer.
/// </summary>
/// <param name="session">The target recipient.</param>
private void SendNextPieceUpdate(IPlayerSession session)
{
SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(NextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock), session);
}
/// <summary>
/// Broadcasts the state of the currently held piece to all viewers.
/// </summary>
private void SendHoldPieceUpdate()
{
if (HeldPiece.HasValue)
SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(HeldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock));
else
SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(Array.Empty<BlockGameBlock>(), BlockGameMessages.BlockGameVisualType.HoldBlock));
}
/// <summary>
/// Broadcasts the state of the currently held piece to a specific viewer.
/// </summary>
/// <param name="session">The target recipient.</param>
private void SendHoldPieceUpdate(IPlayerSession session)
{
if (HeldPiece.HasValue)
SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(HeldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock), session);
else
SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(Array.Empty<BlockGameBlock>(), BlockGameMessages.BlockGameVisualType.HoldBlock), session);
}
/// <summary>
/// Broadcasts the current game level to all viewers.
/// </summary>
private void SendLevelUpdate()
{
SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level));
}
/// <summary>
/// Broadcasts the current game level to a specific viewer.
/// </summary>
/// <param name="session">The target recipient.</param>
private void SendLevelUpdate(IPlayerSession session)
{
SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level), session);
}
/// <summary>
/// Broadcasts the current game score to all viewers.
/// </summary>
private void SendPointsUpdate()
{
SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points));
}
/// <summary>
/// Broadcasts the current game score to a specific viewer.
/// </summary>
/// <param name="session">The target recipient.</param>
private void SendPointsUpdate(IPlayerSession session)
{
SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points), session);
}
/// <summary>
/// Broadcasts the current game high score positions to all viewers.
/// </summary>
private void SendHighscoreUpdate()
{
SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(_arcadeSystem.GetLocalHighscores(), _arcadeSystem.GetGlobalHighscores()));
}
/// <summary>
/// Broadcasts the current game high score positions to a specific viewer.
/// </summary>
/// <param name="session">The target recipient.</param>
private void SendHighscoreUpdate(IPlayerSession session)
{
SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(_arcadeSystem.GetLocalHighscores(), _arcadeSystem.GetGlobalHighscores()), session);
}
}

View File

@@ -0,0 +1,303 @@
using Content.Shared.Arcade;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Arcade.BlockGame;
public sealed partial class BlockGame
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private readonly ArcadeSystem _arcadeSystem = default!;
private readonly UserInterfaceSystem _uiSystem = default!;
/// <summary>
/// What entity is currently hosting this game of NT-BG.
/// </summary>
private readonly EntityUid _owner = default!;
/// <summary>
/// Whether the game has been started.
/// </summary>
public bool Started { get; private set; } = false;
/// <summary>
/// Whether the game is currently running (not paused).
/// </summary>
private bool _running = false;
/// <summary>
/// Whether the game should not currently be running.
/// </summary>
private bool Paused => !(Started && _running);
/// <summary>
/// Whether the game has finished.
/// </summary>
private bool _gameOver = false;
/// <summary>
/// Whether the game should have finished given the current game state.
/// </summary>
private bool IsGameOver => _field.Any(block => block.Position.Y == 0);
public BlockGame(EntityUid owner)
{
IoCManager.InjectDependencies(this);
_arcadeSystem = _entityManager.System<ArcadeSystem>();
_uiSystem = _entityManager.System<UserInterfaceSystem>();
_owner = owner;
_allBlockGamePieces = (BlockGamePieceType[]) Enum.GetValues(typeof(BlockGamePieceType));
_internalNextPiece = GetRandomBlockGamePiece(_random);
InitializeNewBlock();
}
/// <summary>
/// Starts the game. Including relaying this info to everyone watching.
/// </summary>
public void StartGame()
{
SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game));
FullUpdate();
Started = true;
_running = true;
_gameOver = false;
}
/// <summary>
/// Handles ending the game and updating the high scores.
/// </summary>
private void InvokeGameover()
{
_running = false;
_gameOver = true;
if (_entityManager.TryGetComponent<BlockGameArcadeComponent>(_owner, out var cabinet)
&& _entityManager.TryGetComponent<MetaDataComponent>(cabinet.Player?.AttachedEntity, out var meta))
{
_highScorePlacement = _arcadeSystem.RegisterHighScore(meta.EntityName, Points);
SendHighscoreUpdate();
}
SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement));
}
/// <summary>
/// Handle the game simulation and user input.
/// </summary>
/// <param name="frameTime">The amount of time the current game tick covers.</param>
public void GameTick(float frameTime)
{
if (!_running)
return;
InputTick(frameTime);
FieldTick(frameTime);
}
/// <summary>
/// The amount of time that has passed since the active piece last moved vertically,
/// </summary>
private float _accumulatedFieldFrameTime;
/// <summary>
/// Handles timing the movements of the active game piece.
/// </summary>
/// <param name="frameTime">The amount of time the current game tick covers.</param>
private void FieldTick(float frameTime)
{
_accumulatedFieldFrameTime += frameTime;
// Speed goes negative sometimes. uhhhh max() it I guess!!!
var checkTime = Math.Max(0.03f, Speed);
while (_accumulatedFieldFrameTime >= checkTime)
{
if (_softDropPressed)
AddPoints(1);
InternalFieldTick();
_accumulatedFieldFrameTime -= checkTime;
}
}
/// <summary>
/// Handles the active game piece moving down.
/// Also triggers scanning for cleared lines.
/// </summary>
private void InternalFieldTick()
{
if (CurrentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation)
.All(DropCheck))
{
_currentPiecePosition = _currentPiecePosition.AddToY(1);
}
else
{
var blocks = CurrentPiece.Blocks(_currentPiecePosition, _currentRotation);
_field.AddRange(blocks);
//check loose conditions
if (IsGameOver)
{
InvokeGameover();
return;
}
InitializeNewBlock();
}
CheckField();
UpdateFieldUI();
}
/// <summary>
/// Handles scanning for cleared lines and accumulating points.
/// </summary>
private void CheckField()
{
var pointsToAdd = 0;
var consecutiveLines = 0;
var clearedLines = 0;
for (var y = 0; y < 20; y++)
{
if (CheckLine(y))
{
//line was cleared
y--;
consecutiveLines++;
clearedLines++;
}
else if (consecutiveLines != 0)
{
var mod = consecutiveLines switch
{
1 => 40,
2 => 100,
3 => 300,
4 => 1200,
_ => 0
};
pointsToAdd += mod * (Level + 1);
}
}
ClearedLines += clearedLines;
AddPoints(pointsToAdd);
}
/// <summary>
/// Returns whether the line at the given position is full.
/// Clears the line if it was full and moves the above lines down.
/// </summary>
/// <param name="y">The position of the line to check.</param>
private bool CheckLine(int y)
{
for (var x = 0; x < 10; x++)
{
if (!_field.Any(b => b.Position.X == x && b.Position.Y == y))
return false;
}
//clear line
_field.RemoveAll(b => b.Position.Y == y);
//move everything down
FillLine(y);
return true;
}
/// <summary>
/// Moves all of the lines above the given line down by one.
/// Used to fill in cleared lines.
/// </summary>
/// <param name="y">The position of the line above which to drop the lines.</param>
private void FillLine(int y)
{
for (var c_y = y; c_y > 0; c_y--)
{
for (var j = 0; j < _field.Count; j++)
{
if (_field[j].Position.Y != c_y - 1)
continue;
_field[j] = new BlockGameBlock(_field[j].Position.AddToY(1), _field[j].GameBlockColor);
}
}
}
/// <summary>
/// Generates a new active piece from the previewed next piece.
/// Repopulates the previewed next piece with a piece from the pool of possible next pieces.
/// </summary>
private void InitializeNewBlock()
{
InitializeNewBlock(NextPiece);
NextPiece = GetRandomBlockGamePiece(_random);
_holdBlock = false;
SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(NextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock));
}
/// <summary>
/// Generates a new active piece from the previewed next piece.
/// </summary>
/// <param name="piece">The piece to set as the active piece.</param>
private void InitializeNewBlock(BlockGamePiece piece)
{
_currentPiecePosition = new Vector2i(5, 0);
_currentRotation = BlockGamePieceRotation.North;
CurrentPiece = piece;
UpdateFieldUI();
}
/// <summary>
/// Buffers the currently active piece.
/// Replaces the active piece with either the previously held piece or the previewed next piece as necessary.
/// </summary>
private void HoldPiece()
{
if (!_running)
return;
if (_holdBlock)
return;
var tempHeld = HeldPiece;
HeldPiece = CurrentPiece;
_holdBlock = true;
if (!tempHeld.HasValue)
{
InitializeNewBlock();
return;
}
InitializeNewBlock(tempHeld.Value);
}
/// <summary>
/// Immediately drops the currently active piece the remaining distance.
/// </summary>
private void PerformHarddrop()
{
var spacesDropped = 0;
while (CurrentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation)
.All(DropCheck))
{
_currentPiecePosition = _currentPiecePosition.AddToY(1);
spacesDropped++;
}
AddPoints(spacesDropped * 2);
InternalFieldTick();
}
}

View File

@@ -0,0 +1,22 @@
using Robust.Server.Player;
namespace Content.Server.Arcade.BlockGame;
[RegisterComponent]
public sealed class BlockGameArcadeComponent : Component
{
/// <summary>
/// The currently active session of NT-BG.
/// </summary>
public BlockGame? Game = null;
/// <summary>
/// The player currently playing the active session of NT-BG.
/// </summary>
public IPlayerSession? Player = null;
/// <summary>
/// The players currently viewing (but not playing) the active session of NT-BG.
/// </summary>
public readonly List<IPlayerSession> Spectators = new();
}

View File

@@ -0,0 +1,123 @@
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.Arcade;
using Robust.Server.GameObjects;
using Robust.Server.Player;
namespace Content.Server.Arcade.BlockGame;
public sealed class BlockGameArcadeSystem : EntitySystem
{
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BlockGameArcadeComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<BlockGameArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpen);
SubscribeLocalEvent<BlockGameArcadeComponent, BoundUIClosedEvent>(OnAfterUiClose);
SubscribeLocalEvent<BlockGameArcadeComponent, PowerChangedEvent>(OnBlockPowerChanged);
SubscribeLocalEvent<BlockGameArcadeComponent, BlockGameMessages.BlockGamePlayerActionMessage>(OnPlayerAction);
}
public override void Update(float frameTime)
{
var query = EntityManager.EntityQueryEnumerator<BlockGameArcadeComponent>();
while (query.MoveNext(out var _, out var blockGame))
{
blockGame.Game?.GameTick(frameTime);
}
}
private void UpdatePlayerStatus(EntityUid uid, IPlayerSession session, BoundUserInterface? bui = null, BlockGameArcadeComponent? blockGame = null)
{
if (!Resolve(uid, ref blockGame))
return;
if (bui == null && !_uiSystem.TryGetUi(uid, BlockGameUiKey.Key, out bui))
return;
_uiSystem.TrySendUiMessage(bui, new BlockGameMessages.BlockGameUserStatusMessage(blockGame.Player == session), session);
}
private void OnComponentInit(EntityUid uid, BlockGameArcadeComponent component, ComponentInit args)
{
component.Game = new(uid);
}
private void OnAfterUIOpen(EntityUid uid, BlockGameArcadeComponent component, AfterActivatableUIOpenEvent args)
{
if (!TryComp<ActorComponent>(args.User, out var actor))
return;
if (!_uiSystem.TryGetUi(uid, BlockGameUiKey.Key, out var bui))
return;
var session = actor.PlayerSession;
if (!bui.SubscribedSessions.Contains(session))
return;
if (component.Player == null)
component.Player = session;
else
component.Spectators.Add(session);
UpdatePlayerStatus(uid, session, bui, component);
component.Game?.UpdateNewPlayerUI(session);
}
private void OnAfterUiClose(EntityUid uid, BlockGameArcadeComponent component, BoundUIClosedEvent args)
{
if (args.Session is not IPlayerSession session)
return;
if (component.Player != session)
{
component.Spectators.Remove(session);
UpdatePlayerStatus(uid, session, blockGame: component);
return;
}
var temp = component.Player;
if (component.Spectators.Count > 0)
{
component.Player = component.Spectators[0];
component.Spectators.Remove(component.Player);
UpdatePlayerStatus(uid, component.Player, blockGame: component);
}
else
component.Player = null;
UpdatePlayerStatus(uid, temp, blockGame: component);
}
private void OnBlockPowerChanged(EntityUid uid, BlockGameArcadeComponent component, ref PowerChangedEvent args)
{
if (args.Powered)
return;
if (_uiSystem.TryGetUi(uid, BlockGameUiKey.Key, out var bui))
_uiSystem.CloseAll(bui);
component.Player = null;
component.Spectators.Clear();
}
private void OnPlayerAction(EntityUid uid, BlockGameArcadeComponent component, BlockGameMessages.BlockGamePlayerActionMessage msg)
{
if (component.Game == null)
return;
if (!BlockGameUiKey.Key.Equals(msg.UiKey))
return;
if (msg.Session != component.Player)
return;
if (msg.PlayerAction == BlockGamePlayerAction.NewGame)
{
if (component.Game.Started == true)
component.Game = new(uid);
component.Game.StartGame();
return;
}
component.Game.ProcessInput(msg.PlayerAction);
}
}