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

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -20,16 +21,16 @@ namespace Content.Client.Arcade
{
public sealed class BlockGameMenu : DefaultWindow
{
private static readonly Color OverlayBackgroundColor = new(74,74,81,180);
private static readonly Color OverlayShadowColor = new(0,0,0,83);
private static readonly Color OverlayBackgroundColor = new(74, 74, 81, 180);
private static readonly Color OverlayShadowColor = new(0, 0, 0, 83);
private static readonly Vector2 BlockSize = new(15,15);
private static readonly Vector2 BlockSize = new(15, 15);
private readonly BlockGameBoundUserInterface _owner;
private readonly PanelContainer _mainPanel;
private BoxContainer _gameRootContainer;
private readonly BoxContainer _gameRootContainer;
private GridContainer _gameGrid = default!;
private GridContainer _nextBlockGrid = default!;
private GridContainer _holdBlockGrid = default!;
@@ -82,7 +83,7 @@ namespace Content.Client.Arcade
_gameRootContainer.AddChild(_levelLabel);
_gameRootContainer.AddChild(new Control
{
MinSize = new Vector2(1,5)
MinSize = new Vector2(1, 5)
});
_pointsLabel = new Label
@@ -93,7 +94,7 @@ namespace Content.Client.Arcade
_gameRootContainer.AddChild(_pointsLabel);
_gameRootContainer.AddChild(new Control
{
MinSize = new Vector2(1,10)
MinSize = new Vector2(1, 10)
});
var gameBox = new BoxContainer
@@ -103,12 +104,12 @@ namespace Content.Client.Arcade
gameBox.AddChild(SetupHoldBox(backgroundTexture));
gameBox.AddChild(new Control
{
MinSize = new Vector2(10,1)
MinSize = new Vector2(10, 1)
});
gameBox.AddChild(SetupGameGrid(backgroundTexture));
gameBox.AddChild(new Control
{
MinSize = new Vector2(10,1)
MinSize = new Vector2(10, 1)
});
gameBox.AddChild(SetupNextBox(backgroundTexture));
@@ -116,7 +117,7 @@ namespace Content.Client.Arcade
_gameRootContainer.AddChild(new Control
{
MinSize = new Vector2(1,10)
MinSize = new Vector2(1, 10)
});
_pauseButton = new Button
@@ -176,7 +177,7 @@ namespace Content.Client.Arcade
_owner.SendAction(BlockGamePlayerAction.NewGame);
};
pauseMenuContainer.AddChild(_newGameButton);
pauseMenuContainer.AddChild(new Control{MinSize = new Vector2(1,10)});
pauseMenuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) });
_scoreBoardButton = new Button
{
@@ -185,7 +186,7 @@ namespace Content.Client.Arcade
};
_scoreBoardButton.OnPressed += (e) => _owner.SendAction(BlockGamePlayerAction.ShowHighscores);
pauseMenuContainer.AddChild(_scoreBoardButton);
_unpauseButtonMargin = new Control {MinSize = new Vector2(1, 10), Visible = false};
_unpauseButtonMargin = new Control { MinSize = new Vector2(1, 10), Visible = false };
pauseMenuContainer.AddChild(_unpauseButtonMargin);
_unpauseButton = new Button
@@ -239,13 +240,13 @@ namespace Content.Client.Arcade
VerticalAlignment = VAlignment.Center
};
gameOverMenuContainer.AddChild(new Label{Text = Loc.GetString("blockgame-menu-msg-game-over"),Align = Label.AlignMode.Center});
gameOverMenuContainer.AddChild(new Control{MinSize = new Vector2(1,10)});
gameOverMenuContainer.AddChild(new Label { Text = Loc.GetString("blockgame-menu-msg-game-over"), Align = Label.AlignMode.Center });
gameOverMenuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) });
_finalScoreLabel = new Label{Align = Label.AlignMode.Center};
_finalScoreLabel = new Label { Align = Label.AlignMode.Center };
gameOverMenuContainer.AddChild(_finalScoreLabel);
gameOverMenuContainer.AddChild(new Control{MinSize = new Vector2(1,10)});
gameOverMenuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) });
_finalNewGameButton = new Button
{
@@ -275,7 +276,7 @@ namespace Content.Client.Arcade
HorizontalAlignment = HAlignment.Center
};
var c = new Color(OverlayBackgroundColor.R,OverlayBackgroundColor.G,OverlayBackgroundColor.B,220);
var c = new Color(OverlayBackgroundColor.R, OverlayBackgroundColor.G, OverlayBackgroundColor.B, 220);
var innerBack = new StyleBoxTexture
{
Texture = backgroundTexture,
@@ -298,8 +299,8 @@ namespace Content.Client.Arcade
VerticalAlignment = VAlignment.Center
};
menuContainer.AddChild(new Label{Text = Loc.GetString("blockgame-menu-label-highscores")});
menuContainer.AddChild(new Control{MinSize = new Vector2(1,10)});
menuContainer.AddChild(new Label { Text = Loc.GetString("blockgame-menu-label-highscores") });
menuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) });
var highScoreBox = new BoxContainer
{
@@ -311,14 +312,14 @@ namespace Content.Client.Arcade
Align = Label.AlignMode.Center
};
highScoreBox.AddChild(_localHighscoresLabel);
highScoreBox.AddChild(new Control{MinSize = new Vector2(40,1)});
highScoreBox.AddChild(new Control { MinSize = new Vector2(40, 1) });
_globalHighscoresLabel = new Label
{
Align = Label.AlignMode.Center
};
highScoreBox.AddChild(_globalHighscoresLabel);
menuContainer.AddChild(highScoreBox);
menuContainer.AddChild(new Control{MinSize = new Vector2(1,10)});
menuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) });
_highscoreBackButton = new Button
{
Text = Loc.GetString("blockgame-menu-button-back"),
@@ -359,7 +360,7 @@ namespace Content.Client.Arcade
HSeparationOverride = 1,
VSeparationOverride = 1
};
UpdateBlocks(new BlockGameBlock[0]);
UpdateBlocks(Array.Empty<BlockGameBlock>());
var back = new StyleBoxTexture
{
@@ -376,7 +377,7 @@ namespace Content.Client.Arcade
};
var backgroundPanel = new PanelContainer
{
PanelOverride = new StyleBoxFlat{BackgroundColor = Color.FromHex("#86868d")}
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#86868d") }
};
backgroundPanel.AddChild(_gameGrid);
gamePanel.AddChild(backgroundPanel);
@@ -416,7 +417,7 @@ namespace Content.Client.Arcade
nextBlockPanel.AddChild(nextCenterContainer);
grid.AddChild(nextBlockPanel);
grid.AddChild(new Label{Text = Loc.GetString("blockgame-menu-label-next"), Align = Label.AlignMode.Center});
grid.AddChild(new Label { Text = Loc.GetString("blockgame-menu-label-next"), Align = Label.AlignMode.Center });
return grid;
}
@@ -454,15 +455,17 @@ namespace Content.Client.Arcade
holdBlockPanel.AddChild(holdCenterContainer);
grid.AddChild(holdBlockPanel);
grid.AddChild(new Label{Text = Loc.GetString("blockgame-menu-label-hold"), Align = Label.AlignMode.Center});
grid.AddChild(new Label { Text = Loc.GetString("blockgame-menu-label-hold"), Align = Label.AlignMode.Center });
return grid;
}
protected override void KeyboardFocusExited()
{
if (!IsOpen) return;
if(_gameOver) return;
if (!IsOpen)
return;
if (_gameOver)
return;
TryPause();
}
@@ -480,7 +483,8 @@ namespace Content.Client.Arcade
public void SetScreen(BlockGameMessages.BlockGameScreen screen)
{
if (_gameOver) return;
if (_gameOver)
return;
switch (screen)
{
@@ -512,9 +516,12 @@ namespace Content.Client.Arcade
private void CloseMenus()
{
if(_mainPanel.Children.Contains(_menuRootContainer)) _mainPanel.RemoveChild(_menuRootContainer);
if(_mainPanel.Children.Contains(_gameOverRootContainer)) _mainPanel.RemoveChild(_gameOverRootContainer);
if(_mainPanel.Children.Contains(_highscoresRootContainer)) _mainPanel.RemoveChild(_highscoresRootContainer);
if (_mainPanel.Children.Contains(_menuRootContainer))
_mainPanel.RemoveChild(_menuRootContainer);
if (_mainPanel.Children.Contains(_gameOverRootContainer))
_mainPanel.RemoveChild(_gameOverRootContainer);
if (_mainPanel.Children.Contains(_highscoresRootContainer))
_mainPanel.RemoveChild(_highscoresRootContainer);
}
public void SetGameoverInfo(int amount, int? localPlacement, int? globalPlacement)
@@ -563,72 +570,56 @@ namespace Content.Client.Arcade
{
base.KeyBindDown(args);
if(!_isPlayer || args.Handled) return;
if (!_isPlayer || args.Handled)
return;
if (args.Function == ContentKeyFunctions.ArcadeLeft)
{
else if (args.Function == ContentKeyFunctions.ArcadeLeft)
_owner.SendAction(BlockGamePlayerAction.StartLeft);
}
else if (args.Function == ContentKeyFunctions.ArcadeRight)
{
_owner.SendAction(BlockGamePlayerAction.StartRight);
}
else if (args.Function == ContentKeyFunctions.ArcadeUp)
{
_owner.SendAction(BlockGamePlayerAction.Rotate);
}
else if (args.Function == ContentKeyFunctions.Arcade3)
{
_owner.SendAction(BlockGamePlayerAction.CounterRotate);
}
else if (args.Function == ContentKeyFunctions.ArcadeDown)
{
_owner.SendAction(BlockGamePlayerAction.SoftdropStart);
}
else if (args.Function == ContentKeyFunctions.Arcade2)
{
_owner.SendAction(BlockGamePlayerAction.Hold);
}
else if (args.Function == ContentKeyFunctions.Arcade1)
{
_owner.SendAction(BlockGamePlayerAction.Harddrop);
}
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
if(!_isPlayer || args.Handled) return;
if (!_isPlayer || args.Handled)
return;
if (args.Function == ContentKeyFunctions.ArcadeLeft)
{
else if (args.Function == ContentKeyFunctions.ArcadeLeft)
_owner.SendAction(BlockGamePlayerAction.EndLeft);
}
else if (args.Function == ContentKeyFunctions.ArcadeRight)
{
_owner.SendAction(BlockGamePlayerAction.EndRight);
}else if (args.Function == ContentKeyFunctions.ArcadeDown)
{
else if (args.Function == ContentKeyFunctions.ArcadeDown)
_owner.SendAction(BlockGamePlayerAction.SoftdropEnd);
}
}
public void UpdateNextBlock(BlockGameBlock[] blocks)
{
_nextBlockGrid.RemoveAllChildren();
if (blocks.Length == 0) return;
if (blocks.Length == 0)
return;
var columnCount = blocks.Max(b => b.Position.X) + 1;
var rowCount = blocks.Max(b => b.Position.Y) + 1;
_nextBlockGrid.Columns = columnCount;
for (int y = 0; y < rowCount; y++)
for (var y = 0; y < rowCount; y++)
{
for (int x = 0; x < columnCount; x++)
for (var x = 0; x < columnCount; x++)
{
var c = GetColorForPosition(blocks, x, y);
_nextBlockGrid.AddChild(new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = c},
PanelOverride = new StyleBoxFlat { BackgroundColor = c },
MinSize = BlockSize,
RectDrawClipMargin = 0
});
@@ -639,18 +630,19 @@ namespace Content.Client.Arcade
public void UpdateHeldBlock(BlockGameBlock[] blocks)
{
_holdBlockGrid.RemoveAllChildren();
if (blocks.Length == 0) return;
if (blocks.Length == 0)
return;
var columnCount = blocks.Max(b => b.Position.X) + 1;
var rowCount = blocks.Max(b => b.Position.Y) + 1;
_holdBlockGrid.Columns = columnCount;
for (int y = 0; y < rowCount; y++)
for (var y = 0; y < rowCount; y++)
{
for (int x = 0; x < columnCount; x++)
for (var x = 0; x < columnCount; x++)
{
var c = GetColorForPosition(blocks, x, y);
_holdBlockGrid.AddChild(new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = c},
PanelOverride = new StyleBoxFlat { BackgroundColor = c },
MinSize = BlockSize,
RectDrawClipMargin = 0
});
@@ -661,14 +653,14 @@ namespace Content.Client.Arcade
public void UpdateBlocks(BlockGameBlock[] blocks)
{
_gameGrid.RemoveAllChildren();
for (int y = 0; y < 20; y++)
for (var y = 0; y < 20; y++)
{
for (int x = 0; x < 10; x++)
for (var x = 0; x < 10; x++)
{
var c = GetColorForPosition(blocks, x, y);
_gameGrid.AddChild(new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = c},
PanelOverride = new StyleBoxFlat { BackgroundColor = c },
MinSize = BlockSize,
RectDrawClipMargin = 0
});
@@ -676,9 +668,9 @@ namespace Content.Client.Arcade
}
}
private Color GetColorForPosition(BlockGameBlock[] blocks, int x, int y)
private static Color GetColorForPosition(BlockGameBlock[] blocks, int x, int y)
{
Color c = Color.Transparent;
var c = Color.Transparent;
var matchingBlock = blocks.FirstOrNull(b => b.Position.X == x && b.Position.Y == y);
if (matchingBlock.HasValue)
{

View File

@@ -24,40 +24,46 @@ namespace Content.Client.Arcade
Title = Loc.GetString("spacevillain-menu-title");
Owner = owner;
var grid = new GridContainer {Columns = 1};
var grid = new GridContainer { Columns = 1 };
var infoGrid = new GridContainer {Columns = 3};
infoGrid.AddChild(new Label{ Text = Loc.GetString("spacevillain-menu-label-player"), Align = Label.AlignMode.Center });
infoGrid.AddChild(new Label{ Text = "|", Align = Label.AlignMode.Center });
_enemyNameLabel = new Label{ Align = Label.AlignMode.Center};
var infoGrid = new GridContainer { Columns = 3 };
infoGrid.AddChild(new Label { Text = Loc.GetString("spacevillain-menu-label-player"), Align = Label.AlignMode.Center });
infoGrid.AddChild(new Label { Text = "|", Align = Label.AlignMode.Center });
_enemyNameLabel = new Label { Align = Label.AlignMode.Center };
infoGrid.AddChild(_enemyNameLabel);
_playerInfoLabel = new Label {Align = Label.AlignMode.Center};
_playerInfoLabel = new Label { Align = Label.AlignMode.Center };
infoGrid.AddChild(_playerInfoLabel);
infoGrid.AddChild(new Label{ Text = "|", Align = Label.AlignMode.Center });
_enemyInfoLabel = new Label {Align = Label.AlignMode.Center};
infoGrid.AddChild(new Label { Text = "|", Align = Label.AlignMode.Center });
_enemyInfoLabel = new Label { Align = Label.AlignMode.Center };
infoGrid.AddChild(_enemyInfoLabel);
var centerContainer = new CenterContainer();
centerContainer.AddChild(infoGrid);
grid.AddChild(centerContainer);
_playerActionLabel = new Label {Align = Label.AlignMode.Center};
_playerActionLabel = new Label { Align = Label.AlignMode.Center };
grid.AddChild(_playerActionLabel);
_enemyActionLabel = new Label {Align = Label.AlignMode.Center};
_enemyActionLabel = new Label { Align = Label.AlignMode.Center };
grid.AddChild(_enemyActionLabel);
var buttonGrid = new GridContainer {Columns = 3};
var buttonGrid = new GridContainer { Columns = 3 };
_gameButtons[0] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Attack)
{Text = Loc.GetString("spacevillain-menu-button-attack")};
{
Text = Loc.GetString("spacevillain-menu-button-attack")
};
buttonGrid.AddChild(_gameButtons[0]);
_gameButtons[1] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Heal)
{Text = Loc.GetString("spacevillain-menu-button-heal")};
{
Text = Loc.GetString("spacevillain-menu-button-heal")
};
buttonGrid.AddChild(_gameButtons[1]);
_gameButtons[2] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Recharge)
{Text = Loc.GetString("spacevillain-menu-button-recharge")};
{
Text = Loc.GetString("spacevillain-menu-button-recharge")
};
buttonGrid.AddChild(_gameButtons[2]);
centerContainer = new CenterContainer();
@@ -65,7 +71,9 @@ namespace Content.Client.Arcade
grid.AddChild(centerContainer);
var newGame = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.NewGame)
{Text = Loc.GetString("spacevillain-menu-button-new-game")};
{
Text = Loc.GetString("spacevillain-menu-button-new-game")
};
grid.AddChild(newGame);
Contents.AddChild(grid);
@@ -84,7 +92,8 @@ namespace Content.Client.Arcade
public void UpdateInfo(SharedSpaceVillainArcadeComponent.SpaceVillainArcadeDataUpdateMessage message)
{
if(message is SharedSpaceVillainArcadeComponent.SpaceVillainArcadeMetaDataUpdateMessage metaMessage) UpdateMetadata(metaMessage);
if (message is SharedSpaceVillainArcadeComponent.SpaceVillainArcadeMetaDataUpdateMessage metaMessage)
UpdateMetadata(metaMessage);
_playerInfoLabel.Text = $"HP: {message.PlayerHP} MP: {message.PlayerMP}";
_enemyInfoLabel.Text = $"HP: {message.EnemyHP} MP: {message.EnemyMP}";
@@ -97,7 +106,7 @@ namespace Content.Client.Arcade
private readonly SpaceVillainArcadeBoundUserInterface _owner;
private readonly SharedSpaceVillainArcadeComponent.PlayerAction _playerAction;
public ActionButton(SpaceVillainArcadeBoundUserInterface owner,SharedSpaceVillainArcadeComponent.PlayerAction playerAction)
public ActionButton(SpaceVillainArcadeBoundUserInterface owner, SharedSpaceVillainArcadeComponent.PlayerAction playerAction)
{
_owner = owner;
_playerAction = playerAction;

View File

@@ -1,11 +1,10 @@
using Content.Shared.Arcade;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
namespace Content.Client.Arcade.UI
namespace Content.Client.Arcade.UI;
public sealed class BlockGameBoundUserInterface : BoundUserInterface
{
public sealed class BlockGameBoundUserInterface : BoundUserInterface
{
private BlockGameMenu? _menu;
public BlockGameBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
@@ -74,5 +73,4 @@ namespace Content.Client.Arcade.UI
_menu?.Dispose();
}
}
}

View File

@@ -3,10 +3,10 @@ using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
namespace Content.Client.Arcade.UI
namespace Content.Client.Arcade.UI;
public sealed class SpaceVillainArcadeBoundUserInterface : BoundUserInterface
{
public sealed class SpaceVillainArcadeBoundUserInterface : BoundUserInterface
{
[ViewVariables] private SpaceVillainArcadeMenu? _menu;
//public SharedSpaceVillainArcadeComponent SpaceVillainArcade;
@@ -25,13 +25,6 @@ namespace Content.Client.Arcade.UI
{
base.Open();
/*if(!Owner.Owner.TryGetComponent(out SharedSpaceVillainArcadeComponent spaceVillainArcade))
{
return;
}
SpaceVillainArcade = spaceVillainArcade;*/
_menu = new SpaceVillainArcadeMenu(this);
_menu.OnClose += Close;
@@ -40,14 +33,15 @@ namespace Content.Client.Arcade.UI
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
if (message is SpaceVillainArcadeDataUpdateMessage msg) _menu?.UpdateInfo(msg);
if (message is SpaceVillainArcadeDataUpdateMessage msg)
_menu?.UpdateInfo(msg);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing) _menu?.Dispose();
}
if (disposing)
_menu?.Dispose();
}
}

View File

@@ -1,17 +0,0 @@
using Content.Server.Arcade.Components;
using Content.Server.Power.Components;
namespace Content.Server.Arcade;
public sealed partial class ArcadeSystem
{
private void InitializeBlockGame()
{
SubscribeLocalEvent<BlockGameArcadeComponent, PowerChangedEvent>(OnBlockPowerChanged);
}
private static void OnBlockPowerChanged(EntityUid uid, BlockGameArcadeComponent component, ref PowerChangedEvent args)
{
component.OnPowerStateChanged(args);
}
}

View File

@@ -1,17 +0,0 @@
using Content.Server.Arcade.Components;
using Content.Server.Power.Components;
namespace Content.Server.Arcade;
public sealed partial class ArcadeSystem
{
private void InitializeSpaceVillain()
{
SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower);
}
private void OnSVillainPower(EntityUid uid, SpaceVillainArcadeComponent component, ref PowerChangedEvent args)
{
component.OnPowerStateChanged(args);
}
}

