Nanotrasen Block Game is here (#2131)

* tetris!

* softdropping & left,right key holding

* started work on the ui

* playable state

* there you go exp

* multiuser rework

* ui update refactor

* blockgame™️

* highscores, keybindings, ui refactor

* speed adjusts
leveling

* highscorebackground tweak
speed tweak

* NULLABLE

* yes
This commit is contained in:
Paul Ritter
2020-09-26 15:25:22 +02:00
committed by GitHub
parent 4eb5891c4a
commit 84ce845ea3
14 changed files with 2013 additions and 0 deletions

View File

@@ -0,0 +1,691 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using Content.Client.GameObjects.Components.Arcade;
using Content.Client.Utility;
using Content.Shared.Arcade;
using Content.Shared.GameObjects.Components.Arcade;
using Content.Shared.Input;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.Client.Arcade
{
public class BlockGameMenu : SS14Window
{
private static Color overlayBackgroundColor = new Color(74,74,81,180);
private static Color overlayShadowColor = new Color(0,0,0,83);
private static Vector2 blockSize = new Vector2(15,15);
private BlockGameBoundUserInterface _owner;
private PanelContainer _mainPanel;
private VBoxContainer _gameRootContainer;
private GridContainer _gameGrid;
private GridContainer _nextBlockGrid;
private GridContainer _holdBlockGrid;
private Label _pointsLabel;
private Label _levelLabel;
private Button _pauseButton;
private PanelContainer _menuRootContainer;
private Button _unpauseButton;
private Control _unpauseButtonMargin;
private Button _newGameButton;
private Button _scoreBoardButton;
private PanelContainer _gameOverRootContainer;
private Label _finalScoreLabel;
private Button _finalNewGameButton;
private PanelContainer _highscoresRootContainer;
private Label _localHighscoresLabel;
private Label _globalHighscoresLabel;
private Button _highscoreBackButton;
private bool _isPlayer = false;
private bool _gameOver = false;
public BlockGameMenu(BlockGameBoundUserInterface owner)
{
Title = "Nanotrasen Block Game";
_owner = owner;
var resourceCache = IoCManager.Resolve<IResourceCache>();
var backgroundTexture = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
_mainPanel = new PanelContainer();
SetupGameMenu(backgroundTexture);
_mainPanel.AddChild(_gameRootContainer);
SetupPauseMenu(backgroundTexture);
SetupGameoverScreen(backgroundTexture);
SetupHighScoreScreen(backgroundTexture);
Contents.AddChild(_mainPanel);
CanKeyboardFocus = true;
}
private void SetupHighScoreScreen(Texture backgroundTexture)
{
var rootBack = new StyleBoxTexture
{
Texture = backgroundTexture,
Modulate = overlayShadowColor
};
rootBack.SetPatchMargin(StyleBox.Margin.All, 10);
_highscoresRootContainer = new PanelContainer
{
PanelOverride = rootBack,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsHorizontal = SizeFlags.ShrinkCenter
};
var c = new Color(overlayBackgroundColor.R,overlayBackgroundColor.G,overlayBackgroundColor.B,220);
var innerBack = new StyleBoxTexture
{
Texture = backgroundTexture,
Modulate = c
};
innerBack.SetPatchMargin(StyleBox.Margin.All, 10);
var menuInnerPanel = new PanelContainer
{
PanelOverride = innerBack,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsHorizontal = SizeFlags.ShrinkCenter
};
_highscoresRootContainer.AddChild(menuInnerPanel);
var menuContainer = new VBoxContainer()
{
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter
};
menuContainer.AddChild(new Label{Text = "Highscores"});
menuContainer.AddChild(new Control{CustomMinimumSize = new Vector2(1,10)});
var highScoreBox = new HBoxContainer();
_localHighscoresLabel = new Label
{
Align = Label.AlignMode.Center
};
highScoreBox.AddChild(_localHighscoresLabel);
highScoreBox.AddChild(new Control{CustomMinimumSize = new Vector2(40,1)});
_globalHighscoresLabel = new Label
{
Align = Label.AlignMode.Center
};
highScoreBox.AddChild(_globalHighscoresLabel);
menuContainer.AddChild(highScoreBox);
menuContainer.AddChild(new Control{CustomMinimumSize = new Vector2(1,10)});
_highscoreBackButton = new Button
{
Text = "Back",
TextAlign = Label.AlignMode.Center
};
_highscoreBackButton.OnPressed += (e) => _owner.SendAction(BlockGamePlayerAction.Pause);
menuContainer.AddChild(_highscoreBackButton);
menuInnerPanel.AddChild(menuContainer);
}
private void SetupGameoverScreen(Texture backgroundTexture)
{
var rootBack = new StyleBoxTexture
{
Texture = backgroundTexture,
Modulate = overlayShadowColor
};
rootBack.SetPatchMargin(StyleBox.Margin.All, 10);
_gameOverRootContainer = new PanelContainer
{
PanelOverride = rootBack,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsHorizontal = SizeFlags.ShrinkCenter
};
var innerBack = new StyleBoxTexture
{
Texture = backgroundTexture,
Modulate = overlayBackgroundColor
};
innerBack.SetPatchMargin(StyleBox.Margin.All, 10);
var menuInnerPanel = new PanelContainer
{
PanelOverride = innerBack,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsHorizontal = SizeFlags.ShrinkCenter
};
_gameOverRootContainer.AddChild(menuInnerPanel);
var menuContainer = new VBoxContainer
{
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter
};
menuContainer.AddChild(new Label{Text = "Gameover!",Align = Label.AlignMode.Center});
menuContainer.AddChild(new Control{CustomMinimumSize = new Vector2(1,10)});
_finalScoreLabel = new Label{Align = Label.AlignMode.Center};
menuContainer.AddChild(_finalScoreLabel);
menuContainer.AddChild(new Control{CustomMinimumSize = new Vector2(1,10)});
_finalNewGameButton = new Button
{
Text = "New Game",
TextAlign = Label.AlignMode.Center
};
_finalNewGameButton.OnPressed += (e) =>
{
_owner.SendAction(BlockGamePlayerAction.NewGame);
};
menuContainer.AddChild(_finalNewGameButton);
menuInnerPanel.AddChild(menuContainer);
}
private void SetupPauseMenu(Texture backgroundTexture)
{
var rootBack = new StyleBoxTexture
{
Texture = backgroundTexture,
Modulate = overlayShadowColor
};
rootBack.SetPatchMargin(StyleBox.Margin.All, 10);
_menuRootContainer = new PanelContainer
{
PanelOverride = rootBack,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsHorizontal = SizeFlags.ShrinkCenter
};
var innerBack = new StyleBoxTexture
{
Texture = backgroundTexture,
Modulate = overlayBackgroundColor
};
innerBack.SetPatchMargin(StyleBox.Margin.All, 10);
var menuInnerPanel = new PanelContainer
{
PanelOverride = innerBack,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsHorizontal = SizeFlags.ShrinkCenter
};
_menuRootContainer.AddChild(menuInnerPanel);
var menuContainer = new VBoxContainer
{
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter
};
_newGameButton = new Button
{
Text = "New Game",
TextAlign = Label.AlignMode.Center
};
_newGameButton.OnPressed += (e) =>
{
_owner.SendAction(BlockGamePlayerAction.NewGame);
};
menuContainer.AddChild(_newGameButton);
menuContainer.AddChild(new Control{CustomMinimumSize = new Vector2(1,10)});
_scoreBoardButton = new Button
{
Text = "Scoreboard",
TextAlign = Label.AlignMode.Center
};
_scoreBoardButton.OnPressed += (e) => _owner.SendAction(BlockGamePlayerAction.ShowHighscores);
menuContainer.AddChild(_scoreBoardButton);
_unpauseButtonMargin = new Control {CustomMinimumSize = new Vector2(1, 10), Visible = false};
menuContainer.AddChild(_unpauseButtonMargin);
_unpauseButton = new Button
{
Text = "Unpause",
TextAlign = Label.AlignMode.Center,
Visible = false
};
_unpauseButton.OnPressed += (e) =>
{
_owner.SendAction(BlockGamePlayerAction.Unpause);
};
menuContainer.AddChild(_unpauseButton);
menuInnerPanel.AddChild(menuContainer);
}
public void SetUsability(bool isPlayer)
{
_isPlayer = isPlayer;
UpdateUsability();
}
private void UpdateUsability()
{
_pauseButton.Disabled = !_isPlayer;
_newGameButton.Disabled = !_isPlayer;
_scoreBoardButton.Disabled = !_isPlayer;
_unpauseButton.Disabled = !_isPlayer;
_finalNewGameButton.Disabled = !_isPlayer;
_highscoreBackButton.Disabled = !_isPlayer;
}
private void SetupGameMenu(Texture backgroundTexture)
{
// building the game container
_gameRootContainer = new VBoxContainer();
_levelLabel = new Label
{
Align = Label.AlignMode.Center,
SizeFlagsHorizontal = SizeFlags.FillExpand
};
_gameRootContainer.AddChild(_levelLabel);
_gameRootContainer.AddChild(new Control
{
CustomMinimumSize = new Vector2(1,5)
});
_pointsLabel = new Label
{
Align = Label.AlignMode.Center,
SizeFlagsHorizontal = SizeFlags.FillExpand
};
_gameRootContainer.AddChild(_pointsLabel);
_gameRootContainer.AddChild(new Control
{
CustomMinimumSize = new Vector2(1,10)
});
var gameBox = new HBoxContainer();
gameBox.AddChild(SetupHoldBox(backgroundTexture));
gameBox.AddChild(new Control
{
CustomMinimumSize = new Vector2(10,1)
});
gameBox.AddChild(SetupGameGrid(backgroundTexture));
gameBox.AddChild(new Control
{
CustomMinimumSize = new Vector2(10,1)
});
gameBox.AddChild(SetupNextBox(backgroundTexture));
_gameRootContainer.AddChild(gameBox);
_gameRootContainer.AddChild(new Control
{
CustomMinimumSize = new Vector2(1,10)
});
_pauseButton = new Button
{
Text = "Pause",
TextAlign = Label.AlignMode.Center
};
_pauseButton.OnPressed += (e) => TryPause();
_gameRootContainer.AddChild(_pauseButton);
}
private Control SetupGameGrid(Texture panelTex)
{
_gameGrid = new GridContainer
{
Columns = 10,
HSeparationOverride = 1,
VSeparationOverride = 1
};
UpdateBlocks(new BlockGameBlock[0]);
var back = new StyleBoxTexture
{
Texture = panelTex,
Modulate = Color.FromHex("#4a4a51"),
};
back.SetPatchMargin(StyleBox.Margin.All, 10);
var gamePanel = new PanelContainer
{
PanelOverride = back,
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsStretchRatio = 60
};
var backgroundPanel = new PanelContainer
{
PanelOverride = new StyleBoxFlat{BackgroundColor = Color.FromHex("#86868d")}
};
backgroundPanel.AddChild(_gameGrid);
gamePanel.AddChild(backgroundPanel);
return gamePanel;
}
private Control SetupNextBox(Texture panelTex)
{
var previewBack = new StyleBoxTexture
{
Texture = panelTex,
Modulate = Color.FromHex("#4a4a51")
};
previewBack.SetPatchMargin(StyleBox.Margin.All, 10);
var grid = new GridContainer
{
Columns = 1,
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsStretchRatio = 20
};
var nextBlockPanel = new PanelContainer
{
PanelOverride = previewBack,
CustomMinimumSize = blockSize * 6.5f,
SizeFlagsHorizontal = SizeFlags.None,
SizeFlagsVertical = SizeFlags.None
};
var nextCenterContainer = new CenterContainer();
_nextBlockGrid = new GridContainer
{
HSeparationOverride = 1,
VSeparationOverride = 1
};
nextCenterContainer.AddChild(_nextBlockGrid);
nextBlockPanel.AddChild(nextCenterContainer);
grid.AddChild(nextBlockPanel);
grid.AddChild(new Label{Text = "Next", Align = Label.AlignMode.Center});
return grid;
}
private Control SetupHoldBox(Texture panelTex)
{
var previewBack = new StyleBoxTexture
{
Texture = panelTex,
Modulate = Color.FromHex("#4a4a51")
};
previewBack.SetPatchMargin(StyleBox.Margin.All, 10);
var grid = new GridContainer
{
Columns = 1,
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsStretchRatio = 20
};
var holdBlockPanel = new PanelContainer
{
PanelOverride = previewBack,
CustomMinimumSize = blockSize * 6.5f,
SizeFlagsHorizontal = SizeFlags.None,
SizeFlagsVertical = SizeFlags.None
};
var holdCenterContainer = new CenterContainer();
_holdBlockGrid = new GridContainer
{
HSeparationOverride = 1,
VSeparationOverride = 1
};
holdCenterContainer.AddChild(_holdBlockGrid);
holdBlockPanel.AddChild(holdCenterContainer);
grid.AddChild(holdBlockPanel);
grid.AddChild(new Label{Text = "Hold", Align = Label.AlignMode.Center});
return grid;
}
protected override void FocusExited()
{
if (!IsOpen) return;
if(_gameOver) return;
TryPause();
}
private void TryPause()
{
_owner.SendAction(BlockGamePlayerAction.Pause);
}
public void SetStarted()
{
_gameOver = false;
_unpauseButton.Visible = true;
_unpauseButtonMargin.Visible = true;
}
public void SetScreen(BlockGameMessages.BlockGameScreen screen)
{
if (_gameOver) return;
switch (screen)
{
case BlockGameMessages.BlockGameScreen.Game:
GrabKeyboardFocus();
CloseMenus();
_pauseButton.Disabled = !_isPlayer;
break;
case BlockGameMessages.BlockGameScreen.Pause:
//ReleaseKeyboardFocus();
CloseMenus();
_mainPanel.AddChild(_menuRootContainer);
_pauseButton.Disabled = true;
break;
case BlockGameMessages.BlockGameScreen.Gameover:
_gameOver = true;
_pauseButton.Disabled = true;
//ReleaseKeyboardFocus();
CloseMenus();
_mainPanel.AddChild(_gameOverRootContainer);
break;
case BlockGameMessages.BlockGameScreen.Highscores:
//ReleaseKeyboardFocus();
CloseMenus();
_mainPanel.AddChild(_highscoresRootContainer);
break;
}
}
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);
}
public void SetGameoverInfo(int amount, int? localPlacement, int? globalPlacement)
{
var globalPlacementText = globalPlacement == null ? "-" : $"#{globalPlacement}";
var localPlacementText = localPlacement == null ? "-" : $"#{localPlacement}";
_finalScoreLabel.Text = $"Global: {globalPlacementText}\nLocal: {localPlacementText}\nPoints: {amount}";
}
public void UpdatePoints(int points)
{
_pointsLabel.Text = $"Points: {points}";
}
public void UpdateLevel(int level)
{
_levelLabel.Text = $"Level {level + 1}";
}
public void UpdateHighscores(List<BlockGameMessages.HighScoreEntry> localHighscores,
List<BlockGameMessages.HighScoreEntry> globalHighscores)
{
var localHighscoreText = "Station:\n";
var globalHighscoreText = "Nanotrasen:\n";
for (int i = 0; i < 5; i++)
{
localHighscoreText += $"#{i + 1} " + (localHighscores.Count > i
? $"{localHighscores[i].Name} - {localHighscores[i].Score}\n" : "??? - 0\n");
globalHighscoreText += $"#{i + 1} " + (globalHighscores.Count > i
? $"{globalHighscores[i].Name} - {globalHighscores[i].Score}\n" : "??? - 0\n");
}
_localHighscoresLabel.Text = localHighscoreText;
_globalHighscoresLabel.Text = globalHighscoreText;
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
if(!_isPlayer) return;
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)
{
if(!_isPlayer) return;
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)
{
_owner.SendAction(BlockGamePlayerAction.SoftdropEnd);
}
}
public void UpdateNextBlock(BlockGameBlock[] blocks)
{
_nextBlockGrid.RemoveAllChildren();
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 (int x = 0; x < columnCount; x++)
{
var c = GetColorForPosition(blocks, x, y);
_nextBlockGrid.AddChild(new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = c},
CustomMinimumSize = blockSize,
RectDrawClipMargin = 0
});
}
}
}
public void UpdateHeldBlock(BlockGameBlock[] blocks)
{
_holdBlockGrid.RemoveAllChildren();
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 (int x = 0; x < columnCount; x++)
{
var c = GetColorForPosition(blocks, x, y);
_holdBlockGrid.AddChild(new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = c},
CustomMinimumSize = blockSize,
RectDrawClipMargin = 0
});
}
}
}
public void UpdateBlocks(BlockGameBlock[] blocks)
{
_gameGrid.RemoveAllChildren();
for (int y = 0; y < 20; y++)
{
for (int x = 0; x < 10; x++)
{
var c = GetColorForPosition(blocks, x, y);
_gameGrid.AddChild(new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = c},
CustomMinimumSize = blockSize,
RectDrawClipMargin = 0
});
}
}
}
private Color GetColorForPosition(BlockGameBlock[] blocks, int x, int y)
{
Color c = Color.Transparent;
var matchingBlock = blocks.FirstOrNull(b => b.Position.X == x && b.Position.Y == y);
if (matchingBlock.HasValue)
{
c = matchingBlock.Value.GameBlockColor switch
{
BlockGameBlock.BlockGameBlockColor.Red => Color.Red,
BlockGameBlock.BlockGameBlockColor.Orange => Color.Orange,
BlockGameBlock.BlockGameBlockColor.Yellow => Color.Yellow,
BlockGameBlock.BlockGameBlockColor.Green => Color.LimeGreen,
BlockGameBlock.BlockGameBlockColor.Blue => Color.Blue,
BlockGameBlock.BlockGameBlockColor.Purple => Color.Purple,
BlockGameBlock.BlockGameBlockColor.LightBlue => Color.LightBlue,
_ => Color.Olive //olive is error
};
}
return c;
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using Content.Client.Arcade;
using Content.Shared.Arcade;
using Content.Shared.GameObjects.Components.Arcade;
using JetBrains.Annotations;
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects.Components.UserInterface;
namespace Content.Client.GameObjects.Components.Arcade
{
public class BlockGameBoundUserInterface : BoundUserInterface
{
private BlockGameMenu _menu;
public BlockGameBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] object uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_menu = new BlockGameMenu(this);
_menu.OnClose += () => SendMessage(new BlockGameMessages.BlockGameUserUnregisterMessage());
_menu.OnClose += Close;
_menu.OpenCentered();
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
switch (message)
{
case BlockGameMessages.BlockGameVisualUpdateMessage updateMessage:
switch (updateMessage.GameVisualType)
{
case BlockGameMessages.BlockGameVisualType.GameField:
_menu?.UpdateBlocks(updateMessage.Blocks);
break;
case BlockGameMessages.BlockGameVisualType.HoldBlock:
_menu?.UpdateHeldBlock(updateMessage.Blocks);
break;
case BlockGameMessages.BlockGameVisualType.NextBlock:
_menu?.UpdateNextBlock(updateMessage.Blocks);
break;
}
break;
case BlockGameMessages.BlockGameScoreUpdateMessage scoreUpdate:
_menu?.UpdatePoints(scoreUpdate.Points);
break;
case BlockGameMessages.BlockGameUserStatusMessage userMessage:
_menu?.SetUsability(userMessage.IsPlayer);
break;
case BlockGameMessages.BlockGameSetScreenMessage statusMessage:
if (statusMessage.isStarted) _menu?.SetStarted();
_menu?.SetScreen(statusMessage.Screen);
if (statusMessage is BlockGameMessages.BlockGameGameOverScreenMessage gameOverScreenMessage)
_menu?.SetGameoverInfo(gameOverScreenMessage.FinalScore, gameOverScreenMessage.LocalPlacement, gameOverScreenMessage.GlobalPlacement);
break;
case BlockGameMessages.BlockGameHighScoreUpdateMessage highScoreUpdateMessage:
_menu?.UpdateHighscores(highScoreUpdateMessage.LocalHighscores,
highScoreUpdateMessage.GlobalHighscores);
break;
case BlockGameMessages.BlockGameLevelUpdateMessage levelUpdateMessage:
_menu?.UpdateLevel(levelUpdateMessage.Level);
break;
}
}
public void SendAction(BlockGamePlayerAction action)
{
SendMessage(new BlockGameMessages.BlockGamePlayerActionMessage(action));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if(!disposing) { return; }
_menu?.Dispose();
}
}
}

