diff --git a/Content.Client/Arcade/BlockGameMenu.cs b/Content.Client/Arcade/BlockGameMenu.cs new file mode 100644 index 0000000000..0434e3d2c6 --- /dev/null +++ b/Content.Client/Arcade/BlockGameMenu.cs @@ -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(); + 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 localHighscores, + List 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; + } + } +} diff --git a/Content.Client/GameObjects/Components/Arcade/BlockGameBoundUserInterface.cs b/Content.Client/GameObjects/Components/Arcade/BlockGameBoundUserInterface.cs new file mode 100644 index 0000000000..306abd0aef --- /dev/null +++ b/Content.Client/GameObjects/Components/Arcade/BlockGameBoundUserInterface.cs @@ -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(); + } + } +} diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index 6286b4cef3..0272e4da91 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -174,6 +174,7 @@ "Flammable", "CreamPie", "CreamPied", + "BlockGameArcade" }; } } diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index a01d9dc307..fcd0be1519 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -39,6 +39,13 @@ namespace Content.Client.Input human.AddFunction(ContentKeyFunctions.MouseMiddle); human.AddFunction(ContentKeyFunctions.ToggleCombatMode); 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"); ghost.AddFunction(EngineKeyFunctions.MoveUp); diff --git a/Content.Server/GameObjects/Components/Arcade/BlockGameArcadeComponent.cs b/Content.Server/GameObjects/Components/Arcade/BlockGameArcadeComponent.cs new file mode 100644 index 0000000000..57ad17572c --- /dev/null +++ b/Content.Server/GameObjects/Components/Arcade/BlockGameArcadeComponent.cs @@ -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 _spectators = new List(); + + 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 _field = new List(); + + 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(); + _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(entitySystem.GetLocalHighscores(), entitySystem.GetGlobalHighscores())); + } + + private void SendHighscoreUpdate(IPlayerSession session) + { + var entitySystem = EntitySystem.Get(); + _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(); + + _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 ComputeField() + { + var result = new List(); + 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)}} + }; + } + } + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/BlockGameSystem.cs b/Content.Server/GameObjects/EntitySystems/BlockGameSystem.cs new file mode 100644 index 0000000000..34d2a79906 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/BlockGameSystem.cs @@ -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 _roundHighscores = new List(); + private readonly List _globalHighscores = new List(); + + 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 GetLocalHighscores() => GetSortedHighscores(_roundHighscores); + + public List GetGlobalHighscores() => GetSortedHighscores(_globalHighscores); + + private List GetSortedHighscores(List highScoreEntries) + { + var result = highScoreEntries.ShallowClone(); + result.Sort((p1, p2) => p2.Score.CompareTo(p1.Score)); + return result; + } + + private int? TryInsertIntoList(List 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 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()) + { + 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; + } + } + } +} diff --git a/Content.Shared/Arcade/BlockGameBlock.cs b/Content.Shared/Arcade/BlockGameBlock.cs new file mode 100644 index 0000000000..e1fc1cd385 --- /dev/null +++ b/Content.Shared/Arcade/BlockGameBlock.cs @@ -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); + } + + } +} diff --git a/Content.Shared/Arcade/BlockGameMessages.cs b/Content.Shared/Arcade/BlockGameMessages.cs new file mode 100644 index 0000000000..8f6d6aba81 --- /dev/null +++ b/Content.Shared/Arcade/BlockGameMessages.cs @@ -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 LocalHighscores; + public List GlobalHighscores; + + public BlockGameHighScoreUpdateMessage(List localHighscores, List 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; + } + } + } +} diff --git a/Content.Shared/Arcade/BlockGamePlayerAction.cs b/Content.Shared/Arcade/BlockGamePlayerAction.cs new file mode 100644 index 0000000000..f88b213a56 --- /dev/null +++ b/Content.Shared/Arcade/BlockGamePlayerAction.cs @@ -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 + } +} diff --git a/Content.Shared/Arcade/BlockGameUiKey.cs b/Content.Shared/Arcade/BlockGameUiKey.cs new file mode 100644 index 0000000000..6ffb216668 --- /dev/null +++ b/Content.Shared/Arcade/BlockGameUiKey.cs @@ -0,0 +1,11 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.Arcade +{ + [Serializable, NetSerializable] + public enum BlockGameUiKey + { + Key + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index 9d47e3ba1a..753e768e09 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -76,6 +76,7 @@ public const uint MOB_STATE_MANAGER = 1070; public const uint SLIP = 1071; public const uint SPACE_VILLAIN_ARCADE = 1072; + public const uint BLOCKGAME_ARCADE = 1073; // Net IDs for integration tests. public const uint PREDICTION_TEST = 10001; diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index cb4e5b233a..d21f722d99 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -34,5 +34,12 @@ namespace Content.Shared.Input public static readonly BoundKeyFunction TakeScreenshot = "TakeScreenshot"; public static readonly BoundKeyFunction TakeScreenshotNoUI = "TakeScreenshotNoUI"; 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"; } } diff --git a/Resources/Prototypes/Entities/Constructible/Power/arcade.yml b/Resources/Prototypes/Entities/Constructible/Power/arcade.yml index 533438391b..86354495ec 100644 --- a/Resources/Prototypes/Entities/Constructible/Power/arcade.yml +++ b/Resources/Prototypes/Entities/Constructible/Power/arcade.yml @@ -30,3 +30,35 @@ type: SpaceVillainArcadeBoundUserInterface - key: enum.WiresUiKey.Key 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 diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index a7cd07bfaa..8fff686f91 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -280,3 +280,24 @@ binds: type: State key: MouseMiddle 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