View File

@@ -1,5 +1,4 @@
using System.Linq;
using Content.Server.Arcade.Components;
using Content.Server.UserInterface;
using Content.Shared.Arcade;
using Robust.Shared.Utility;
@@ -17,25 +16,6 @@ namespace Content.Server.Arcade
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BlockGameArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpen);
SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
SubscribeLocalEvent<BlockGameArcadeComponent, BoundUIClosedEvent>((_,c,args) => c.UnRegisterPlayerSession((IPlayerSession)args.Session));
InitializeBlockGame();
InitializeSpaceVillain();
}
private void OnAfterUIOpen(EntityUid uid, BlockGameArcadeComponent component, AfterActivatableUIOpenEvent args)
{
var actor = Comp<ActorComponent>(args.User);
if (component.UserInterface?.SessionHasOpen(actor.PlayerSession) == true)
{
component.RegisterPlayerSession(actor.PlayerSession);
}
}
private void OnAfterUIOpenSV(EntityUid uid, SpaceVillainArcadeComponent component, AfterActivatableUIOpenEvent args)
{
component.Game ??= new SpaceVillainArcadeComponent.SpaceVillainGame(component);
}
public HighScorePlacement RegisterHighScore(string name, int score)
@@ -87,14 +67,6 @@ namespace Content.Server.Arcade
return placement;
}
public override void Update(float frameTime)
{
foreach (var comp in EntityManager.EntityQuery<BlockGameArcadeComponent>())
{
comp.DoGameTick(frameTime);
}
}
public readonly struct HighScorePlacement
{
public readonly int? GlobalPlacement;

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

View File

@@ -1,867 +0,0 @@
using System.Linq;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.Arcade;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Random;
namespace Content.Server.Arcade.Components
{
[RegisterComponent]
public sealed class BlockGameArcadeComponent : Component
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
public bool Powered => _entityManager.TryGetComponent<ApcPowerReceiverComponent>(Owner, out var powerReceiverComponent) && powerReceiverComponent.Powered;
public BoundUserInterface? UserInterface => Owner.GetUIOrNull(BlockGameUiKey.Key);
private BlockGame? _game;
private IPlayerSession? _player;
private readonly List<IPlayerSession> _spectators = new();
public void RegisterPlayerSession(IPlayerSession session)
{
if (_player == null) _player = session;
else _spectators.Add(session);
UpdatePlayerStatus(session);
_game?.UpdateNewPlayerUI(session);
}
private void DeactivePlayer(IPlayerSession session)
{
if (_player != session) return;
var temp = _player;
_player = null;
if (_spectators.Count != 0)
{
_player = _spectators[0];
_spectators.Remove(_player);
UpdatePlayerStatus(_player);
}
_spectators.Add(temp);
UpdatePlayerStatus(temp);
}
public void UnRegisterPlayerSession(IPlayerSession session)
{
if (_player == session)
{
DeactivePlayer(_player);
}
else
{
_spectators.Remove(session);
UpdatePlayerStatus(session);
}
}
private void UpdatePlayerStatus(IPlayerSession session)
{
UserInterface?.SendMessage(new BlockGameMessages.BlockGameUserStatusMessage(_player == session), session);
}
protected override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
_game = new BlockGame(this);
}
public void OnPowerStateChanged(PowerChangedEvent e)
{
if (e.Powered) return;
UserInterface?.CloseAll();
_player = null;
_spectators.Clear();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
switch (obj.Message)
{
case BlockGameMessages.BlockGamePlayerActionMessage playerActionMessage:
if (obj.Session != _player) break;
if (playerActionMessage.PlayerAction == BlockGamePlayerAction.NewGame)
{
if(_game?.Started == true) _game = new BlockGame(this);
_game?.StartGame();
}
else
{
_game?.ProcessInput(playerActionMessage.PlayerAction);
}
break;
}
}
public void DoGameTick(float frameTime)
{
_game?.GameTick(frameTime);
}
private sealed class BlockGame
{
//note: field is 10(0 -> 9) wide and 20(0 -> 19) high
private readonly BlockGameArcadeComponent _component;
private readonly List<BlockGameBlock> _field = new();
private BlockGamePiece _currentPiece;
private BlockGamePiece _nextPiece
{
get => _internalNextPiece;
set
{
_internalNextPiece = value;
SendNextPieceUpdate();
}
}
private BlockGamePiece _internalNextPiece;
private void SendNextPieceUpdate()
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock));
}
private void SendNextPieceUpdate(IPlayerSession session)
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock), session);
}
private bool _holdBlock = false;
private BlockGamePiece? _heldPiece
{
get => _internalHeldPiece;
set
{
_internalHeldPiece = value;
SendHoldPieceUpdate();
}
}
private BlockGamePiece? _internalHeldPiece = null;
private void SendHoldPieceUpdate()
{
if(_heldPiece.HasValue) _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_heldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock));
else _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(new BlockGameBlock[0], BlockGameMessages.BlockGameVisualType.HoldBlock));
}
private void SendHoldPieceUpdate(IPlayerSession session)
{
if(_heldPiece.HasValue) _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_heldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock), session);
else _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(new BlockGameBlock[0], BlockGameMessages.BlockGameVisualType.HoldBlock), session);
}
private Vector2i _currentPiecePosition;
private BlockGamePieceRotation _currentRotation;
private float _softDropModifier = 0.1f;
private float Speed =>
-0.03f * Level + 1 * (!_softDropPressed ? 1 : _softDropModifier);
private const float _pressCheckSpeed = 0.08f;
private bool _running;
public bool Paused => !(_running && _started);
private bool _started;
public bool Started => _started;
private bool _gameOver;
private bool _leftPressed;
private bool _rightPressed;
private bool _softDropPressed;
private int Points
{
get => _internalPoints;
set
{
if (_internalPoints == value) return;
_internalPoints = value;
SendPointsUpdate();
}
}
private int _internalPoints;
private ArcadeSystem.HighScorePlacement? _highScorePlacement = null;
private void SendPointsUpdate()
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points));
}
private void SendPointsUpdate(IPlayerSession session)
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points));
}
public int Level
{
get => _level;
set
{
_level = value;
SendLevelUpdate();
}
}
private int _level = 0;
private void SendLevelUpdate()
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level));
}
private void SendLevelUpdate(IPlayerSession session)
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level));
}
private int ClearedLines
{
get => _clearedLines;
set
{
_clearedLines = value;
if (_clearedLines < LevelRequirement) return;
_clearedLines -= LevelRequirement;
Level++;
}
}
private int _clearedLines = 0;
private int LevelRequirement => Math.Min(100, Math.Max(Level * 10 - 50, 10));
public BlockGame(BlockGameArcadeComponent component)
{
_component = component;
_allBlockGamePieces = (BlockGamePieceType[]) Enum.GetValues(typeof(BlockGamePieceType));
_internalNextPiece = GetRandomBlockGamePiece(_component._random);
InitializeNewBlock();
}
private void SendHighscoreUpdate()
{
var entitySystem = EntitySystem.Get<ArcadeSystem>();
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(entitySystem.GetLocalHighscores(), entitySystem.GetGlobalHighscores()));
}
private void SendHighscoreUpdate(IPlayerSession session)
{
var entitySystem = EntitySystem.Get<ArcadeSystem>();
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(entitySystem.GetLocalHighscores(), entitySystem.GetGlobalHighscores()), session);
}
public void StartGame()
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game));
FullUpdate();
_running = true;
_started = true;
}
private void FullUpdate()
{
UpdateAllFieldUI();
SendHoldPieceUpdate();
SendNextPieceUpdate();
SendPointsUpdate();
SendHighscoreUpdate();
SendLevelUpdate();
}
private void FullUpdate(IPlayerSession session)
{
UpdateFieldUI(session);
SendPointsUpdate(session);
SendNextPieceUpdate(session);
SendHoldPieceUpdate(session);
SendHighscoreUpdate(session);
SendLevelUpdate(session);
}
public void GameTick(float frameTime)
{
if (!_running) return;
InputTick(frameTime);
FieldTick(frameTime);
}
private float _accumulatedLeftPressTime;
private float _accumulatedRightPressTime;
private void InputTick(float frameTime)
{
bool 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) UpdateAllFieldUI();
}
private float _accumulatedFieldFrameTime;
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;
}
}
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();
UpdateAllFieldUI();
}
private void CheckField()
{
int pointsToAdd = 0;
int consecutiveLines = 0;
int clearedLines = 0;
for (int 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);
}
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;
}
private void AddPoints(int amount)
{
if (amount == 0) return;
Points += amount;
}
private void FillLine(int y)
{
for (int c_y = y; c_y > 0; c_y--)
{
for (int 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);
}
}
}
private void InitializeNewBlock()
{
InitializeNewBlock(_nextPiece);
_nextPiece = GetRandomBlockGamePiece(_component._random);
_holdBlock = false;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock));
}
private void InitializeNewBlock(BlockGamePiece piece)
{
_currentPiecePosition = new Vector2i(5,0);
_currentRotation = BlockGamePieceRotation.North;
_currentPiece = piece;
UpdateAllFieldUI();
}
private bool LowerBoundCheck(Vector2i position) => position.Y < 20;
private bool BorderCheck(Vector2i position) => position.X >= 0 && position.X < 10;
private bool ClearCheck(Vector2i position) => _field.All(block => !position.Equals(block.Position));
private bool DropCheck(Vector2i position) => LowerBoundCheck(position) && ClearCheck(position);
private bool MoveCheck(Vector2i position) => BorderCheck(position) && ClearCheck(position);
private bool RotateCheck(Vector2i position) => BorderCheck(position) && LowerBoundCheck(position) && ClearCheck(position);
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;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, _started));
break;
case BlockGamePlayerAction.Unpause:
if (!_gameOver && _started)
{
_running = true;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game));
}
break;
case BlockGamePlayerAction.ShowHighscores:
_running = false;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Highscores, _started));
break;
}
}
private void TrySetRotation(BlockGamePieceRotation rotation)
{
if(!_running) return;
if (!_currentPiece.CanSpin) return;
if (!_currentPiece.Positions(_currentPiecePosition, rotation)
.All(RotateCheck))
{
return;
}
_currentRotation = rotation;
UpdateAllFieldUI();
}
private void HoldPiece()
{
if (!_running) return;
if (_holdBlock) return;
var tempHeld = _heldPiece;
_heldPiece = _currentPiece;
_holdBlock = true;
if (!tempHeld.HasValue)
{
InitializeNewBlock();
return;
}
InitializeNewBlock(tempHeld.Value);
}
private void PerformHarddrop()
{
int spacesDropped = 0;
while (_currentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation)
.All(DropCheck))
{
_currentPiecePosition = _currentPiecePosition.AddToY(1);
spacesDropped++;
}
AddPoints(spacesDropped * 2);
InternalFieldTick();
}
public void UpdateAllFieldUI()
{
if (!_started) return;
var computedField = ComputeField();
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField));
}
public void UpdateFieldUI(IPlayerSession session)
{
if (!_started) return;
var computedField = ComputeField();
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField), session);
}
private bool IsGameOver => _field.Any(block => block.Position.Y == 0);
private void InvokeGameover()
{
_running = false;
_gameOver = true;
if (_component._player?.AttachedEntity is {Valid: true} playerEntity)
{
var blockGameSystem = EntitySystem.Get<ArcadeSystem>();
_highScorePlacement = blockGameSystem.RegisterHighScore(IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(playerEntity).EntityName, Points);
SendHighscoreUpdate();
}
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement));
}
public void UpdateNewPlayerUI(IPlayerSession session)
{
if (_gameOver)
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement), session);
}
else
{
if (Paused)
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, Started), session);
}
else
{
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game, Started), session);
}
}
FullUpdate(session);
}
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;
}
private enum BlockGamePieceType
{
I,
L,
LInverted,
S,
SInverted,
T,
O
}
private enum BlockGamePieceRotation
{
North,
East,
South,
West
}
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)
};
}
private readonly BlockGamePieceType[] _allBlockGamePieces;
private List<BlockGamePieceType> _blockGamePiecesBuffer = new();
private BlockGamePiece GetRandomBlockGamePiece(IRobustRandom random)
{
if (_blockGamePiecesBuffer.Count == 0)
{
_blockGamePiecesBuffer = _allBlockGamePieces.ToList();
}
var chosenPiece = random.Pick(_blockGamePiecesBuffer);
_blockGamePiecesBuffer.Remove(chosenPiece);
return BlockGamePiece.GetPiece(chosenPiece);
}
private struct BlockGamePiece
{
public Vector2i[] Offsets;
private BlockGameBlock.BlockGameBlockColor _gameBlockColor;
public bool CanSpin;
public Vector2i[] Positions(Vector2i center,
BlockGamePieceRotation rotation)
{
return RotatedOffsets(rotation).Select(v => center + v).ToArray();
}
private Vector2i[] RotatedOffsets(BlockGamePieceRotation rotation)
{
Vector2i[] 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;
}
public 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;
}
public 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);
}
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