View File

@@ -174,6 +174,7 @@
"Flammable", "Flammable",
"CreamPie", "CreamPie",
"CreamPied", "CreamPied",
"BlockGameArcade"
}; };
} }
} }

View File

@@ -39,6 +39,13 @@ namespace Content.Client.Input
human.AddFunction(ContentKeyFunctions.MouseMiddle); human.AddFunction(ContentKeyFunctions.MouseMiddle);
human.AddFunction(ContentKeyFunctions.ToggleCombatMode); human.AddFunction(ContentKeyFunctions.ToggleCombatMode);
human.AddFunction(ContentKeyFunctions.WideAttack); human.AddFunction(ContentKeyFunctions.WideAttack);
human.AddFunction(ContentKeyFunctions.ArcadeUp);
human.AddFunction(ContentKeyFunctions.ArcadeDown);
human.AddFunction(ContentKeyFunctions.ArcadeLeft);
human.AddFunction(ContentKeyFunctions.ArcadeRight);
human.AddFunction(ContentKeyFunctions.Arcade1);
human.AddFunction(ContentKeyFunctions.Arcade2);
human.AddFunction(ContentKeyFunctions.Arcade3);
var ghost = contexts.New("ghost", "common"); var ghost = contexts.New("ghost", "common");
ghost.AddFunction(EngineKeyFunctions.MoveUp); ghost.AddFunction(EngineKeyFunctions.MoveUp);