@@ -1,389 +0,0 @@
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.Arcade;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
// TODO: ECS.
namespace Content.Server.Arcade.Components
{
[RegisterComponent]
public sealed class SpaceVillainArcadeComponent : SharedSpaceVillainArcadeComponent
{
[Dependency] private readonly IRobustRandom _random = null!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private bool Powered => _entityManager.TryGetComponent<ApcPowerReceiverComponent>(Owner, out var powerReceiverComponent) && powerReceiverComponent.Powered;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SpaceVillainArcadeUiKey.Key);
[ViewVariables] public bool OverflowFlag;
[ViewVariables] public bool PlayerInvincibilityFlag;
[ViewVariables] public bool EnemyInvincibilityFlag;
[ViewVariables] public SpaceVillainGame Game = null!;
[DataField("newGameSound")] private SoundSpecifier _newGameSound = new SoundPathSpecifier("/Audio/Effects/Arcade/newgame.ogg");
[DataField("playerAttackSound")] private SoundSpecifier _playerAttackSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_attack.ogg");
[DataField("playerHealSound")] private SoundSpecifier _playerHealSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_heal.ogg");
[DataField("playerChargeSound")] private SoundSpecifier _playerChargeSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_charge.ogg");
[DataField("winSound")] private SoundSpecifier _winSound = new SoundPathSpecifier("/Audio/Effects/Arcade/win.ogg");
[DataField("gameOverSound")] private SoundSpecifier _gameOverSound = new SoundPathSpecifier("/Audio/Effects/Arcade/gameover.ogg");
[ViewVariables(VVAccess.ReadWrite)]
[DataField("possibleFightVerbs")]
private List<string> _possibleFightVerbs = new List<string>()
{"Defeat", "Annihilate", "Save", "Strike", "Stop", "Destroy", "Robust", "Romance", "Pwn", "Own"};
[ViewVariables(VVAccess.ReadWrite)]
[DataField("possibleFirstEnemyNames")]
private List<string> _possibleFirstEnemyNames = new List<string>(){
"the Automatic", "Farmer", "Lord", "Professor", "the Cuban", "the Evil", "the Dread King",
"the Space", "Lord", "the Great", "Duke", "General"
};
[ViewVariables(VVAccess.ReadWrite)]
[DataField("possibleLastEnemyNames")]
private List<string> _possibleLastEnemyNames = new List<string>()
{
"Melonoid", "Murdertron", "Sorcerer", "Ruin", "Jeff", "Ectoplasm", "Crushulon", "Uhangoid",
"Vhakoid", "Peteoid", "slime", "Griefer", "ERPer", "Lizard Man", "Unicorn"
};
[ViewVariables(VVAccess.ReadWrite)]
[DataField("possibleRewards", customTypeSerializer:typeof(PrototypeIdListSerializer<EntityPrototype>))]
private List<string> _possibleRewards = new List<string>()
{
"ToyMouse", "ToyAi", "ToyNuke", "ToyAssistant", "ToyGriffin", "ToyHonk", "ToyIan",
"ToyMarauder", "ToyMauler", "ToyGygax", "ToyOdysseus", "ToyOwlman", "ToyDeathRipley",
"ToyPhazon", "ToyFireRipley", "ToyReticence", "ToyRipley", "ToySeraph", "ToyDurand", "ToySkeleton",
"FoamCrossbow", "RevolverCapGun", "PlushieHampter", "PlushieLizard", "PlushieAtmosian", "PlushieSpaceLizard",
"PlushieNuke", "PlushieCarp", "PlushieRatvar", "PlushieNar", "PlushieSnake", "Basketball", "Football",
"PlushieRouny", "PlushieBee", "PlushieSlime", "BalloonCorgi", "ToySword", "CrayonBox", "BoxDonkSoftBox", "BoxCartridgeCap",
"HarmonicaInstrument", "OcarinaInstrument", "RecorderInstrument", "GunpetInstrument", "BirdToyInstrument", "PlushieXeno"
};
[DataField("rewardMinAmount")]
public int _rewardMinAmount;
[DataField("rewardMaxAmount")]
public int _rewardMaxAmount;
[ViewVariables(VVAccess.ReadWrite)]
public int _rewardAmount = 0;
protected override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
// Random amount of prizes
_rewardAmount = new Random().Next(_rewardMinAmount, _rewardMaxAmount + 1);
}
public void OnPowerStateChanged(PowerChangedEvent e)
{
if (e.Powered) return;
UserInterface?.CloseAll();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
{
if (!Powered)
return;
if (serverMsg.Message is not SpaceVillainArcadePlayerActionMessage msg) return;
switch (msg.PlayerAction)
{
case PlayerAction.Attack:
Game?.ExecutePlayerAction(msg.PlayerAction);
break;
case PlayerAction.Heal:
Game?.ExecutePlayerAction(msg.PlayerAction);
break;
case PlayerAction.Recharge:
Game?.ExecutePlayerAction(msg.PlayerAction);
break;
case PlayerAction.NewGame:
SoundSystem.Play(_newGameSound.GetSound(), Filter.Pvs(Owner), Owner, AudioParams.Default.WithVolume(-4f));
Game = new SpaceVillainGame(this);
UserInterface?.SendMessage(Game.GenerateMetaDataMessage());
break;
case PlayerAction.RequestData:
UserInterface?.SendMessage(Game.GenerateMetaDataMessage());
break;
}
}
/// <summary>
/// Called when the user wins the game.
/// </summary>
public void ProcessWin()
{
if (_rewardAmount > 0)
{
_entityManager.SpawnEntity(_random.Pick(_possibleRewards), _entityManager.GetComponent<TransformComponent>(Owner).Coordinates);
_rewardAmount--;
}
}
/// <summary>
/// Picks a fight-verb from the list of possible Verbs.
/// </summary>
/// <returns>A fight-verb.</returns>
public string GenerateFightVerb()
{
return _random.Pick(_possibleFightVerbs);
}
/// <summary>
/// Generates an enemy-name comprised of a first- and last-name.
/// </summary>
/// <returns>An enemy-name.</returns>
public string GenerateEnemyName()
{
return $"{_random.Pick(_possibleFirstEnemyNames)} {_random.Pick(_possibleLastEnemyNames)}";
}
/// <summary>
/// A Class to handle all the game-logic of the SpaceVillain-game.
/// </summary>
public sealed class SpaceVillainGame
{
[Dependency] private readonly IRobustRandom _random = default!;
[ViewVariables] private readonly SpaceVillainArcadeComponent _owner;
[ViewVariables] public string Name => $"{_fightVerb} {_enemyName}";
[ViewVariables(VVAccess.ReadWrite)] private int _playerHp = 30;
[ViewVariables(VVAccess.ReadWrite)] private int _playerHpMax = 30;
[ViewVariables(VVAccess.ReadWrite)] private int _playerMp = 10;
[ViewVariables(VVAccess.ReadWrite)] private int _playerMpMax = 10;
[ViewVariables(VVAccess.ReadWrite)] private int _enemyHp = 45;
[ViewVariables(VVAccess.ReadWrite)] private int _enemyHpMax = 45;
[ViewVariables(VVAccess.ReadWrite)] private int _enemyMp = 20;
[ViewVariables(VVAccess.ReadWrite)] private int _enemyMpMax = 20;
[ViewVariables(VVAccess.ReadWrite)] private int _turtleTracker;
[ViewVariables(VVAccess.ReadWrite)] private readonly string _fightVerb;
[ViewVariables(VVAccess.ReadWrite)] private readonly string _enemyName;
[ViewVariables] private bool _running = true;
private string _latestPlayerActionMessage = "";
private string _latestEnemyActionMessage = "";
public SpaceVillainGame(SpaceVillainArcadeComponent owner) : this(owner, owner.GenerateFightVerb(), owner.GenerateEnemyName()) { }
public SpaceVillainGame(SpaceVillainArcadeComponent owner, string fightVerb, string enemyName)
{
IoCManager.InjectDependencies(this);
_owner = owner;
//todo defeat the curse secret game mode
_fightVerb = fightVerb;
_enemyName = enemyName;
}
/// <summary>
/// Validates all vars incase they overshoot their max-values.
/// Does not check if vars surpass 0.
/// </summary>
private void ValidateVars()
{
if (_owner.OverflowFlag) return;
if (_playerHp > _playerHpMax) _playerHp = _playerHpMax;
if (_playerMp > _playerMpMax) _playerMp = _playerMpMax;
if (_enemyHp > _enemyHpMax) _enemyHp = _enemyHpMax;
if (_enemyMp > _enemyMpMax) _enemyMp = _enemyMpMax;
}
/// <summary>
/// Called by the SpaceVillainArcadeComponent when Userinput is received.
/// </summary>
/// <param name="action">The action the user picked.</param>
public void ExecutePlayerAction(PlayerAction action)
{
if (!_running) return;
switch (action)
{
case PlayerAction.Attack:
var attackAmount = _random.Next(2, 6);
_latestPlayerActionMessage = Loc.GetString("space-villain-game-player-attack-message",
("enemyName", _enemyName),
("attackAmount", attackAmount));
SoundSystem.Play(_owner._playerAttackSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f));
if (!_owner.EnemyInvincibilityFlag)
_enemyHp -= attackAmount;
_turtleTracker -= _turtleTracker > 0 ? 1 : 0;
break;
case PlayerAction.Heal:
var pointAmount = _random.Next(1, 3);
var healAmount = _random.Next(6, 8);
_latestPlayerActionMessage = Loc.GetString("space-villain-game-player-heal-message",
("magicPointAmount", pointAmount),
("healAmount", healAmount));
SoundSystem.Play(_owner._playerHealSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f));
if (!_owner.PlayerInvincibilityFlag)
_playerMp -= pointAmount;
_playerHp += healAmount;
_turtleTracker++;
break;
case PlayerAction.Recharge:
var chargeAmount = _random.Next(4, 7);
_latestPlayerActionMessage = Loc.GetString("space-villain-game-player-recharge-message", ("regainedPoints", chargeAmount));
SoundSystem.Play(_owner._playerChargeSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f));
_playerMp += chargeAmount;
_turtleTracker -= _turtleTracker > 0 ? 1 : 0;
break;
}
if (!CheckGameConditions())
{
return;
}
ValidateVars();
ExecuteAiAction();
if (!CheckGameConditions())
{
return;
}
ValidateVars();
UpdateUi();
}
/// <summary>
/// Checks the Game conditions and Updates the Ui & Plays a sound accordingly.
/// </summary>
/// <returns>A bool indicating if the game should continue.</returns>
private bool CheckGameConditions()
{
if ((_playerHp > 0 && _playerMp > 0) && (_enemyHp <= 0 || _enemyMp <= 0))
{
_running = false;
UpdateUi(Loc.GetString("space-villain-game-player-wins-message"),
Loc.GetString("space-villain-game-enemy-dies-message", ("enemyName", _enemyName)),
true);
SoundSystem.Play(_owner._winSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f));
_owner.ProcessWin();
return false;
}
if (_playerHp > 0 && _playerMp > 0) return true;
if ((_enemyHp > 0 && _enemyMp > 0))
{
_running = false;
UpdateUi(Loc.GetString("space-villain-game-player-loses-message"),
Loc.GetString("space-villain-game-enemy-cheers-message", ("enemyName", _enemyName)),
true);
SoundSystem.Play(_owner._gameOverSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f));
return false;
}
if (_enemyHp <= 0 || _enemyMp <= 0)
{
_running = false;
UpdateUi(Loc.GetString("space-villain-game-player-loses-message"),
Loc.GetString("space-villain-game-enemy-dies-with-player-message ", ("enemyName", _enemyName)),
true);
SoundSystem.Play(_owner._gameOverSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f));
return false;
}
return true;
}
/// <summary>
/// Updates the UI.
/// </summary>
private void UpdateUi(bool metadata = false)
{
_owner.UserInterface?.SendMessage(metadata ? GenerateMetaDataMessage() : GenerateUpdateMessage());
}
private void UpdateUi(string message1, string message2, bool metadata = false)
{
_latestPlayerActionMessage = message1;
_latestEnemyActionMessage = message2;
UpdateUi(metadata);
}
/// <summary>
/// Handles the logic of the AI
/// </summary>
/// <returns>An Enemyaction-message.</returns>
private void ExecuteAiAction()
{
if (_turtleTracker >= 4)
{
var boomAmount = _random.Next(5, 10);
_latestEnemyActionMessage = Loc.GetString("space-villain-game-enemy-throws-bomb-message",
("enemyName", _enemyName),
("damageReceived", boomAmount));
if (_owner.PlayerInvincibilityFlag) return;
_playerHp -= boomAmount;
_turtleTracker--;
}
else if (_enemyMp <= 5 && _random.Prob(0.7f))
{
var stealAmount = _random.Next(2, 3);
_latestEnemyActionMessage = Loc.GetString("space-villain-game-enemy-steals-player-power-message",
("enemyName", _enemyName),
("stolenAmount", stealAmount));
if (_owner.PlayerInvincibilityFlag) return;
_playerMp -= stealAmount;
_enemyMp += stealAmount;
}
else if (_enemyHp <= 10 && _enemyMp > 4)
{
_enemyHp += 4;
_enemyMp -= 4;
_latestEnemyActionMessage = Loc.GetString("space-villain-game-enemy-heals-message",
("enemyName", _enemyName),
("healedAmount", 4));
}
else
{
var attackAmount = _random.Next(3, 6);
_latestEnemyActionMessage =
Loc.GetString("space-villain-game-enemy-attacks-message",
("enemyName", _enemyName),
("damageDealt", attackAmount));
if (_owner.PlayerInvincibilityFlag) return;
_playerHp -= attackAmount;
}
}
/// <summary>
/// Generates a Metadata-message based on the objects values.
/// </summary>
/// <returns>A Metadata-message.</returns>
public SpaceVillainArcadeMetaDataUpdateMessage GenerateMetaDataMessage()
{
return new(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage, _latestEnemyActionMessage, Name, _enemyName, !_running);
}
/// <summary>
/// Creates an Update-message based on the objects values.
/// </summary>
/// <returns>An Update-Message.</returns>
public SpaceVillainArcadeDataUpdateMessage
GenerateUpdateMessage()
{
return new(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage,
_latestEnemyActionMessage);
}
}
}
}

View File

@@ -0,0 +1,124 @@
using Content.Shared.Arcade;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
// TODO: ECS.
namespace Content.Server.Arcade.SpaceVillain;
[RegisterComponent]
public sealed class SpaceVillainArcadeComponent : SharedSpaceVillainArcadeComponent
{
/// <summary>
/// Unused flag that can be hacked via wires.
/// Name suggests that it was intended to either make the health/mana values underflow while playing the game or turn the arcade machine into an infinite prize fountain.
/// </summary>
[ViewVariables]
public bool OverflowFlag;
/// <summary>
/// The current session of the SpaceVillain game for this arcade machine.
/// </summary>
[ViewVariables]
public SpaceVillainGame? Game = null;
/// <summary>
/// The sound played when a new session of the SpaceVillain game is begun.
/// </summary>
[DataField("newGameSound")]
public SoundSpecifier NewGameSound = new SoundPathSpecifier("/Audio/Effects/Arcade/newgame.ogg");
/// <summary>
/// The sound played when the player chooses to attack.
/// </summary>
[DataField("playerAttackSound")]
public SoundSpecifier PlayerAttackSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_attack.ogg");
/// <summary>
/// The sound played when the player chooses to heal.
/// </summary>
[DataField("playerHealSound")]
public SoundSpecifier PlayerHealSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_heal.ogg");
/// <summary>
/// The sound played when the player chooses to regain mana.
/// </summary>
[DataField("playerChargeSound")]
public SoundSpecifier PlayerChargeSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_charge.ogg");
/// <summary>
/// The sound played when the player wins.
/// </summary>
[DataField("winSound")]
public SoundSpecifier WinSound = new SoundPathSpecifier("/Audio/Effects/Arcade/win.ogg");
/// <summary>
/// The sound played when the player loses.
/// </summary>
[DataField("gameOverSound")]
public SoundSpecifier GameOverSound = new SoundPathSpecifier("/Audio/Effects/Arcade/gameover.ogg");
/// <summary>
/// The prefixes that can be used to create the game name.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("possibleFightVerbs")]
public List<string> PossibleFightVerbs = new()
{"Defeat", "Annihilate", "Save", "Strike", "Stop", "Destroy", "Robust", "Romance", "Pwn", "Own"};
/// <summary>
/// The first names/titles that can be used to construct the name of the villain.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("possibleFirstEnemyNames")]
public List<string> PossibleFirstEnemyNames = new(){
"the Automatic", "Farmer", "Lord", "Professor", "the Cuban", "the Evil", "the Dread King",
"the Space", "Lord", "the Great", "Duke", "General"
};
/// <summary>
/// The last names that can be used to construct the name of the villain.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("possibleLastEnemyNames")]
public List<string> PossibleLastEnemyNames = new()
{
"Melonoid", "Murdertron", "Sorcerer", "Ruin", "Jeff", "Ectoplasm", "Crushulon", "Uhangoid",
"Vhakoid", "Peteoid", "slime", "Griefer", "ERPer", "Lizard Man", "Unicorn"
};
/// <summary>
/// The prototypes that can be dispensed as a reward for winning the game.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("possibleRewards", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> PossibleRewards = new()
{
"ToyMouse", "ToyAi", "ToyNuke", "ToyAssistant", "ToyGriffin", "ToyHonk", "ToyIan",
"ToyMarauder", "ToyMauler", "ToyGygax", "ToyOdysseus", "ToyOwlman", "ToyDeathRipley",
"ToyPhazon", "ToyFireRipley", "ToyReticence", "ToyRipley", "ToySeraph", "ToyDurand", "ToySkeleton",
"FoamCrossbow", "RevolverCapGun", "PlushieHampter", "PlushieLizard", "PlushieAtmosian", "PlushieSpaceLizard",
"PlushieNuke", "PlushieCarp", "PlushieRatvar", "PlushieNar", "PlushieSnake", "Basketball", "Football",
"PlushieRouny", "PlushieBee", "PlushieSlime", "BalloonCorgi", "ToySword", "CrayonBox", "BoxDonkSoftBox", "BoxCartridgeCap",
"HarmonicaInstrument", "OcarinaInstrument", "RecorderInstrument", "GunpetInstrument", "BirdToyInstrument", "PlushieXeno"
};
/// <summary>
/// The minimum number of prizes the arcade machine can have.
/// </summary>
[DataField("rewardMinAmount")]
public int RewardMinAmount;
/// <summary>
/// The maximum number of prizes the arcade machine can have.
/// </summary>
[DataField("rewardMaxAmount")]
public int RewardMaxAmount;
/// <summary>
/// The remaining number of prizes the arcade machine can dispense.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int RewardAmount = 0;
}