View File

@@ -0,0 +1,859 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Utility;
using Content.Shared.Arcade;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Random;
namespace Content.Server.GameObjects.Components.Arcade
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class BlockGameArcadeComponent : Component, IActivate
{
[Dependency] private IRobustRandom _random = null!;
public override string Name => "BlockGameArcade";
public override uint? NetID => ContentNetIDs.BLOCKGAME_ARCADE;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BlockGameUiKey.Key);
private BlockGame _game = null!;
private IPlayerSession? _player;
private List<IPlayerSession> _spectators = new List<IPlayerSession>();
public void Activate(ActivateEventArgs eventArgs)
{
if(!eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
return;
}
if (!Powered)
{
return;
}
if(!ActionBlockerSystem.CanInteract(Owner)) return;
UserInterface?.Toggle(actor.playerSession);
RegisterPlayerSession(actor.playerSession);
}
private 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);
}
private 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);
}
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
_game = new BlockGame(this);
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Message is BlockGameMessages.BlockGameUserUnregisterMessage unregisterMessage)
{
UnRegisterPlayerSession(obj.Session);
return;
}
if (obj.Session != _player) return;
if (!ActionBlockerSystem.CanInteract(Owner))
{
DeactivePlayer(obj.Session);
}
if (!(obj.Message is BlockGameMessages.BlockGamePlayerActionMessage message)) return;
if (message.PlayerAction == BlockGamePlayerAction.NewGame)
{
if(_game.Started) _game = new BlockGame(this);
_game.StartGame();
}
else
{
_game.ProcessInput(message.PlayerAction);
}
}
public void DoGameTick(float frameTime)
{
_game.GameTick(frameTime);
}
private class BlockGame
{
//note: field is 10(0 -> 9) wide and 20(0 -> 19) high
private BlockGameArcadeComponent _component;
private List<BlockGameBlock> _field = new List<BlockGameBlock>();
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 _softDropOverride = 0.1f;
private float Speed => !_softDropPressed
? -0.03f * Level + 1
: _softDropOverride;
private 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 BlockGameSystem.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;
_internalNextPiece = BlockGamePiece.GetRandom(_component._random);
}
private void SendHighscoreUpdate()
{
var entitySystem = EntitySystem.Get<BlockGameSystem>();
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(entitySystem.GetLocalHighscores(), entitySystem.GetGlobalHighscores()));
}
private void SendHighscoreUpdate(IPlayerSession session)
{
var entitySystem = EntitySystem.Get<BlockGameSystem>();
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(entitySystem.GetLocalHighscores(), entitySystem.GetGlobalHighscores()), session);
}
public void StartGame()
{
InitializeNewBlock();
_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;
if (_accumulatedLeftPressTime >= _pressCheckSpeed)
{
if (_currentPiece.Positions(_currentPiecePosition.AddToX(-1), _currentRotation)
.All(MoveCheck))
{
_currentPiecePosition = _currentPiecePosition.AddToX(-1);
anythingChanged = true;
}
_accumulatedLeftPressTime -= _pressCheckSpeed;
}
}
if (_rightPressed)
{
_accumulatedRightPressTime += frameTime;
if (_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;
var checkTime = Speed;
if (_accumulatedFieldFrameTime < checkTime) return;
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 = BlockGamePiece.GetRandom(_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)
{
switch (action)
{
case BlockGamePlayerAction.StartLeft:
_leftPressed = true;
break;
case BlockGamePlayerAction.EndLeft:
_leftPressed = false;
break;
case BlockGamePlayerAction.StartRight:
_rightPressed = true;
break;
case BlockGamePlayerAction.EndRight:
_rightPressed = false;
break;
case BlockGamePlayerAction.Rotate:
TrySetRotation(Next(_currentRotation, false));
break;
case BlockGamePlayerAction.CounterRotate:
TrySetRotation(Next(_currentRotation, true));
break;
case BlockGamePlayerAction.SoftdropStart:
_softDropPressed = true;
break;
case BlockGamePlayerAction.SoftdropEnd:
_softDropPressed = false;
break;
case BlockGamePlayerAction.Harddrop:
PerformHarddrop();
break;
case BlockGamePlayerAction.Pause:
_running = false;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause));
break;
case BlockGamePlayerAction.Unpause:
if (!_gameOver)
{
_running = true;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game));
}
break;
case BlockGamePlayerAction.Hold:
HoldPiece();
break;
case BlockGamePlayerAction.ShowHighscores:
_running = false;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Highscores));
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 != null)
{
var blockGameSystem = EntitySystem.Get<BlockGameSystem>();
_highScorePlacement = blockGameSystem.RegisterHighScore(_component._player.AttachedEntity.Name, 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));
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 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 GetRandom(IRobustRandom random)
{
var pieces = (BlockGamePieceType[])Enum.GetValues(typeof(BlockGamePieceType));
var choice = random.Pick(pieces);
return GetPiece(choice);
}
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,84 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Arcade;
using Content.Shared.Arcade;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems
{
// ReSharper disable once ClassNeverInstantiated.Global
public class BlockGameSystem : EntitySystem
{
private readonly List<BlockGameMessages.HighScoreEntry> _roundHighscores = new List<BlockGameMessages.HighScoreEntry>();
private readonly List<BlockGameMessages.HighScoreEntry> _globalHighscores = new List<BlockGameMessages.HighScoreEntry>();
public HighScorePlacement RegisterHighScore(string name, int score)
{
var entry = new BlockGameMessages.HighScoreEntry(name, score);
return new HighScorePlacement(TryInsertIntoList(_roundHighscores, entry), TryInsertIntoList(_globalHighscores, entry));
}
public List<BlockGameMessages.HighScoreEntry> GetLocalHighscores() => GetSortedHighscores(_roundHighscores);
public List<BlockGameMessages.HighScoreEntry> GetGlobalHighscores() => GetSortedHighscores(_globalHighscores);
private List<BlockGameMessages.HighScoreEntry> GetSortedHighscores(List<BlockGameMessages.HighScoreEntry> highScoreEntries)
{
var result = highScoreEntries.ShallowClone();
result.Sort((p1, p2) => p2.Score.CompareTo(p1.Score));
return result;
}
private int? TryInsertIntoList(List<BlockGameMessages.HighScoreEntry> highScoreEntries, BlockGameMessages.HighScoreEntry entry)
{
if (highScoreEntries.Count < 5)
{
highScoreEntries.Add(entry);
return GetPlacement(highScoreEntries, entry);
}
if (highScoreEntries.Min(e => e.Score) >= entry.Score) return null;
var lowestHighscore = highScoreEntries.Min();
highScoreEntries.Remove(lowestHighscore);
highScoreEntries.Add(entry);
return GetPlacement(highScoreEntries, entry);
}
private int? GetPlacement(List<BlockGameMessages.HighScoreEntry> highScoreEntries, BlockGameMessages.HighScoreEntry entry)
{
int? placement = null;
if (highScoreEntries.Contains(entry))
{
highScoreEntries.Sort((p1,p2) => p2.Score.CompareTo(p1.Score));
placement = 1 + highScoreEntries.IndexOf(entry);
}
return placement;
}
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<BlockGameArcadeComponent>())
{
comp.DoGameTick(frameTime);
}
}
public readonly struct HighScorePlacement
{
public readonly int? GlobalPlacement;
public readonly int? LocalPlacement;
public HighScorePlacement(int? globalPlacement, int? localPlacement)
{
GlobalPlacement = globalPlacement;
LocalPlacement = localPlacement;
}
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
namespace Content.Shared.Arcade
{
[Serializable, NetSerializable]
public struct BlockGameBlock
{
public Vector2i Position;
public readonly BlockGameBlockColor GameBlockColor;
public BlockGameBlock(Vector2i position, BlockGameBlockColor gameBlockColor)
{
Position = position;
GameBlockColor = gameBlockColor;
}
[Serializable, NetSerializable]
public enum BlockGameBlockColor
{
Red,
Orange,
Yellow,
Green,
Blue,
LightBlue,
Purple
}
}
public static class BlockGameVector2Extensions{
public static BlockGameBlock ToBlockGameBlock(this Vector2i vector2, BlockGameBlock.BlockGameBlockColor gameBlockColor)
{
return new BlockGameBlock(vector2, gameBlockColor);
}
public static Vector2i AddToX(this Vector2i vector2, int amount)
{
return new Vector2i(vector2.X + amount, vector2.Y);
}
public static Vector2i AddToY(this Vector2i vector2, int amount)
{
return new Vector2i(vector2.X, vector2.Y + amount);
}
public static Vector2i Rotate90DegreesAsOffset(this Vector2i vector)
{
return new Vector2i(-vector.Y, vector.X);
}
}
}

View File

@@ -0,0 +1,141 @@
#nullable enable
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects.Components.UserInterface;
using Robust.Shared.Serialization;
namespace Content.Shared.Arcade
{
public static class BlockGameMessages
{
[Serializable, NetSerializable]
public class BlockGamePlayerActionMessage : BoundUserInterfaceMessage
{
public readonly BlockGamePlayerAction PlayerAction;
public BlockGamePlayerActionMessage(BlockGamePlayerAction playerAction)
{
PlayerAction = playerAction;
}
}
[Serializable, NetSerializable]
public class BlockGameVisualUpdateMessage : BoundUserInterfaceMessage
{
public readonly BlockGameVisualType GameVisualType;
public readonly BlockGameBlock[] Blocks;
public BlockGameVisualUpdateMessage(BlockGameBlock[] blocks, BlockGameVisualType gameVisualType)
{
Blocks = blocks;
GameVisualType = gameVisualType;
}
}
public enum BlockGameVisualType
{
GameField,
HoldBlock,
NextBlock
}
[Serializable, NetSerializable]
public class BlockGameScoreUpdateMessage : BoundUserInterfaceMessage
{
public readonly int Points;
public BlockGameScoreUpdateMessage(int points)
{
Points = points;
}
}
[Serializable, NetSerializable]
public class BlockGameUserStatusMessage : BoundUserInterfaceMessage
{
public readonly bool IsPlayer;
public BlockGameUserStatusMessage(bool isPlayer)
{
IsPlayer = isPlayer;
}
}
[Serializable, NetSerializable]
public class BlockGameUserUnregisterMessage : BoundUserInterfaceMessage{}
[Serializable, NetSerializable]
public class BlockGameSetScreenMessage : BoundUserInterfaceMessage
{
public readonly BlockGameScreen Screen;
public readonly bool isStarted;
public BlockGameSetScreenMessage(BlockGameScreen screen, bool isStarted = true)
{
Screen = screen;
this.isStarted = isStarted;
}
}
[Serializable, NetSerializable]
public class BlockGameGameOverScreenMessage : BlockGameSetScreenMessage
{
public readonly int FinalScore;
public readonly int? LocalPlacement;
public readonly int? GlobalPlacement;
public BlockGameGameOverScreenMessage(int finalScore, int? localPlacement, int? globalPlacement) : base(BlockGameScreen.Gameover)
{
FinalScore = finalScore;
LocalPlacement = localPlacement;
GlobalPlacement = globalPlacement;
}
}
[Serializable, NetSerializable]
public enum BlockGameScreen
{
Game,
Pause,
Gameover,
Highscores
}
[Serializable, NetSerializable]
public class BlockGameHighScoreUpdateMessage : BoundUserInterfaceMessage
{
public List<HighScoreEntry> LocalHighscores;
public List<HighScoreEntry> GlobalHighscores;
public BlockGameHighScoreUpdateMessage(List<HighScoreEntry> localHighscores, List<HighScoreEntry> globalHighscores)
{
LocalHighscores = localHighscores;
GlobalHighscores = globalHighscores;
}
}
[Serializable, NetSerializable]
public class HighScoreEntry : IComparable
{
public string Name;
public int Score;
public HighScoreEntry(string name, int score)
{
Name = name;
Score = score;
}
public int CompareTo(object? obj)
{
if (!(obj is HighScoreEntry entry)) return 0;
return Score.CompareTo(entry.Score);
}
}
[Serializable, NetSerializable]
public class BlockGameLevelUpdateMessage : BoundUserInterfaceMessage
{
public readonly int Level;
public BlockGameLevelUpdateMessage(int level)
{
Level = level;
}
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.Arcade
{
[Serializable, NetSerializable]
public enum BlockGamePlayerAction
{
NewGame,
StartLeft,
EndLeft,
StartRight,
EndRight,
Rotate,
CounterRotate,
SoftdropStart,
SoftdropEnd,
Harddrop,
Pause,
Unpause,
Hold,
ShowHighscores
}
}

View File

@@ -0,0 +1,11 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.Arcade
{
[Serializable, NetSerializable]
public enum BlockGameUiKey
{
Key
}
}

View File

@@ -76,6 +76,7 @@
public const uint MOB_STATE_MANAGER = 1070; public const uint MOB_STATE_MANAGER = 1070;
public const uint SLIP = 1071; public const uint SLIP = 1071;
public const uint SPACE_VILLAIN_ARCADE = 1072; public const uint SPACE_VILLAIN_ARCADE = 1072;
public const uint BLOCKGAME_ARCADE = 1073;
// Net IDs for integration tests. // Net IDs for integration tests.
public const uint PREDICTION_TEST = 10001; public const uint PREDICTION_TEST = 10001;

View File

@@ -34,5 +34,12 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction TakeScreenshot = "TakeScreenshot"; public static readonly BoundKeyFunction TakeScreenshot = "TakeScreenshot";
public static readonly BoundKeyFunction TakeScreenshotNoUI = "TakeScreenshotNoUI"; public static readonly BoundKeyFunction TakeScreenshotNoUI = "TakeScreenshotNoUI";
public static readonly BoundKeyFunction Point = "Point"; public static readonly BoundKeyFunction Point = "Point";
public static readonly BoundKeyFunction ArcadeUp = "ArcadeUp";
public static readonly BoundKeyFunction ArcadeDown = "ArcadeDown";
public static readonly BoundKeyFunction ArcadeLeft = "ArcadeLeft";
public static readonly BoundKeyFunction ArcadeRight = "ArcadeRight";
public static readonly BoundKeyFunction Arcade1 = "Arcade1";
public static readonly BoundKeyFunction Arcade2 = "Arcade2";
public static readonly BoundKeyFunction Arcade3 = "Arcade3";
} }
} }