View File

@@ -0,0 +1,109 @@
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Random;
namespace Content.Server.Arcade.SpaceVillain;
public sealed partial class SpaceVillainArcadeSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
SubscribeLocalEvent<SpaceVillainArcadeComponent, SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower);
}
/// <summary>
/// Called when the user wins the game.
/// Dispenses a prize if the arcade machine has any left.
/// </summary>
/// <param name="uid"></param>
/// <param name="arcade"></param>
/// <param name="xform"></param>
public void ProcessWin(EntityUid uid, SpaceVillainArcadeComponent? arcade = null, TransformComponent? xform = null)
{
if (!Resolve(uid, ref arcade, ref xform))
return;
if (arcade.RewardAmount <= 0)
return;
EntityManager.SpawnEntity(_random.Pick(arcade.PossibleRewards), xform.Coordinates);
arcade.RewardAmount--;
}
/// <summary>
/// Picks a fight-verb from the list of possible Verbs.
/// </summary>
/// <returns>A fight-verb.</returns>
public string GenerateFightVerb(SpaceVillainArcadeComponent arcade)
{
return _random.Pick(arcade.PossibleFightVerbs);
}
/// <summary>
/// Generates an enemy-name comprised of a first- and last-name.
/// </summary>
/// <returns>An enemy-name.</returns>
public string GenerateEnemyName(SpaceVillainArcadeComponent arcade)
{
return $"{_random.Pick(arcade.PossibleFirstEnemyNames)} {_random.Pick(arcade.PossibleLastEnemyNames)}";
}
private void OnComponentInit(EntityUid uid, SpaceVillainArcadeComponent component, ComponentInit args)
{
// Random amount of prizes
component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1);
}
private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SpaceVillainArcadePlayerActionMessage msg)
{
if (component.Game == null)
return;
if (!TryComp<ApcPowerReceiverComponent>(uid, out var power) || !power.Powered)
return;
switch (msg.PlayerAction)
{
case PlayerAction.Attack:
case PlayerAction.Heal:
case PlayerAction.Recharge:
component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component);
break;
case PlayerAction.NewGame:
_audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f));
component.Game = new SpaceVillainGame(uid, component, this);
if (_uiSystem.TryGetUi(uid, SpaceVillainArcadeUiKey.Key, out var bui))
_uiSystem.SendUiMessage(bui, component.Game.GenerateMetaDataMessage());
break;
case PlayerAction.RequestData:
if (_uiSystem.TryGetUi(uid, SpaceVillainArcadeUiKey.Key, out bui))
_uiSystem.SendUiMessage(bui, component.Game.GenerateMetaDataMessage());
break;
}
}
private void OnAfterUIOpenSV(EntityUid uid, SpaceVillainArcadeComponent component, AfterActivatableUIOpenEvent args)
{
component.Game ??= new(uid, component, this);
}
private void OnSVillainPower(EntityUid uid, SpaceVillainArcadeComponent component, ref PowerChangedEvent args)
{
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered)
return;
if (_uiSystem.TryGetUi(uid, SpaceVillainArcadeUiKey.Key, out var bui))
_uiSystem.CloseAll(bui);
}
}

View File

@@ -0,0 +1,68 @@
namespace Content.Server.Arcade.SpaceVillain;
public sealed partial class SpaceVillainGame
{
/// <summary>
/// A state holder for the fighters in the SpaceVillain game.
/// </summary>
public sealed class Fighter
{
/// <summary>
/// The current hit point total of the fighter.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int Hp
{
get => _hp;
set => _hp = MathHelper.Clamp(value, 0, HpMax);
}
private int _hp;
/// <summary>
/// The maximum hit point total of the fighter.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int HpMax
{
get => _hpMax;
set
{
_hpMax = Math.Max(value, 0);
Hp = MathHelper.Clamp(Hp, 0, HpMax);
}
}
private int _hpMax;
/// <summary>
/// The current mana total of the fighter.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int Mp
{
get => _mp;
set => _mp = MathHelper.Clamp(value, 0, MpMax);
}
private int _mp;
/// <summary>
/// The maximum mana total of the fighter.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int MpMax
{
get => _mpMax;
set
{
_mpMax = Math.Max(value, 0);
Mp = MathHelper.Clamp(Mp, 0, MpMax);
}
}
private int _mpMax;
/// <summary>
/// Whether the given fighter can take damage/lose mana.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Invincible = false;
}
}

View File

@@ -0,0 +1,53 @@
using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
namespace Content.Server.Arcade.SpaceVillain;
public sealed partial class SpaceVillainGame
{
/// <summary>
/// Updates the UI.
/// </summary>
private void UpdateUi(EntityUid uid, bool metadata = false)
{
if (_uiSystem.TryGetUi(uid, SpaceVillainArcadeUiKey.Key, out var bui))
_uiSystem.SendUiMessage(bui, metadata ? GenerateMetaDataMessage() : GenerateUpdateMessage());
}
private void UpdateUi(EntityUid uid, string message1, string message2, bool metadata = false)
{
_latestPlayerActionMessage = message1;
_latestEnemyActionMessage = message2;
UpdateUi(uid, metadata);
}
/// <summary>
/// Generates a Metadata-message based on the objects values.
/// </summary>
/// <returns>A Metadata-message.</returns>
public SpaceVillainArcadeMetaDataUpdateMessage GenerateMetaDataMessage()
{
return new(
PlayerChar.Hp, PlayerChar.Mp,
VillainChar.Hp, VillainChar.Mp,
_latestPlayerActionMessage,
_latestEnemyActionMessage,
Name,
_villainName,
!_running
);
}
/// <summary>
/// Creates an Update-message based on the objects values.
/// </summary>
/// <returns>An Update-Message.</returns>
public SpaceVillainArcadeDataUpdateMessage GenerateUpdateMessage()
{
return new(
PlayerChar.Hp, PlayerChar.Mp,
VillainChar.Hp, VillainChar.Mp,
_latestPlayerActionMessage,
_latestEnemyActionMessage
);
}
}