View File

@@ -30,3 +30,35 @@
type: SpaceVillainArcadeBoundUserInterface type: SpaceVillainArcadeBoundUserInterface
- key: enum.WiresUiKey.Key - key: enum.WiresUiKey.Key
type: WiresBoundUserInterface type: WiresBoundUserInterface
- type: entity
id: BlockGameArcade
name: blockGameArcade
parent: ComputerBase
components:
- type: Icon
state: arcade
- type: PowerReceiver
- type: Sprite
layers:
- state: arcade
map: ["enum.ComputerVisualizer+Layers.Body"]
- state: invaders
shader: unshaded
map: ["enum.ComputerVisualizer+Layers.Screen"]
- type: Appearance
visuals:
- type: ComputerVisualizer
screen: invaders
key: ""
body: arcade
bodyBroken: arcade
- type: Anchorable
- type: Pullable
- type: BlockGameArcade
- type: UserInterface
interfaces:
- key: enum.BlockGameUiKey.Key
type: BlockGameBoundUserInterface
- key: enum.WiresUiKey.Key
type: WiresBoundUserInterface

View File

@@ -280,3 +280,24 @@ binds:
type: State type: State
key: MouseMiddle key: MouseMiddle
mod1: Shift mod1: Shift
- function: ArcadeUp
type: State
key: Up
- function: ArcadeDown
type: State
key: Down
- function: ArcadeLeft
type: State
key: Left
- function: ArcadeRight
type: State
key: Right
- function: Arcade1
type: State
key: Space
- function: Arcade2
type: State
key: C
- function: Arcade3
type: State
key: Z