View File

@@ -0,0 +1,252 @@
using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Random;
namespace Content.Server.Arcade.SpaceVillain;
/// <summary>
/// A Class to handle all the game-logic of the SpaceVillain-game.
/// </summary>
public sealed partial class SpaceVillainGame
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private readonly SharedAudioSystem _audioSystem = default!;
private readonly UserInterfaceSystem _uiSystem = default!;
private readonly SpaceVillainArcadeSystem _svArcade = default!;
[ViewVariables]
private readonly EntityUid _owner = default!;
[ViewVariables]
private bool _running = true;
[ViewVariables]
public string Name => $"{_fightVerb} {_villainName}";
[ViewVariables]
private readonly string _fightVerb;
[ViewVariables]
public readonly Fighter PlayerChar;
[ViewVariables]
private readonly string _villainName;
[ViewVariables]
public readonly Fighter VillainChar;
[ViewVariables]
private int _turtleTracker = 0;
[ViewVariables]
private string _latestPlayerActionMessage = "";
[ViewVariables]
private string _latestEnemyActionMessage = "";
public SpaceVillainGame(EntityUid owner, SpaceVillainArcadeComponent arcade, SpaceVillainArcadeSystem arcadeSystem)
: this(owner, arcade, arcadeSystem, arcadeSystem.GenerateFightVerb(arcade), arcadeSystem.GenerateEnemyName(arcade))
{
}
public SpaceVillainGame(EntityUid owner, SpaceVillainArcadeComponent arcade, SpaceVillainArcadeSystem arcadeSystem, string fightVerb, string enemyName)
{
IoCManager.InjectDependencies(this);
_audioSystem = _entityManager.System<SharedAudioSystem>();
_uiSystem = _entityManager.System<UserInterfaceSystem>();
_svArcade = _entityManager.System<SpaceVillainArcadeSystem>();
_owner = owner;
//todo defeat the curse secret game mode
_fightVerb = fightVerb;
_villainName = enemyName;
PlayerChar = new()
{
HpMax = 30,
Hp = 30,
MpMax = 10,
Mp = 10
};
VillainChar = new()
{
HpMax = 45,
Hp = 45,
MpMax = 20,
Mp = 20
};
}
/// <summary>
/// Called by the SpaceVillainArcadeComponent when Userinput is received.
/// </summary>
/// <param name="uid">The action the user picked.</param>
/// <param name="action">The action the user picked.</param>
/// <param name="arcade">The action the user picked.</param>
public void ExecutePlayerAction(EntityUid uid, PlayerAction action, SpaceVillainArcadeComponent arcade)
{
if (!_running)
return;
switch (action)
{
case PlayerAction.Attack:
var attackAmount = _random.Next(2, 6);
_latestPlayerActionMessage = Loc.GetString(
"space-villain-game-player-attack-message",
("enemyName", _villainName),
("attackAmount", attackAmount)
);
_audioSystem.PlayPvs(arcade.PlayerAttackSound, uid, AudioParams.Default.WithVolume(-4f));
if (!VillainChar.Invincible)
VillainChar.Hp -= attackAmount;
_turtleTracker -= _turtleTracker > 0 ? 1 : 0;
break;
case PlayerAction.Heal:
var pointAmount = _random.Next(1, 3);
var healAmount = _random.Next(6, 8);
_latestPlayerActionMessage = Loc.GetString(
"space-villain-game-player-heal-message",
("magicPointAmount", pointAmount),
("healAmount", healAmount)
);
_audioSystem.PlayPvs(arcade.PlayerHealSound, uid, AudioParams.Default.WithVolume(-4f));
if (!PlayerChar.Invincible)
PlayerChar.Mp -= pointAmount;
PlayerChar.Hp += healAmount;
_turtleTracker++;
break;
case PlayerAction.Recharge:
var chargeAmount = _random.Next(4, 7);
_latestPlayerActionMessage = Loc.GetString(
"space-villain-game-player-recharge-message",
("regainedPoints", chargeAmount)
);
_audioSystem.PlayPvs(arcade.PlayerChargeSound, uid, AudioParams.Default.WithVolume(-4f));
PlayerChar.Mp += chargeAmount;
_turtleTracker -= _turtleTracker > 0 ? 1 : 0;
break;
}
if (!CheckGameConditions(uid, arcade))
return;
ExecuteAiAction();
if (!CheckGameConditions(uid, arcade))
return;
UpdateUi(uid);
}
/// <summary>
/// Handles the logic of the AI
/// </summary>
private void ExecuteAiAction()
{
if (_turtleTracker >= 4)
{
var boomAmount = _random.Next(5, 10);
_latestEnemyActionMessage = Loc.GetString(
"space-villain-game-enemy-throws-bomb-message",
("enemyName", _villainName),
("damageReceived", boomAmount)
);
if (PlayerChar.Invincible)
return;
PlayerChar.Hp -= boomAmount;
_turtleTracker--;
return;
}
if (VillainChar.Mp <= 5 && _random.Prob(0.7f))
{
var stealAmount = _random.Next(2, 3);
_latestEnemyActionMessage = Loc.GetString(
"space-villain-game-enemy-steals-player-power-message",
("enemyName", _villainName),
("stolenAmount", stealAmount)
);
if (PlayerChar.Invincible)
return;
PlayerChar.Mp -= stealAmount;
VillainChar.Mp += stealAmount;
return;
}
if (VillainChar.Hp <= 10 && VillainChar.Mp > 4)
{
VillainChar.Hp += 4;
VillainChar.Mp -= 4;
_latestEnemyActionMessage = Loc.GetString(
"space-villain-game-enemy-heals-message",
("enemyName", _villainName),
("healedAmount", 4)
);
return;
}
var attackAmount = _random.Next(3, 6);
_latestEnemyActionMessage =
Loc.GetString(
"space-villain-game-enemy-attacks-message",
("enemyName", _villainName),
("damageDealt", attackAmount)
);
if (PlayerChar.Invincible)
return;
PlayerChar.Hp -= attackAmount;
}
/// <summary>
/// Checks the Game conditions and Updates the Ui & Plays a sound accordingly.
/// </summary>
/// <returns>A bool indicating if the game should continue.</returns>
private bool CheckGameConditions(EntityUid uid, SpaceVillainArcadeComponent arcade)
{
switch (
PlayerChar.Hp > 0 && PlayerChar.Mp > 0,
VillainChar.Hp > 0 && VillainChar.Mp > 0
)
{
case (true, true):
return true;
case (true, false):
_running = false;
UpdateUi(
uid,
Loc.GetString("space-villain-game-player-wins-message"),
Loc.GetString("space-villain-game-enemy-dies-message", ("enemyName", _villainName)),
true
);
_audioSystem.PlayPvs(arcade.WinSound, uid, AudioParams.Default.WithVolume(-4f));
_svArcade.ProcessWin(uid, arcade);
return false;
case (false, true):
_running = false;
UpdateUi(
uid,
Loc.GetString("space-villain-game-player-loses-message"),
Loc.GetString("space-villain-game-enemy-cheers-message", ("enemyName", _villainName)),
true
);
_audioSystem.PlayPvs(arcade.GameOverSound, uid, AudioParams.Default.WithVolume(-4f));
return false;
case (false, false):
_running = false;
UpdateUi(
uid,
Loc.GetString("space-villain-game-player-loses-message"),
Loc.GetString("space-villain-game-enemy-dies-with-player-message ", ("enemyName", _villainName)),
true
);
_audioSystem.PlayPvs(arcade.GameOverSound, uid, AudioParams.Default.WithVolume(-4f));
return false;
}
}
}

View File

@@ -1,4 +1,4 @@
using Content.Server.Arcade.Components;
using Content.Server.Arcade.SpaceVillain;
using Content.Server.Wires;
using Content.Shared.Arcade;
using Content.Shared.Wires;
@@ -15,23 +15,26 @@ public sealed class ArcadePlayerInvincibleWireAction : BaseToggleWireAction
public override void ToggleValue(EntityUid owner, bool setting)
{
if (EntityManager.TryGetComponent<SpaceVillainArcadeComponent>(owner, out var arcade))
if (EntityManager.TryGetComponent<SpaceVillainArcadeComponent>(owner, out var arcade)
&& arcade.Game != null)
{
arcade.PlayerInvincibilityFlag = !setting;
arcade.Game.PlayerChar.Invincible = !setting;
}
}
public override bool GetValue(EntityUid owner)
{
return EntityManager.TryGetComponent<SpaceVillainArcadeComponent>(owner, out var arcade)
&& !arcade.PlayerInvincibilityFlag;
&& arcade.Game != null
&& !arcade.Game.PlayerChar.Invincible;
}
public override StatusLightState? GetLightState(Wire wire)
{
if (EntityManager.TryGetComponent<SpaceVillainArcadeComponent>(wire.Owner, out var arcade))
if (EntityManager.TryGetComponent<SpaceVillainArcadeComponent>(wire.Owner, out var arcade)
&& arcade.Game != null)
{
return arcade.PlayerInvincibilityFlag || arcade.EnemyInvincibilityFlag
return arcade.Game.PlayerChar.Invincible || arcade.Game.VillainChar.Invincible
? StatusLightState.BlinkingSlow
: StatusLightState.On;
}
@@ -49,19 +52,24 @@ public sealed class ArcadeEnemyInvincibleWireAction : BaseToggleWireAction
public override void ToggleValue(EntityUid owner, bool setting)
{
if (EntityManager.TryGetComponent<SpaceVillainArcadeComponent>(owner, out var arcade))
if (EntityManager.TryGetComponent<SpaceVillainArcadeComponent>(owner, out var arcade)
&& arcade.Game != null)
{
arcade.PlayerInvincibilityFlag = !setting;
arcade.Game.VillainChar.Invincible = !setting;
}
}
public override bool GetValue(EntityUid owner)
{
return EntityManager.TryGetComponent<SpaceVillainArcadeComponent>(owner, out var arcade)
&& !arcade.PlayerInvincibilityFlag;
&& arcade.Game != null
&& !arcade.Game.VillainChar.Invincible;
}
public override StatusLightData? GetStatusLightData(Wire wire) => null;
public override StatusLightData? GetStatusLightData(Wire wire)
{
return null;
}
}
public enum ArcadeInvincibilityWireActionKeys : short

View File

@@ -1,4 +1,4 @@
using Content.Server.Arcade.Components;
using Content.Server.Arcade.SpaceVillain;
using Content.Server.Wires;
using Content.Shared.Arcade;
using Content.Shared.Wires;

View File

@@ -71,7 +71,8 @@ namespace Content.Shared.Arcade
}
}
public static class BlockGameVector2Extensions{
public static class BlockGameVector2Extensions
{
public static BlockGameBlock ToBlockGameBlock(this Vector2i vector2, BlockGameBlock.BlockGameBlockColor gameBlockColor)
{
return new(vector2, gameBlockColor);
@@ -90,6 +91,5 @@ namespace Content.Shared.Arcade
{
return new(-vector.Y, vector.X);
}
}
}