From bc3f42d82249cc47d49bec97a23cade279ab5636 Mon Sep 17 00:00:00 2001 From: TemporalOroboros Date: Thu, 15 Jun 2023 04:25:25 -0700 Subject: [PATCH] ECS Arcade Machines (#16791) --- Content.Client/Arcade/BlockGameMenu.cs | 126 ++- .../Arcade/SpaceVillainArcadeMenu.cs | 43 +- .../Arcade/UI/BlockGameBoundUserInterface.cs | 134 ++- .../SpaceVillainArcadeBoundUserInterface.cs | 68 +- .../Arcade/ArcadeSystem.BlockGame.cs | 17 - .../Arcade/ArcadeSystem.SpaceVillain.cs | 17 - Content.Server/Arcade/ArcadeSystem.cs | 28 - .../Arcade/BlockGame/BlockGame.GameState.cs | 256 ++++++ .../Arcade/BlockGame/BlockGame.Pieces.cs | 238 +++++ .../Arcade/BlockGame/BlockGame.Ui.cs | 364 ++++++++ Content.Server/Arcade/BlockGame/BlockGame.cs | 303 ++++++ .../BlockGame/BlockGameArcadeComponent.cs | 22 + .../Arcade/BlockGame/BlockGameArcadeSystem.cs | 123 +++ .../Components/BlockGameArcadeComponent.cs | 867 ------------------ .../Components/SpaceVillainArcadeComponent.cs | 389 -------- .../SpaceVillainArcadeComponent.cs | 124 +++ .../SpaceVillainArcadeSystem.cs | 109 +++ .../SpaceVillainGame.Fighter.cs | 68 ++ .../SpaceVillainGame/SpaceVillainGame.Ui.cs | 53 ++ .../SpaceVillainGame/SpaceVillainGame.cs | 252 +++++ .../ArcadeInvincibilityWireActions.cs | 28 +- .../WireActions/ArcadeOverflowWireAction.cs | 4 +- Content.Shared/Arcade/BlockGameBlock.cs | 4 +- 23 files changed, 2116 insertions(+), 1521 deletions(-) delete mode 100644 Content.Server/Arcade/ArcadeSystem.BlockGame.cs delete mode 100644 Content.Server/Arcade/ArcadeSystem.SpaceVillain.cs create mode 100644 Content.Server/Arcade/BlockGame/BlockGame.GameState.cs create mode 100644 Content.Server/Arcade/BlockGame/BlockGame.Pieces.cs create mode 100644 Content.Server/Arcade/BlockGame/BlockGame.Ui.cs create mode 100644 Content.Server/Arcade/BlockGame/BlockGame.cs create mode 100644 Content.Server/Arcade/BlockGame/BlockGameArcadeComponent.cs create mode 100644 Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs delete mode 100644 Content.Server/Arcade/Components/BlockGameArcadeComponent.cs delete mode 100644 Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs create mode 100644 Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeComponent.cs create mode 100644 Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs create mode 100644 Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.Fighter.cs create mode 100644 Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.Ui.cs create mode 100644 Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs diff --git a/Content.Client/Arcade/BlockGameMenu.cs b/Content.Client/Arcade/BlockGameMenu.cs index 5452d7c5d0..17f150c756 100644 --- a/Content.Client/Arcade/BlockGameMenu.cs +++ b/Content.Client/Arcade/BlockGameMenu.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -20,16 +21,16 @@ namespace Content.Client.Arcade { public sealed class BlockGameMenu : DefaultWindow { - private static readonly Color OverlayBackgroundColor = new(74,74,81,180); - private static readonly Color OverlayShadowColor = new(0,0,0,83); + private static readonly Color OverlayBackgroundColor = new(74, 74, 81, 180); + private static readonly Color OverlayShadowColor = new(0, 0, 0, 83); - private static readonly Vector2 BlockSize = new(15,15); + private static readonly Vector2 BlockSize = new(15, 15); private readonly BlockGameBoundUserInterface _owner; private readonly PanelContainer _mainPanel; - private BoxContainer _gameRootContainer; + private readonly BoxContainer _gameRootContainer; private GridContainer _gameGrid = default!; private GridContainer _nextBlockGrid = default!; private GridContainer _holdBlockGrid = default!; @@ -82,7 +83,7 @@ namespace Content.Client.Arcade _gameRootContainer.AddChild(_levelLabel); _gameRootContainer.AddChild(new Control { - MinSize = new Vector2(1,5) + MinSize = new Vector2(1, 5) }); _pointsLabel = new Label @@ -93,7 +94,7 @@ namespace Content.Client.Arcade _gameRootContainer.AddChild(_pointsLabel); _gameRootContainer.AddChild(new Control { - MinSize = new Vector2(1,10) + MinSize = new Vector2(1, 10) }); var gameBox = new BoxContainer @@ -103,12 +104,12 @@ namespace Content.Client.Arcade gameBox.AddChild(SetupHoldBox(backgroundTexture)); gameBox.AddChild(new Control { - MinSize = new Vector2(10,1) + MinSize = new Vector2(10, 1) }); gameBox.AddChild(SetupGameGrid(backgroundTexture)); gameBox.AddChild(new Control { - MinSize = new Vector2(10,1) + MinSize = new Vector2(10, 1) }); gameBox.AddChild(SetupNextBox(backgroundTexture)); @@ -116,7 +117,7 @@ namespace Content.Client.Arcade _gameRootContainer.AddChild(new Control { - MinSize = new Vector2(1,10) + MinSize = new Vector2(1, 10) }); _pauseButton = new Button @@ -176,7 +177,7 @@ namespace Content.Client.Arcade _owner.SendAction(BlockGamePlayerAction.NewGame); }; pauseMenuContainer.AddChild(_newGameButton); - pauseMenuContainer.AddChild(new Control{MinSize = new Vector2(1,10)}); + pauseMenuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) }); _scoreBoardButton = new Button { @@ -185,7 +186,7 @@ namespace Content.Client.Arcade }; _scoreBoardButton.OnPressed += (e) => _owner.SendAction(BlockGamePlayerAction.ShowHighscores); pauseMenuContainer.AddChild(_scoreBoardButton); - _unpauseButtonMargin = new Control {MinSize = new Vector2(1, 10), Visible = false}; + _unpauseButtonMargin = new Control { MinSize = new Vector2(1, 10), Visible = false }; pauseMenuContainer.AddChild(_unpauseButtonMargin); _unpauseButton = new Button @@ -239,13 +240,13 @@ namespace Content.Client.Arcade VerticalAlignment = VAlignment.Center }; - gameOverMenuContainer.AddChild(new Label{Text = Loc.GetString("blockgame-menu-msg-game-over"),Align = Label.AlignMode.Center}); - gameOverMenuContainer.AddChild(new Control{MinSize = new Vector2(1,10)}); + gameOverMenuContainer.AddChild(new Label { Text = Loc.GetString("blockgame-menu-msg-game-over"), Align = Label.AlignMode.Center }); + gameOverMenuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) }); - _finalScoreLabel = new Label{Align = Label.AlignMode.Center}; + _finalScoreLabel = new Label { Align = Label.AlignMode.Center }; gameOverMenuContainer.AddChild(_finalScoreLabel); - gameOverMenuContainer.AddChild(new Control{MinSize = new Vector2(1,10)}); + gameOverMenuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) }); _finalNewGameButton = new Button { @@ -275,7 +276,7 @@ namespace Content.Client.Arcade HorizontalAlignment = HAlignment.Center }; - var c = new Color(OverlayBackgroundColor.R,OverlayBackgroundColor.G,OverlayBackgroundColor.B,220); + var c = new Color(OverlayBackgroundColor.R, OverlayBackgroundColor.G, OverlayBackgroundColor.B, 220); var innerBack = new StyleBoxTexture { Texture = backgroundTexture, @@ -298,8 +299,8 @@ namespace Content.Client.Arcade VerticalAlignment = VAlignment.Center }; - menuContainer.AddChild(new Label{Text = Loc.GetString("blockgame-menu-label-highscores")}); - menuContainer.AddChild(new Control{MinSize = new Vector2(1,10)}); + menuContainer.AddChild(new Label { Text = Loc.GetString("blockgame-menu-label-highscores") }); + menuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) }); var highScoreBox = new BoxContainer { @@ -311,14 +312,14 @@ namespace Content.Client.Arcade Align = Label.AlignMode.Center }; highScoreBox.AddChild(_localHighscoresLabel); - highScoreBox.AddChild(new Control{MinSize = new Vector2(40,1)}); + highScoreBox.AddChild(new Control { MinSize = new Vector2(40, 1) }); _globalHighscoresLabel = new Label { Align = Label.AlignMode.Center }; highScoreBox.AddChild(_globalHighscoresLabel); menuContainer.AddChild(highScoreBox); - menuContainer.AddChild(new Control{MinSize = new Vector2(1,10)}); + menuContainer.AddChild(new Control { MinSize = new Vector2(1, 10) }); _highscoreBackButton = new Button { Text = Loc.GetString("blockgame-menu-button-back"), @@ -359,7 +360,7 @@ namespace Content.Client.Arcade HSeparationOverride = 1, VSeparationOverride = 1 }; - UpdateBlocks(new BlockGameBlock[0]); + UpdateBlocks(Array.Empty()); var back = new StyleBoxTexture { @@ -376,7 +377,7 @@ namespace Content.Client.Arcade }; var backgroundPanel = new PanelContainer { - PanelOverride = new StyleBoxFlat{BackgroundColor = Color.FromHex("#86868d")} + PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#86868d") } }; backgroundPanel.AddChild(_gameGrid); gamePanel.AddChild(backgroundPanel); @@ -416,7 +417,7 @@ namespace Content.Client.Arcade nextBlockPanel.AddChild(nextCenterContainer); grid.AddChild(nextBlockPanel); - grid.AddChild(new Label{Text = Loc.GetString("blockgame-menu-label-next"), Align = Label.AlignMode.Center}); + grid.AddChild(new Label { Text = Loc.GetString("blockgame-menu-label-next"), Align = Label.AlignMode.Center }); return grid; } @@ -454,15 +455,17 @@ namespace Content.Client.Arcade holdBlockPanel.AddChild(holdCenterContainer); grid.AddChild(holdBlockPanel); - grid.AddChild(new Label{Text = Loc.GetString("blockgame-menu-label-hold"), Align = Label.AlignMode.Center}); + grid.AddChild(new Label { Text = Loc.GetString("blockgame-menu-label-hold"), Align = Label.AlignMode.Center }); return grid; } protected override void KeyboardFocusExited() { - if (!IsOpen) return; - if(_gameOver) return; + if (!IsOpen) + return; + if (_gameOver) + return; TryPause(); } @@ -480,7 +483,8 @@ namespace Content.Client.Arcade public void SetScreen(BlockGameMessages.BlockGameScreen screen) { - if (_gameOver) return; + if (_gameOver) + return; switch (screen) { @@ -512,9 +516,12 @@ namespace Content.Client.Arcade private void CloseMenus() { - if(_mainPanel.Children.Contains(_menuRootContainer)) _mainPanel.RemoveChild(_menuRootContainer); - if(_mainPanel.Children.Contains(_gameOverRootContainer)) _mainPanel.RemoveChild(_gameOverRootContainer); - if(_mainPanel.Children.Contains(_highscoresRootContainer)) _mainPanel.RemoveChild(_highscoresRootContainer); + if (_mainPanel.Children.Contains(_menuRootContainer)) + _mainPanel.RemoveChild(_menuRootContainer); + if (_mainPanel.Children.Contains(_gameOverRootContainer)) + _mainPanel.RemoveChild(_gameOverRootContainer); + if (_mainPanel.Children.Contains(_highscoresRootContainer)) + _mainPanel.RemoveChild(_highscoresRootContainer); } public void SetGameoverInfo(int amount, int? localPlacement, int? globalPlacement) @@ -563,72 +570,56 @@ namespace Content.Client.Arcade { base.KeyBindDown(args); - if(!_isPlayer || args.Handled) return; + if (!_isPlayer || args.Handled) + return; - if (args.Function == ContentKeyFunctions.ArcadeLeft) - { + else if (args.Function == ContentKeyFunctions.ArcadeLeft) _owner.SendAction(BlockGamePlayerAction.StartLeft); - } else if (args.Function == ContentKeyFunctions.ArcadeRight) - { _owner.SendAction(BlockGamePlayerAction.StartRight); - } else if (args.Function == ContentKeyFunctions.ArcadeUp) - { _owner.SendAction(BlockGamePlayerAction.Rotate); - } else if (args.Function == ContentKeyFunctions.Arcade3) - { _owner.SendAction(BlockGamePlayerAction.CounterRotate); - } else if (args.Function == ContentKeyFunctions.ArcadeDown) - { _owner.SendAction(BlockGamePlayerAction.SoftdropStart); - } else if (args.Function == ContentKeyFunctions.Arcade2) - { _owner.SendAction(BlockGamePlayerAction.Hold); - } else if (args.Function == ContentKeyFunctions.Arcade1) - { _owner.SendAction(BlockGamePlayerAction.Harddrop); - } } protected override void KeyBindUp(GUIBoundKeyEventArgs args) { base.KeyBindUp(args); - if(!_isPlayer || args.Handled) return; + if (!_isPlayer || args.Handled) + return; - if (args.Function == ContentKeyFunctions.ArcadeLeft) - { + else if (args.Function == ContentKeyFunctions.ArcadeLeft) _owner.SendAction(BlockGamePlayerAction.EndLeft); - } else if (args.Function == ContentKeyFunctions.ArcadeRight) - { _owner.SendAction(BlockGamePlayerAction.EndRight); - }else if (args.Function == ContentKeyFunctions.ArcadeDown) - { + else if (args.Function == ContentKeyFunctions.ArcadeDown) _owner.SendAction(BlockGamePlayerAction.SoftdropEnd); - } } public void UpdateNextBlock(BlockGameBlock[] blocks) { _nextBlockGrid.RemoveAllChildren(); - if (blocks.Length == 0) return; + if (blocks.Length == 0) + return; var columnCount = blocks.Max(b => b.Position.X) + 1; var rowCount = blocks.Max(b => b.Position.Y) + 1; _nextBlockGrid.Columns = columnCount; - for (int y = 0; y < rowCount; y++) + for (var y = 0; y < rowCount; y++) { - for (int x = 0; x < columnCount; x++) + for (var x = 0; x < columnCount; x++) { var c = GetColorForPosition(blocks, x, y); _nextBlockGrid.AddChild(new PanelContainer { - PanelOverride = new StyleBoxFlat {BackgroundColor = c}, + PanelOverride = new StyleBoxFlat { BackgroundColor = c }, MinSize = BlockSize, RectDrawClipMargin = 0 }); @@ -639,18 +630,19 @@ namespace Content.Client.Arcade public void UpdateHeldBlock(BlockGameBlock[] blocks) { _holdBlockGrid.RemoveAllChildren(); - if (blocks.Length == 0) return; + if (blocks.Length == 0) + return; var columnCount = blocks.Max(b => b.Position.X) + 1; var rowCount = blocks.Max(b => b.Position.Y) + 1; _holdBlockGrid.Columns = columnCount; - for (int y = 0; y < rowCount; y++) + for (var y = 0; y < rowCount; y++) { - for (int x = 0; x < columnCount; x++) + for (var x = 0; x < columnCount; x++) { var c = GetColorForPosition(blocks, x, y); _holdBlockGrid.AddChild(new PanelContainer { - PanelOverride = new StyleBoxFlat {BackgroundColor = c}, + PanelOverride = new StyleBoxFlat { BackgroundColor = c }, MinSize = BlockSize, RectDrawClipMargin = 0 }); @@ -661,14 +653,14 @@ namespace Content.Client.Arcade public void UpdateBlocks(BlockGameBlock[] blocks) { _gameGrid.RemoveAllChildren(); - for (int y = 0; y < 20; y++) + for (var y = 0; y < 20; y++) { - for (int x = 0; x < 10; x++) + for (var x = 0; x < 10; x++) { var c = GetColorForPosition(blocks, x, y); _gameGrid.AddChild(new PanelContainer { - PanelOverride = new StyleBoxFlat {BackgroundColor = c}, + PanelOverride = new StyleBoxFlat { BackgroundColor = c }, MinSize = BlockSize, RectDrawClipMargin = 0 }); @@ -676,9 +668,9 @@ namespace Content.Client.Arcade } } - private Color GetColorForPosition(BlockGameBlock[] blocks, int x, int y) + private static Color GetColorForPosition(BlockGameBlock[] blocks, int x, int y) { - Color c = Color.Transparent; + var c = Color.Transparent; var matchingBlock = blocks.FirstOrNull(b => b.Position.X == x && b.Position.Y == y); if (matchingBlock.HasValue) { diff --git a/Content.Client/Arcade/SpaceVillainArcadeMenu.cs b/Content.Client/Arcade/SpaceVillainArcadeMenu.cs index bbd7635459..4f07593b0e 100644 --- a/Content.Client/Arcade/SpaceVillainArcadeMenu.cs +++ b/Content.Client/Arcade/SpaceVillainArcadeMenu.cs @@ -24,40 +24,46 @@ namespace Content.Client.Arcade Title = Loc.GetString("spacevillain-menu-title"); Owner = owner; - var grid = new GridContainer {Columns = 1}; + var grid = new GridContainer { Columns = 1 }; - var infoGrid = new GridContainer {Columns = 3}; - infoGrid.AddChild(new Label{ Text = Loc.GetString("spacevillain-menu-label-player"), Align = Label.AlignMode.Center }); - infoGrid.AddChild(new Label{ Text = "|", Align = Label.AlignMode.Center }); - _enemyNameLabel = new Label{ Align = Label.AlignMode.Center}; + var infoGrid = new GridContainer { Columns = 3 }; + infoGrid.AddChild(new Label { Text = Loc.GetString("spacevillain-menu-label-player"), Align = Label.AlignMode.Center }); + infoGrid.AddChild(new Label { Text = "|", Align = Label.AlignMode.Center }); + _enemyNameLabel = new Label { Align = Label.AlignMode.Center }; infoGrid.AddChild(_enemyNameLabel); - _playerInfoLabel = new Label {Align = Label.AlignMode.Center}; + _playerInfoLabel = new Label { Align = Label.AlignMode.Center }; infoGrid.AddChild(_playerInfoLabel); - infoGrid.AddChild(new Label{ Text = "|", Align = Label.AlignMode.Center }); - _enemyInfoLabel = new Label {Align = Label.AlignMode.Center}; + infoGrid.AddChild(new Label { Text = "|", Align = Label.AlignMode.Center }); + _enemyInfoLabel = new Label { Align = Label.AlignMode.Center }; infoGrid.AddChild(_enemyInfoLabel); var centerContainer = new CenterContainer(); centerContainer.AddChild(infoGrid); grid.AddChild(centerContainer); - _playerActionLabel = new Label {Align = Label.AlignMode.Center}; + _playerActionLabel = new Label { Align = Label.AlignMode.Center }; grid.AddChild(_playerActionLabel); - _enemyActionLabel = new Label {Align = Label.AlignMode.Center}; + _enemyActionLabel = new Label { Align = Label.AlignMode.Center }; grid.AddChild(_enemyActionLabel); - var buttonGrid = new GridContainer {Columns = 3}; + var buttonGrid = new GridContainer { Columns = 3 }; _gameButtons[0] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Attack) - {Text = Loc.GetString("spacevillain-menu-button-attack")}; + { + Text = Loc.GetString("spacevillain-menu-button-attack") + }; buttonGrid.AddChild(_gameButtons[0]); _gameButtons[1] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Heal) - {Text = Loc.GetString("spacevillain-menu-button-heal")}; + { + Text = Loc.GetString("spacevillain-menu-button-heal") + }; buttonGrid.AddChild(_gameButtons[1]); _gameButtons[2] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Recharge) - {Text = Loc.GetString("spacevillain-menu-button-recharge")}; + { + Text = Loc.GetString("spacevillain-menu-button-recharge") + }; buttonGrid.AddChild(_gameButtons[2]); centerContainer = new CenterContainer(); @@ -65,7 +71,9 @@ namespace Content.Client.Arcade grid.AddChild(centerContainer); var newGame = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.NewGame) - {Text = Loc.GetString("spacevillain-menu-button-new-game")}; + { + Text = Loc.GetString("spacevillain-menu-button-new-game") + }; grid.AddChild(newGame); Contents.AddChild(grid); @@ -84,7 +92,8 @@ namespace Content.Client.Arcade public void UpdateInfo(SharedSpaceVillainArcadeComponent.SpaceVillainArcadeDataUpdateMessage message) { - if(message is SharedSpaceVillainArcadeComponent.SpaceVillainArcadeMetaDataUpdateMessage metaMessage) UpdateMetadata(metaMessage); + if (message is SharedSpaceVillainArcadeComponent.SpaceVillainArcadeMetaDataUpdateMessage metaMessage) + UpdateMetadata(metaMessage); _playerInfoLabel.Text = $"HP: {message.PlayerHP} MP: {message.PlayerMP}"; _enemyInfoLabel.Text = $"HP: {message.EnemyHP} MP: {message.EnemyMP}"; @@ -97,7 +106,7 @@ namespace Content.Client.Arcade private readonly SpaceVillainArcadeBoundUserInterface _owner; private readonly SharedSpaceVillainArcadeComponent.PlayerAction _playerAction; - public ActionButton(SpaceVillainArcadeBoundUserInterface owner,SharedSpaceVillainArcadeComponent.PlayerAction playerAction) + public ActionButton(SpaceVillainArcadeBoundUserInterface owner, SharedSpaceVillainArcadeComponent.PlayerAction playerAction) { _owner = owner; _playerAction = playerAction; diff --git a/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs b/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs index cd74077c12..2abe9311fb 100644 --- a/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs +++ b/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs @@ -1,78 +1,76 @@ using Content.Shared.Arcade; using Robust.Client.GameObjects; -using Robust.Shared.GameObjects; -namespace Content.Client.Arcade.UI +namespace Content.Client.Arcade.UI; + +public sealed class BlockGameBoundUserInterface : BoundUserInterface { - public sealed class BlockGameBoundUserInterface : BoundUserInterface + private BlockGameMenu? _menu; + + public BlockGameBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { - private BlockGameMenu? _menu; + } - public BlockGameBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) + protected override void Open() + { + base.Open(); + + _menu = new BlockGameMenu(this); + _menu.OnClose += Close; + _menu.OpenCentered(); + } + + protected override void ReceiveMessage(BoundUserInterfaceMessage message) + { + switch (message) { - } - - protected override void Open() - { - base.Open(); - - _menu = new BlockGameMenu(this); - _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(); + 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/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs b/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs index bb1582d127..2ecc8b46a8 100644 --- a/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs +++ b/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs @@ -3,51 +3,45 @@ using Robust.Shared.GameObjects; using Robust.Shared.ViewVariables; using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent; -namespace Content.Client.Arcade.UI +namespace Content.Client.Arcade.UI; + +public sealed class SpaceVillainArcadeBoundUserInterface : BoundUserInterface { - public sealed class SpaceVillainArcadeBoundUserInterface : BoundUserInterface + [ViewVariables] private SpaceVillainArcadeMenu? _menu; + + //public SharedSpaceVillainArcadeComponent SpaceVillainArcade; + + public SpaceVillainArcadeBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { - [ViewVariables] private SpaceVillainArcadeMenu? _menu; + SendAction(PlayerAction.RequestData); + } - //public SharedSpaceVillainArcadeComponent SpaceVillainArcade; + public void SendAction(PlayerAction action) + { + SendMessage(new SpaceVillainArcadePlayerActionMessage(action)); + } - public SpaceVillainArcadeBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) - { - SendAction(PlayerAction.RequestData); - } + protected override void Open() + { + base.Open(); - public void SendAction(PlayerAction action) - { - SendMessage(new SpaceVillainArcadePlayerActionMessage(action)); - } + _menu = new SpaceVillainArcadeMenu(this); - protected override void Open() - { - base.Open(); + _menu.OnClose += Close; + _menu.OpenCentered(); + } - /*if(!Owner.Owner.TryGetComponent(out SharedSpaceVillainArcadeComponent spaceVillainArcade)) - { - return; - } + protected override void ReceiveMessage(BoundUserInterfaceMessage message) + { + if (message is SpaceVillainArcadeDataUpdateMessage msg) + _menu?.UpdateInfo(msg); + } - SpaceVillainArcade = spaceVillainArcade;*/ + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); - _menu = new SpaceVillainArcadeMenu(this); - - _menu.OnClose += Close; - _menu.OpenCentered(); - } - - protected override void ReceiveMessage(BoundUserInterfaceMessage message) - { - if (message is SpaceVillainArcadeDataUpdateMessage msg) _menu?.UpdateInfo(msg); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) _menu?.Dispose(); - } + if (disposing) + _menu?.Dispose(); } } diff --git a/Content.Server/Arcade/ArcadeSystem.BlockGame.cs b/Content.Server/Arcade/ArcadeSystem.BlockGame.cs deleted file mode 100644 index 72f416df69..0000000000 --- a/Content.Server/Arcade/ArcadeSystem.BlockGame.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Server.Arcade.Components; -using Content.Server.Power.Components; - -namespace Content.Server.Arcade; - -public sealed partial class ArcadeSystem -{ - private void InitializeBlockGame() - { - SubscribeLocalEvent(OnBlockPowerChanged); - } - - private static void OnBlockPowerChanged(EntityUid uid, BlockGameArcadeComponent component, ref PowerChangedEvent args) - { - component.OnPowerStateChanged(args); - } -} diff --git a/Content.Server/Arcade/ArcadeSystem.SpaceVillain.cs b/Content.Server/Arcade/ArcadeSystem.SpaceVillain.cs deleted file mode 100644 index a223d24655..0000000000 --- a/Content.Server/Arcade/ArcadeSystem.SpaceVillain.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Server.Arcade.Components; -using Content.Server.Power.Components; - -namespace Content.Server.Arcade; - -public sealed partial class ArcadeSystem -{ - private void InitializeSpaceVillain() - { - SubscribeLocalEvent(OnSVillainPower); - } - - private void OnSVillainPower(EntityUid uid, SpaceVillainArcadeComponent component, ref PowerChangedEvent args) - { - component.OnPowerStateChanged(args); - } -} diff --git a/Content.Server/Arcade/ArcadeSystem.cs b/Content.Server/Arcade/ArcadeSystem.cs index 679f35bdfb..d5013c9966 100644 --- a/Content.Server/Arcade/ArcadeSystem.cs +++ b/Content.Server/Arcade/ArcadeSystem.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Server.Arcade.Components; using Content.Server.UserInterface; using Content.Shared.Arcade; using Robust.Shared.Utility; @@ -17,25 +16,6 @@ namespace Content.Server.Arcade public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnAfterUIOpen); - SubscribeLocalEvent(OnAfterUIOpenSV); - SubscribeLocalEvent((_,c,args) => c.UnRegisterPlayerSession((IPlayerSession)args.Session)); - InitializeBlockGame(); - InitializeSpaceVillain(); - } - - private void OnAfterUIOpen(EntityUid uid, BlockGameArcadeComponent component, AfterActivatableUIOpenEvent args) - { - var actor = Comp(args.User); - if (component.UserInterface?.SessionHasOpen(actor.PlayerSession) == true) - { - component.RegisterPlayerSession(actor.PlayerSession); - } - } - - private void OnAfterUIOpenSV(EntityUid uid, SpaceVillainArcadeComponent component, AfterActivatableUIOpenEvent args) - { - component.Game ??= new SpaceVillainArcadeComponent.SpaceVillainGame(component); } public HighScorePlacement RegisterHighScore(string name, int score) @@ -87,14 +67,6 @@ namespace Content.Server.Arcade return placement; } - public override void Update(float frameTime) - { - foreach (var comp in EntityManager.EntityQuery()) - { - comp.DoGameTick(frameTime); - } - } - public readonly struct HighScorePlacement { public readonly int? GlobalPlacement; diff --git a/Content.Server/Arcade/BlockGame/BlockGame.GameState.cs b/Content.Server/Arcade/BlockGame/BlockGame.GameState.cs new file mode 100644 index 0000000000..f99d0cd685 --- /dev/null +++ b/Content.Server/Arcade/BlockGame/BlockGame.GameState.cs @@ -0,0 +1,256 @@ +using Content.Shared.Arcade; +using Robust.Shared.Random; +using System.Linq; + +namespace Content.Server.Arcade.BlockGame; + +public sealed partial class BlockGame +{ + // note: field is 10(0 -> 9) wide and 20(0 -> 19) high + + /// + /// Whether the given position is above the bottom of the playfield. + /// + private bool LowerBoundCheck(Vector2i position) + { + return position.Y < 20; + } + + /// + /// Whether the given position is horizontally positioned within the playfield. + /// + private bool BorderCheck(Vector2i position) + { + return position.X >= 0 && position.X < 10; + } + + /// + /// Whether the given position is currently occupied by a piece. + /// Yes this is on O(n) collision check, it works well enough. + /// + private bool ClearCheck(Vector2i position) + { + return _field.All(block => !position.Equals(block.Position)); + } + + /// + /// Whether a block can be dropped into the given position. + /// + private bool DropCheck(Vector2i position) + { + return LowerBoundCheck(position) && ClearCheck(position); + } + + /// + /// Whether a block can be moved horizontally into the given position. + /// + private bool MoveCheck(Vector2i position) + { + return BorderCheck(position) && ClearCheck(position); + } + + /// + /// Whether a block can be rotated into the given position. + /// + private bool RotateCheck(Vector2i position) + { + return BorderCheck(position) && LowerBoundCheck(position) && ClearCheck(position); + } + + /// + /// The set of blocks that have landed in the field. + /// + private readonly List _field = new(); + + /// + /// The current pool of pickable pieces. + /// Refreshed when a piece is requested while empty. + /// Ensures that the player is given an even spread of pieces by making picked pieces unpickable until the rest are picked. + /// + private List _blockGamePiecesBuffer = new(); + + /// + /// Gets a random piece from the pool of pickable pieces. () + /// + private BlockGamePiece GetRandomBlockGamePiece(IRobustRandom random) + { + if (_blockGamePiecesBuffer.Count == 0) + { + _blockGamePiecesBuffer = _allBlockGamePieces.ToList(); + } + + var chosenPiece = random.Pick(_blockGamePiecesBuffer); + _blockGamePiecesBuffer.Remove(chosenPiece); + return BlockGamePiece.GetPiece(chosenPiece); + } + + /// + /// The piece that is currently falling and controllable by the player. + /// + private BlockGamePiece CurrentPiece + { + get => _internalCurrentPiece; + set + { + _internalCurrentPiece = value; + UpdateFieldUI(); + } + } + private BlockGamePiece _internalCurrentPiece = default!; + + + /// + /// The position of the falling piece. + /// + private Vector2i _currentPiecePosition; + + /// + /// The rotation of the falling piece. + /// + private BlockGamePieceRotation _currentRotation; + + /// + /// The amount of time (in seconds) between piece steps. + /// Decreased by a constant amount per level. + /// Decreased heavily by soft dropping the current piece (holding down). + /// + private float Speed => Math.Max(0.03f, (_softDropPressed ? SoftDropModifier : 1f) - 0.03f * Level); + + /// + /// The base amount of time between piece steps while softdropping. + /// + private const float SoftDropModifier = 0.1f; + + + /// + /// Attempts to rotate the falling piece to a new rotation. + /// + private void TrySetRotation(BlockGamePieceRotation rotation) + { + if (!_running) + return; + + if (!CurrentPiece.CanSpin) + return; + + if (!CurrentPiece.Positions(_currentPiecePosition, rotation) + .All(RotateCheck)) + return; + + _currentRotation = rotation; + UpdateFieldUI(); + } + + + /// + /// The next piece that will be dispensed. + /// + private BlockGamePiece NextPiece + { + get => _internalNextPiece; + set + { + _internalNextPiece = value; + SendNextPieceUpdate(); + } + } + private BlockGamePiece _internalNextPiece = default!; + + + /// + /// The piece the player has chosen to hold in reserve. + /// + private BlockGamePiece? HeldPiece + { + get => _internalHeldPiece; + set + { + _internalHeldPiece = value; + SendHoldPieceUpdate(); + } + } + private BlockGamePiece? _internalHeldPiece = null; + + /// + /// Prevents the player from holding the currently falling piece if true. + /// Set true when a piece is held and set false when a new piece is created. + /// Exists to prevent the player from swapping between two pieces forever and never actually letting the block fall. + /// + private bool _holdBlock = false; + + /// + /// The number of lines that have been cleared in the current level. + /// Automatically advances the game to the next level if enough lines are cleared. + /// + private int ClearedLines + { + get => _clearedLines; + set + { + _clearedLines = value; + + if (_clearedLines < LevelRequirement) + return; + + _clearedLines -= LevelRequirement; + Level++; + } + } + private int _clearedLines = 0; + + /// + /// The number of lines that must be cleared to advance to the next level. + /// + private int LevelRequirement => Math.Min(100, Math.Max(Level * 10 - 50, 10)); + + + /// + /// The current level of the game. + /// Effects the movement speed of the active piece. + /// + private int Level + { + get => _internalLevel; + set + { + if (_internalLevel == value) + return; + _internalLevel = value; + SendLevelUpdate(); + } + } + private int _internalLevel = 0; + + + /// + /// The total number of points accumulated in the current game. + /// + private int Points + { + get => _internalPoints; + set + { + if (_internalPoints == value) + return; + _internalPoints = value; + SendPointsUpdate(); + } + } + private int _internalPoints = 0; + + /// + /// Setter for the setter for the number of points accumulated in the current game. + /// + private void AddPoints(int amount) + { + if (amount == 0) + return; + + Points += amount; + } + + /// + /// Where the current game has placed amongst the leaderboard. + /// + private ArcadeSystem.HighScorePlacement? _highScorePlacement = null; +} diff --git a/Content.Server/Arcade/BlockGame/BlockGame.Pieces.cs b/Content.Server/Arcade/BlockGame/BlockGame.Pieces.cs new file mode 100644 index 0000000000..bf42fa34a4 --- /dev/null +++ b/Content.Server/Arcade/BlockGame/BlockGame.Pieces.cs @@ -0,0 +1,238 @@ +using Content.Shared.Arcade; +using System.Linq; + +namespace Content.Server.Arcade.BlockGame; + +public sealed partial class BlockGame +{ + /// + /// The set of types of game pieces that exist. + /// Used as templates when creating pieces for the game. + /// + private readonly BlockGamePieceType[] _allBlockGamePieces; + + /// + /// The set of types of game pieces that exist. + /// Used to generate the templates used when creating pieces for the game. + /// + private enum BlockGamePieceType + { + I, + L, + LInverted, + S, + SInverted, + T, + O + } + + /// + /// The set of possible rotations for the game pieces. + /// + private enum BlockGamePieceRotation + { + North, + East, + South, + West + } + + /// + /// A static extension for the rotations that allows rotating through the possible rotations. + /// + 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) + }; + } + + /// + /// A static extension for the rotations that allows rotating through the possible rotations. + /// + private struct BlockGamePiece + { + /// + /// Where all of the blocks that make up this piece are located relative to the origin of the piece. + /// + public Vector2i[] Offsets; + + /// + /// The color of all of the blocks that make up this piece. + /// + private BlockGameBlock.BlockGameBlockColor _gameBlockColor; + + /// + /// Whether or not the block should be able to rotate about its origin. + /// + public bool CanSpin; + + /// + /// Generates a list of the positions of each block comprising this game piece in worldspace. + /// + /// The position of the game piece in worldspace. + /// The rotation of the game piece in worldspace. + public readonly Vector2i[] Positions(Vector2i center, BlockGamePieceRotation rotation) + { + return RotatedOffsets(rotation).Select(v => center + v).ToArray(); + } + + /// + /// Gets the relative position of each block comprising this piece given a rotation. + /// + /// The rotation to be applied to the local position of the blocks in this piece. + private readonly Vector2i[] RotatedOffsets(BlockGamePieceRotation rotation) + { + var rotatedOffsets = (Vector2i[]) Offsets.Clone(); + //until i find a better algo + var amount = rotation switch + { + BlockGamePieceRotation.North => 0, + BlockGamePieceRotation.East => 1, + BlockGamePieceRotation.South => 2, + BlockGamePieceRotation.West => 3, + _ => 0 + }; + + for (var i = 0; i < amount; i++) + { + for (var j = 0; j < rotatedOffsets.Length; j++) + { + rotatedOffsets[j] = rotatedOffsets[j].Rotate90DegreesAsOffset(); + } + } + + return rotatedOffsets; + } + + /// + /// Gets a list of all of the blocks comprising this piece in worldspace. + /// + /// The position of the game piece in worldspace. + /// The rotation of the game piece in worldspace. + public readonly BlockGameBlock[] Blocks(Vector2i center, BlockGamePieceRotation rotation) + { + var positions = Positions(center, rotation); + var result = new BlockGameBlock[positions.Length]; + var i = 0; + foreach (var position in positions) + { + result[i++] = position.ToBlockGameBlock(_gameBlockColor); + } + + return result; + } + + /// + /// Gets a list of all of the blocks comprising this piece in worldspace. + /// Used to generate the held piece/next piece preview images. + /// + public readonly BlockGameBlock[] BlocksForPreview() + { + var xOffset = 0; + var yOffset = 0; + foreach (var offset in Offsets) + { + if (offset.X < xOffset) + xOffset = offset.X; + if (offset.Y < yOffset) + yOffset = offset.Y; + } + + return Blocks(new Vector2i(-xOffset, -yOffset), BlockGamePieceRotation.North); + } + + /// + /// Generates a game piece for a given type of game piece. + /// See for the available options. + /// + /// The type of game piece to generate. + 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/Arcade/BlockGame/BlockGame.Ui.cs b/Content.Server/Arcade/BlockGame/BlockGame.Ui.cs new file mode 100644 index 0000000000..92a96ea226 --- /dev/null +++ b/Content.Server/Arcade/BlockGame/BlockGame.Ui.cs @@ -0,0 +1,364 @@ +using Content.Shared.Arcade; +using Robust.Server.Player; +using System.Linq; + +namespace Content.Server.Arcade.BlockGame; + +public sealed partial class BlockGame +{ + /// + /// How often to check the currently pressed inputs for whether to move the active piece horizontally. + /// + private const float PressCheckSpeed = 0.08f; + + /// + /// Whether the left button is pressed. + /// Moves the active piece left if true. + /// + private bool _leftPressed = false; + + /// + /// How long the left button has been pressed. + /// + private float _accumulatedLeftPressTime = 0f; + + /// + /// Whether the right button is pressed. + /// Moves the active piece right if true. + /// + private bool _rightPressed = false; + + /// + /// How long the right button has been pressed. + /// + private float _accumulatedRightPressTime = 0f; + + /// + /// Whether the down button is pressed. + /// Speeds up how quickly the active piece falls if true. + /// + private bool _softDropPressed = false; + + + /// + /// Handles user input. + /// + /// The action to current player has prompted. + public void ProcessInput(BlockGamePlayerAction action) + { + if (_running) + { + switch (action) + { + case BlockGamePlayerAction.StartLeft: + _leftPressed = true; + break; + case BlockGamePlayerAction.StartRight: + _rightPressed = true; + break; + case BlockGamePlayerAction.Rotate: + TrySetRotation(Next(_currentRotation, false)); + break; + case BlockGamePlayerAction.CounterRotate: + TrySetRotation(Next(_currentRotation, true)); + break; + case BlockGamePlayerAction.SoftdropStart: + _softDropPressed = true; + if (_accumulatedFieldFrameTime > Speed) + _accumulatedFieldFrameTime = Speed; //to prevent jumps + break; + case BlockGamePlayerAction.Harddrop: + PerformHarddrop(); + break; + case BlockGamePlayerAction.Hold: + HoldPiece(); + break; + } + } + + switch (action) + { + case BlockGamePlayerAction.EndLeft: + _leftPressed = false; + break; + case BlockGamePlayerAction.EndRight: + _rightPressed = false; + break; + case BlockGamePlayerAction.SoftdropEnd: + _softDropPressed = false; + break; + case BlockGamePlayerAction.Pause: + _running = false; + SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, Started)); + break; + case BlockGamePlayerAction.Unpause: + if (!_gameOver && Started) + { + _running = true; + SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game)); + } + break; + case BlockGamePlayerAction.ShowHighscores: + _running = false; + SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Highscores, Started)); + break; + } + } + + /// + /// Handle moving the active game piece in response to user input. + /// + /// The amount of time the current game tick covers. + private void InputTick(float frameTime) + { + var anythingChanged = false; + if (_leftPressed) + { + _accumulatedLeftPressTime += frameTime; + + while (_accumulatedLeftPressTime >= PressCheckSpeed) + { + + if (CurrentPiece.Positions(_currentPiecePosition.AddToX(-1), _currentRotation) + .All(MoveCheck)) + { + _currentPiecePosition = _currentPiecePosition.AddToX(-1); + anythingChanged = true; + } + + _accumulatedLeftPressTime -= PressCheckSpeed; + } + } + + if (_rightPressed) + { + _accumulatedRightPressTime += frameTime; + + while (_accumulatedRightPressTime >= PressCheckSpeed) + { + if (CurrentPiece.Positions(_currentPiecePosition.AddToX(1), _currentRotation) + .All(MoveCheck)) + { + _currentPiecePosition = _currentPiecePosition.AddToX(1); + anythingChanged = true; + } + + _accumulatedRightPressTime -= PressCheckSpeed; + } + } + + if (anythingChanged) + UpdateFieldUI(); + } + + /// + /// Handles sending a message to all players/spectators. + /// + /// The message to broadcase to all players/spectators. + private void SendMessage(BoundUserInterfaceMessage message) + { + if (_uiSystem.TryGetUi(_owner, BlockGameUiKey.Key, out var bui)) + _uiSystem.SendUiMessage(bui, message); + } + + /// + /// Handles sending a message to a specific player/spectator. + /// + /// The message to send to a specific player/spectator. + /// The target recipient. + private void SendMessage(BoundUserInterfaceMessage message, IPlayerSession session) + { + if (_uiSystem.TryGetUi(_owner, BlockGameUiKey.Key, out var bui)) + _uiSystem.TrySendUiMessage(bui, message, session); + } + + /// + /// Handles sending the current state of the game to a player that has just opened the UI. + /// + /// The target recipient. + public void UpdateNewPlayerUI(IPlayerSession session) + { + if (_gameOver) + { + SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement), session); + return; + } + + if (Paused) + SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, Started), session); + else + SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game, Started), session); + + FullUpdate(session); + } + + /// + /// Handles broadcasting the full player-visible game state to everyone who can see the game. + /// + private void FullUpdate() + { + UpdateFieldUI(); + SendHoldPieceUpdate(); + SendNextPieceUpdate(); + SendLevelUpdate(); + SendPointsUpdate(); + SendHighscoreUpdate(); + } + + /// + /// Handles broadcasting the full player-visible game state to a specific player/spectator. + /// + /// The target recipient. + private void FullUpdate(IPlayerSession session) + { + UpdateFieldUI(session); + SendNextPieceUpdate(session); + SendHoldPieceUpdate(session); + SendLevelUpdate(session); + SendPointsUpdate(session); + SendHighscoreUpdate(session); + } + + /// + /// Handles broadcasting the current location of all of the blocks in the playfield + the active piece to all spectators. + /// + public void UpdateFieldUI() + { + if (!Started) + return; + + var computedField = ComputeField(); + SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField)); + } + + /// + /// Handles broadcasting the current location of all of the blocks in the playfield + the active piece to a specific player/spectator. + /// + /// The target recipient. + public void UpdateFieldUI(IPlayerSession session) + { + if (!Started) + return; + + var computedField = ComputeField(); + SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField), session); + } + + /// + /// Generates the set of blocks to send to viewers. + /// + public List ComputeField() + { + var result = new List(); + result.AddRange(_field); + result.AddRange(CurrentPiece.Blocks(_currentPiecePosition, _currentRotation)); + + var dropGhostPosition = _currentPiecePosition; + while (CurrentPiece.Positions(dropGhostPosition.AddToY(1), _currentRotation) + .All(DropCheck)) + { + dropGhostPosition = dropGhostPosition.AddToY(1); + } + + if (dropGhostPosition != _currentPiecePosition) + { + var blox = CurrentPiece.Blocks(dropGhostPosition, _currentRotation); + for (var i = 0; i < blox.Length; i++) + { + result.Add(new BlockGameBlock(blox[i].Position, BlockGameBlock.ToGhostBlockColor(blox[i].GameBlockColor))); + } + } + return result; + } + + /// + /// Broadcasts the state of the next queued piece to all viewers. + /// + private void SendNextPieceUpdate() + { + SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(NextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock)); + } + + /// + /// Broadcasts the state of the next queued piece to a specific viewer. + /// + /// The target recipient. + private void SendNextPieceUpdate(IPlayerSession session) + { + SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(NextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock), session); + } + + /// + /// Broadcasts the state of the currently held piece to all viewers. + /// + private void SendHoldPieceUpdate() + { + if (HeldPiece.HasValue) + SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(HeldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock)); + else + SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(Array.Empty(), BlockGameMessages.BlockGameVisualType.HoldBlock)); + } + + /// + /// Broadcasts the state of the currently held piece to a specific viewer. + /// + /// The target recipient. + private void SendHoldPieceUpdate(IPlayerSession session) + { + if (HeldPiece.HasValue) + SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(HeldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock), session); + else + SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(Array.Empty(), BlockGameMessages.BlockGameVisualType.HoldBlock), session); + } + + /// + /// Broadcasts the current game level to all viewers. + /// + private void SendLevelUpdate() + { + SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level)); + } + + /// + /// Broadcasts the current game level to a specific viewer. + /// + /// The target recipient. + private void SendLevelUpdate(IPlayerSession session) + { + SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level), session); + } + + /// + /// Broadcasts the current game score to all viewers. + /// + private void SendPointsUpdate() + { + SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points)); + } + + /// + /// Broadcasts the current game score to a specific viewer. + /// + /// The target recipient. + private void SendPointsUpdate(IPlayerSession session) + { + SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points), session); + } + + /// + /// Broadcasts the current game high score positions to all viewers. + /// + private void SendHighscoreUpdate() + { + SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(_arcadeSystem.GetLocalHighscores(), _arcadeSystem.GetGlobalHighscores())); + } + + /// + /// Broadcasts the current game high score positions to a specific viewer. + /// + /// The target recipient. + private void SendHighscoreUpdate(IPlayerSession session) + { + SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(_arcadeSystem.GetLocalHighscores(), _arcadeSystem.GetGlobalHighscores()), session); + } +} diff --git a/Content.Server/Arcade/BlockGame/BlockGame.cs b/Content.Server/Arcade/BlockGame/BlockGame.cs new file mode 100644 index 0000000000..3af1828d56 --- /dev/null +++ b/Content.Server/Arcade/BlockGame/BlockGame.cs @@ -0,0 +1,303 @@ +using Content.Shared.Arcade; +using Robust.Server.GameObjects; +using Robust.Shared.Random; +using System.Linq; + +namespace Content.Server.Arcade.BlockGame; + +public sealed partial class BlockGame +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + private readonly ArcadeSystem _arcadeSystem = default!; + private readonly UserInterfaceSystem _uiSystem = default!; + + /// + /// What entity is currently hosting this game of NT-BG. + /// + private readonly EntityUid _owner = default!; + + /// + /// Whether the game has been started. + /// + public bool Started { get; private set; } = false; + + /// + /// Whether the game is currently running (not paused). + /// + private bool _running = false; + + /// + /// Whether the game should not currently be running. + /// + private bool Paused => !(Started && _running); + + /// + /// Whether the game has finished. + /// + private bool _gameOver = false; + + /// + /// Whether the game should have finished given the current game state. + /// + private bool IsGameOver => _field.Any(block => block.Position.Y == 0); + + + public BlockGame(EntityUid owner) + { + IoCManager.InjectDependencies(this); + _arcadeSystem = _entityManager.System(); + _uiSystem = _entityManager.System(); + + _owner = owner; + _allBlockGamePieces = (BlockGamePieceType[]) Enum.GetValues(typeof(BlockGamePieceType)); + _internalNextPiece = GetRandomBlockGamePiece(_random); + InitializeNewBlock(); + } + + /// + /// Starts the game. Including relaying this info to everyone watching. + /// + public void StartGame() + { + SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game)); + + FullUpdate(); + + Started = true; + _running = true; + _gameOver = false; + } + + /// + /// Handles ending the game and updating the high scores. + /// + private void InvokeGameover() + { + _running = false; + _gameOver = true; + + if (_entityManager.TryGetComponent(_owner, out var cabinet) + && _entityManager.TryGetComponent(cabinet.Player?.AttachedEntity, out var meta)) + { + _highScorePlacement = _arcadeSystem.RegisterHighScore(meta.EntityName, Points); + SendHighscoreUpdate(); + } + SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement)); + } + + /// + /// Handle the game simulation and user input. + /// + /// The amount of time the current game tick covers. + public void GameTick(float frameTime) + { + if (!_running) + return; + + InputTick(frameTime); + + FieldTick(frameTime); + } + + /// + /// The amount of time that has passed since the active piece last moved vertically, + /// + private float _accumulatedFieldFrameTime; + + /// + /// Handles timing the movements of the active game piece. + /// + /// The amount of time the current game tick covers. + private void FieldTick(float frameTime) + { + _accumulatedFieldFrameTime += frameTime; + + // Speed goes negative sometimes. uhhhh max() it I guess!!! + var checkTime = Math.Max(0.03f, Speed); + + while (_accumulatedFieldFrameTime >= checkTime) + { + if (_softDropPressed) + AddPoints(1); + + InternalFieldTick(); + + _accumulatedFieldFrameTime -= checkTime; + } + } + + /// + /// Handles the active game piece moving down. + /// Also triggers scanning for cleared lines. + /// + private void InternalFieldTick() + { + if (CurrentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation) + .All(DropCheck)) + { + _currentPiecePosition = _currentPiecePosition.AddToY(1); + } + else + { + var blocks = CurrentPiece.Blocks(_currentPiecePosition, _currentRotation); + _field.AddRange(blocks); + + //check loose conditions + if (IsGameOver) + { + InvokeGameover(); + return; + } + + InitializeNewBlock(); + } + + CheckField(); + + UpdateFieldUI(); + } + + /// + /// Handles scanning for cleared lines and accumulating points. + /// + private void CheckField() + { + var pointsToAdd = 0; + var consecutiveLines = 0; + var clearedLines = 0; + for (var y = 0; y < 20; y++) + { + if (CheckLine(y)) + { + //line was cleared + y--; + consecutiveLines++; + clearedLines++; + } + else if (consecutiveLines != 0) + { + var mod = consecutiveLines switch + { + 1 => 40, + 2 => 100, + 3 => 300, + 4 => 1200, + _ => 0 + }; + pointsToAdd += mod * (Level + 1); + } + } + + ClearedLines += clearedLines; + AddPoints(pointsToAdd); + } + + /// + /// Returns whether the line at the given position is full. + /// Clears the line if it was full and moves the above lines down. + /// + /// The position of the line to check. + 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; + } + + /// + /// Moves all of the lines above the given line down by one. + /// Used to fill in cleared lines. + /// + /// The position of the line above which to drop the lines. + private void FillLine(int y) + { + for (var c_y = y; c_y > 0; c_y--) + { + for (var j = 0; j < _field.Count; j++) + { + if (_field[j].Position.Y != c_y - 1) + continue; + + _field[j] = new BlockGameBlock(_field[j].Position.AddToY(1), _field[j].GameBlockColor); + } + } + } + + /// + /// Generates a new active piece from the previewed next piece. + /// Repopulates the previewed next piece with a piece from the pool of possible next pieces. + /// + private void InitializeNewBlock() + { + InitializeNewBlock(NextPiece); + NextPiece = GetRandomBlockGamePiece(_random); + _holdBlock = false; + + SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(NextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock)); + } + + /// + /// Generates a new active piece from the previewed next piece. + /// + /// The piece to set as the active piece. + private void InitializeNewBlock(BlockGamePiece piece) + { + _currentPiecePosition = new Vector2i(5, 0); + + _currentRotation = BlockGamePieceRotation.North; + + CurrentPiece = piece; + UpdateFieldUI(); + } + + /// + /// Buffers the currently active piece. + /// Replaces the active piece with either the previously held piece or the previewed next piece as necessary. + /// + private void HoldPiece() + { + if (!_running) + return; + if (_holdBlock) + return; + + var tempHeld = HeldPiece; + HeldPiece = CurrentPiece; + _holdBlock = true; + + if (!tempHeld.HasValue) + { + InitializeNewBlock(); + return; + } + + InitializeNewBlock(tempHeld.Value); + } + + /// + /// Immediately drops the currently active piece the remaining distance. + /// + private void PerformHarddrop() + { + var spacesDropped = 0; + while (CurrentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation) + .All(DropCheck)) + { + _currentPiecePosition = _currentPiecePosition.AddToY(1); + spacesDropped++; + } + AddPoints(spacesDropped * 2); + + InternalFieldTick(); + } +} diff --git a/Content.Server/Arcade/BlockGame/BlockGameArcadeComponent.cs b/Content.Server/Arcade/BlockGame/BlockGameArcadeComponent.cs new file mode 100644 index 0000000000..8f168dd8d5 --- /dev/null +++ b/Content.Server/Arcade/BlockGame/BlockGameArcadeComponent.cs @@ -0,0 +1,22 @@ +using Robust.Server.Player; + +namespace Content.Server.Arcade.BlockGame; + +[RegisterComponent] +public sealed class BlockGameArcadeComponent : Component +{ + /// + /// The currently active session of NT-BG. + /// + public BlockGame? Game = null; + + /// + /// The player currently playing the active session of NT-BG. + /// + public IPlayerSession? Player = null; + + /// + /// The players currently viewing (but not playing) the active session of NT-BG. + /// + public readonly List Spectators = new(); +} diff --git a/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs b/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs new file mode 100644 index 0000000000..06390a4245 --- /dev/null +++ b/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs @@ -0,0 +1,123 @@ +using Content.Server.Power.Components; +using Content.Server.UserInterface; +using Content.Shared.Arcade; +using Robust.Server.GameObjects; +using Robust.Server.Player; + +namespace Content.Server.Arcade.BlockGame; + +public sealed class BlockGameArcadeSystem : EntitySystem +{ + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnAfterUIOpen); + SubscribeLocalEvent(OnAfterUiClose); + SubscribeLocalEvent(OnBlockPowerChanged); + SubscribeLocalEvent(OnPlayerAction); + } + + public override void Update(float frameTime) + { + var query = EntityManager.EntityQueryEnumerator(); + while (query.MoveNext(out var _, out var blockGame)) + { + blockGame.Game?.GameTick(frameTime); + } + } + + private void UpdatePlayerStatus(EntityUid uid, IPlayerSession session, BoundUserInterface? bui = null, BlockGameArcadeComponent? blockGame = null) + { + if (!Resolve(uid, ref blockGame)) + return; + if (bui == null && !_uiSystem.TryGetUi(uid, BlockGameUiKey.Key, out bui)) + return; + + _uiSystem.TrySendUiMessage(bui, new BlockGameMessages.BlockGameUserStatusMessage(blockGame.Player == session), session); + } + + private void OnComponentInit(EntityUid uid, BlockGameArcadeComponent component, ComponentInit args) + { + component.Game = new(uid); + } + + private void OnAfterUIOpen(EntityUid uid, BlockGameArcadeComponent component, AfterActivatableUIOpenEvent args) + { + if (!TryComp(args.User, out var actor)) + return; + if (!_uiSystem.TryGetUi(uid, BlockGameUiKey.Key, out var bui)) + return; + + var session = actor.PlayerSession; + if (!bui.SubscribedSessions.Contains(session)) + return; + + if (component.Player == null) + component.Player = session; + else + component.Spectators.Add(session); + + UpdatePlayerStatus(uid, session, bui, component); + component.Game?.UpdateNewPlayerUI(session); + } + + private void OnAfterUiClose(EntityUid uid, BlockGameArcadeComponent component, BoundUIClosedEvent args) + { + if (args.Session is not IPlayerSession session) + return; + + if (component.Player != session) + { + component.Spectators.Remove(session); + UpdatePlayerStatus(uid, session, blockGame: component); + return; + } + + var temp = component.Player; + if (component.Spectators.Count > 0) + { + component.Player = component.Spectators[0]; + component.Spectators.Remove(component.Player); + UpdatePlayerStatus(uid, component.Player, blockGame: component); + } + else + component.Player = null; + + UpdatePlayerStatus(uid, temp, blockGame: component); + } + + private void OnBlockPowerChanged(EntityUid uid, BlockGameArcadeComponent component, ref PowerChangedEvent args) + { + if (args.Powered) + return; + + if (_uiSystem.TryGetUi(uid, BlockGameUiKey.Key, out var bui)) + _uiSystem.CloseAll(bui); + component.Player = null; + component.Spectators.Clear(); + } + + private void OnPlayerAction(EntityUid uid, BlockGameArcadeComponent component, BlockGameMessages.BlockGamePlayerActionMessage msg) + { + if (component.Game == null) + return; + if (!BlockGameUiKey.Key.Equals(msg.UiKey)) + return; + if (msg.Session != component.Player) + return; + + if (msg.PlayerAction == BlockGamePlayerAction.NewGame) + { + if (component.Game.Started == true) + component.Game = new(uid); + component.Game.StartGame(); + return; + } + + component.Game.ProcessInput(msg.PlayerAction); + } +} diff --git a/Content.Server/Arcade/Components/BlockGameArcadeComponent.cs b/Content.Server/Arcade/Components/BlockGameArcadeComponent.cs deleted file mode 100644 index 924b5709ca..0000000000 --- a/Content.Server/Arcade/Components/BlockGameArcadeComponent.cs +++ /dev/null @@ -1,867 +0,0 @@ -using System.Linq; -using Content.Server.Power.Components; -using Content.Server.UserInterface; -using Content.Shared.Arcade; -using Robust.Server.GameObjects; -using Robust.Server.Player; -using Robust.Shared.Random; - -namespace Content.Server.Arcade.Components -{ - [RegisterComponent] - public sealed class BlockGameArcadeComponent : Component - { - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - - public bool Powered => _entityManager.TryGetComponent(Owner, out var powerReceiverComponent) && powerReceiverComponent.Powered; - public BoundUserInterface? UserInterface => Owner.GetUIOrNull(BlockGameUiKey.Key); - - private BlockGame? _game; - - private IPlayerSession? _player; - private readonly List _spectators = new(); - - public void RegisterPlayerSession(IPlayerSession session) - { - if (_player == null) _player = session; - else _spectators.Add(session); - - UpdatePlayerStatus(session); - _game?.UpdateNewPlayerUI(session); - } - - private void DeactivePlayer(IPlayerSession session) - { - if (_player != session) return; - - var temp = _player; - _player = null; - if (_spectators.Count != 0) - { - _player = _spectators[0]; - _spectators.Remove(_player); - UpdatePlayerStatus(_player); - } - _spectators.Add(temp); - - UpdatePlayerStatus(temp); - } - - public void UnRegisterPlayerSession(IPlayerSession session) - { - if (_player == session) - { - DeactivePlayer(_player); - } - else - { - _spectators.Remove(session); - UpdatePlayerStatus(session); - } - } - - private void UpdatePlayerStatus(IPlayerSession session) - { - UserInterface?.SendMessage(new BlockGameMessages.BlockGameUserStatusMessage(_player == session), session); - } - - protected override void Initialize() - { - base.Initialize(); - if (UserInterface != null) - { - UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage; - } - _game = new BlockGame(this); - } - - public void OnPowerStateChanged(PowerChangedEvent e) - { - if (e.Powered) return; - - UserInterface?.CloseAll(); - _player = null; - _spectators.Clear(); - } - - private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage obj) - { - switch (obj.Message) - { - case BlockGameMessages.BlockGamePlayerActionMessage playerActionMessage: - if (obj.Session != _player) break; - - if (playerActionMessage.PlayerAction == BlockGamePlayerAction.NewGame) - { - if(_game?.Started == true) _game = new BlockGame(this); - _game?.StartGame(); - } - else - { - _game?.ProcessInput(playerActionMessage.PlayerAction); - } - - break; - } - } - - public void DoGameTick(float frameTime) - { - _game?.GameTick(frameTime); - } - - private sealed class BlockGame - { - //note: field is 10(0 -> 9) wide and 20(0 -> 19) high - - private readonly BlockGameArcadeComponent _component; - - private readonly List _field = new(); - - private BlockGamePiece _currentPiece; - - private BlockGamePiece _nextPiece - { - get => _internalNextPiece; - set - { - _internalNextPiece = value; - SendNextPieceUpdate(); - } - } - private BlockGamePiece _internalNextPiece; - - private void SendNextPieceUpdate() - { - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock)); - } - - private void SendNextPieceUpdate(IPlayerSession session) - { - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock), session); - } - - private bool _holdBlock = false; - private BlockGamePiece? _heldPiece - { - get => _internalHeldPiece; - set - { - _internalHeldPiece = value; - SendHoldPieceUpdate(); - } - } - - private BlockGamePiece? _internalHeldPiece = null; - - private void SendHoldPieceUpdate() - { - if(_heldPiece.HasValue) _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_heldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock)); - else _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(new BlockGameBlock[0], BlockGameMessages.BlockGameVisualType.HoldBlock)); - } - - private void SendHoldPieceUpdate(IPlayerSession session) - { - if(_heldPiece.HasValue) _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_heldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock), session); - else _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(new BlockGameBlock[0], BlockGameMessages.BlockGameVisualType.HoldBlock), session); - } - - private Vector2i _currentPiecePosition; - private BlockGamePieceRotation _currentRotation; - private float _softDropModifier = 0.1f; - - private float Speed => - -0.03f * Level + 1 * (!_softDropPressed ? 1 : _softDropModifier); - - private const float _pressCheckSpeed = 0.08f; - - private bool _running; - public bool Paused => !(_running && _started); - private bool _started; - public bool Started => _started; - private bool _gameOver; - - private bool _leftPressed; - private bool _rightPressed; - private bool _softDropPressed; - - private int Points - { - get => _internalPoints; - set - { - if (_internalPoints == value) return; - _internalPoints = value; - SendPointsUpdate(); - } - } - private int _internalPoints; - - private ArcadeSystem.HighScorePlacement? _highScorePlacement = null; - - private void SendPointsUpdate() - { - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points)); - } - - private void SendPointsUpdate(IPlayerSession session) - { - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points)); - } - - public int Level - { - get => _level; - set - { - _level = value; - SendLevelUpdate(); - } - } - private int _level = 0; - private void SendLevelUpdate() - { - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level)); - } - - private void SendLevelUpdate(IPlayerSession session) - { - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level)); - } - - private int ClearedLines - { - get => _clearedLines; - set - { - _clearedLines = value; - - if (_clearedLines < LevelRequirement) return; - - _clearedLines -= LevelRequirement; - Level++; - } - } - - private int _clearedLines = 0; - private int LevelRequirement => Math.Min(100, Math.Max(Level * 10 - 50, 10)); - - public BlockGame(BlockGameArcadeComponent component) - { - _component = component; - _allBlockGamePieces = (BlockGamePieceType[]) Enum.GetValues(typeof(BlockGamePieceType)); - _internalNextPiece = GetRandomBlockGamePiece(_component._random); - InitializeNewBlock(); - } - - private void SendHighscoreUpdate() - { - var entitySystem = EntitySystem.Get(); - _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() - { - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game)); - - FullUpdate(); - - _running = true; - _started = true; - } - - private void FullUpdate() - { - UpdateAllFieldUI(); - SendHoldPieceUpdate(); - SendNextPieceUpdate(); - SendPointsUpdate(); - SendHighscoreUpdate(); - SendLevelUpdate(); - } - - private void FullUpdate(IPlayerSession session) - { - UpdateFieldUI(session); - SendPointsUpdate(session); - SendNextPieceUpdate(session); - SendHoldPieceUpdate(session); - SendHighscoreUpdate(session); - SendLevelUpdate(session); - } - - public void GameTick(float frameTime) - { - if (!_running) return; - - InputTick(frameTime); - - FieldTick(frameTime); - } - - private float _accumulatedLeftPressTime; - private float _accumulatedRightPressTime; - private void InputTick(float frameTime) - { - bool anythingChanged = false; - if (_leftPressed) - { - _accumulatedLeftPressTime += frameTime; - - while (_accumulatedLeftPressTime >= _pressCheckSpeed) - { - - if (_currentPiece.Positions(_currentPiecePosition.AddToX(-1), _currentRotation) - .All(MoveCheck)) - { - _currentPiecePosition = _currentPiecePosition.AddToX(-1); - anythingChanged = true; - } - - _accumulatedLeftPressTime -= _pressCheckSpeed; - } - } - - if (_rightPressed) - { - _accumulatedRightPressTime += frameTime; - - while (_accumulatedRightPressTime >= _pressCheckSpeed) - { - if (_currentPiece.Positions(_currentPiecePosition.AddToX(1), _currentRotation) - .All(MoveCheck)) - { - _currentPiecePosition = _currentPiecePosition.AddToX(1); - anythingChanged = true; - } - - _accumulatedRightPressTime -= _pressCheckSpeed; - } - } - - if(anythingChanged) UpdateAllFieldUI(); - } - - private float _accumulatedFieldFrameTime; - private void FieldTick(float frameTime) - { - _accumulatedFieldFrameTime += frameTime; - - // Speed goes negative sometimes. uhhhh max() it I guess!!! - var checkTime = Math.Max(0.03f, Speed); - - while (_accumulatedFieldFrameTime >= checkTime) - { - if (_softDropPressed) AddPoints(1); - - InternalFieldTick(); - - _accumulatedFieldFrameTime -= checkTime; - } - } - - private void InternalFieldTick() - { - if (_currentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation) - .All(DropCheck)) - { - _currentPiecePosition = _currentPiecePosition.AddToY(1); - } - else - { - var blocks = _currentPiece.Blocks(_currentPiecePosition, _currentRotation); - _field.AddRange(blocks); - - //check loose conditions - if (IsGameOver) - { - InvokeGameover(); - return; - } - - InitializeNewBlock(); - } - - CheckField(); - - UpdateAllFieldUI(); - } - - private void CheckField() - { - int pointsToAdd = 0; - int consecutiveLines = 0; - int clearedLines = 0; - for (int y = 0; y < 20; y++) - { - if (CheckLine(y)) - { - //line was cleared - y--; - consecutiveLines++; - clearedLines++; - } - else if(consecutiveLines != 0) - { - var mod = consecutiveLines switch - { - 1 => 40, - 2 => 100, - 3 => 300, - 4 => 1200, - _ => 0 - }; - pointsToAdd += mod * (_level + 1); - } - } - - ClearedLines += clearedLines; - AddPoints(pointsToAdd); - } - - private bool CheckLine(int y) - { - for (var x = 0; x < 10; x++) - { - if (!_field.Any(b => b.Position.X == x && b.Position.Y == y)) return false; - } - - //clear line - _field.RemoveAll(b => b.Position.Y == y); - //move everything down - FillLine(y); - - return true; - } - - private void AddPoints(int amount) - { - if (amount == 0) return; - - Points += amount; - } - - private void FillLine(int y) - { - for (int c_y = y; c_y > 0; c_y--) - { - for (int j = 0; j < _field.Count; j++) - { - if(_field[j].Position.Y != c_y-1) continue; - - _field[j] = new BlockGameBlock(_field[j].Position.AddToY(1), _field[j].GameBlockColor); - } - } - } - - private void InitializeNewBlock() - { - InitializeNewBlock(_nextPiece); - _nextPiece = GetRandomBlockGamePiece(_component._random); - _holdBlock = false; - - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock)); - } - - private void InitializeNewBlock(BlockGamePiece piece) - { - _currentPiecePosition = new Vector2i(5,0); - - _currentRotation = BlockGamePieceRotation.North; - - _currentPiece = piece; - UpdateAllFieldUI(); - } - - private bool LowerBoundCheck(Vector2i position) => position.Y < 20; - private bool BorderCheck(Vector2i position) => position.X >= 0 && position.X < 10; - private bool ClearCheck(Vector2i position) => _field.All(block => !position.Equals(block.Position)); - - private bool DropCheck(Vector2i position) => LowerBoundCheck(position) && ClearCheck(position); - private bool MoveCheck(Vector2i position) => BorderCheck(position) && ClearCheck(position); - private bool RotateCheck(Vector2i position) => BorderCheck(position) && LowerBoundCheck(position) && ClearCheck(position); - - public void ProcessInput(BlockGamePlayerAction action) - { - if (_running) - { - switch (action) - { - case BlockGamePlayerAction.StartLeft: - _leftPressed = true; - break; - case BlockGamePlayerAction.StartRight: - _rightPressed = true; - break; - case BlockGamePlayerAction.Rotate: - TrySetRotation(Next(_currentRotation, false)); - break; - case BlockGamePlayerAction.CounterRotate: - TrySetRotation(Next(_currentRotation, true)); - break; - case BlockGamePlayerAction.SoftdropStart: - _softDropPressed = true; - if (_accumulatedFieldFrameTime > Speed) _accumulatedFieldFrameTime = Speed; //to prevent jumps - break; - case BlockGamePlayerAction.Harddrop: - PerformHarddrop(); - break; - case BlockGamePlayerAction.Hold: - HoldPiece(); - break; - } - } - - switch (action) - { - case BlockGamePlayerAction.EndLeft: - _leftPressed = false; - break; - case BlockGamePlayerAction.EndRight: - _rightPressed = false; - break; - case BlockGamePlayerAction.SoftdropEnd: - _softDropPressed = false; - break; - case BlockGamePlayerAction.Pause: - _running = false; - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, _started)); - break; - case BlockGamePlayerAction.Unpause: - if (!_gameOver && _started) - { - _running = true; - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game)); - } - break; - case BlockGamePlayerAction.ShowHighscores: - _running = false; - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Highscores, _started)); - break; - } - } - - private void TrySetRotation(BlockGamePieceRotation rotation) - { - if(!_running) return; - - if (!_currentPiece.CanSpin) return; - - if (!_currentPiece.Positions(_currentPiecePosition, rotation) - .All(RotateCheck)) - { - return; - } - - _currentRotation = rotation; - UpdateAllFieldUI(); - } - - private void HoldPiece() - { - if (!_running) return; - - if (_holdBlock) return; - - var tempHeld = _heldPiece; - _heldPiece = _currentPiece; - _holdBlock = true; - - if (!tempHeld.HasValue) - { - InitializeNewBlock(); - return; - } - - InitializeNewBlock(tempHeld.Value); - } - - private void PerformHarddrop() - { - int spacesDropped = 0; - while (_currentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation) - .All(DropCheck)) - { - _currentPiecePosition = _currentPiecePosition.AddToY(1); - spacesDropped++; - } - AddPoints(spacesDropped * 2); - - InternalFieldTick(); - } - - public void UpdateAllFieldUI() - { - if (!_started) return; - - var computedField = ComputeField(); - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField)); - } - - public void UpdateFieldUI(IPlayerSession session) - { - if (!_started) return; - - var computedField = ComputeField(); - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField), session); - } - - private bool IsGameOver => _field.Any(block => block.Position.Y == 0); - - private void InvokeGameover() - { - _running = false; - _gameOver = true; - - if (_component._player?.AttachedEntity is {Valid: true} playerEntity) - { - var blockGameSystem = EntitySystem.Get(); - - _highScorePlacement = blockGameSystem.RegisterHighScore(IoCManager.Resolve().GetComponent(playerEntity).EntityName, Points); - SendHighscoreUpdate(); - } - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement)); - } - - public void UpdateNewPlayerUI(IPlayerSession session) - { - if (_gameOver) - { - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement), session); - } - else - { - if (Paused) - { - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, Started), session); - } - else - { - _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game, Started), session); - } - } - - FullUpdate(session); - } - - public List ComputeField() - { - var result = new List(); - result.AddRange(_field); - result.AddRange(_currentPiece.Blocks(_currentPiecePosition, _currentRotation)); - - var dropGhostPosition = _currentPiecePosition; - while (_currentPiece.Positions(dropGhostPosition.AddToY(1), _currentRotation) - .All(DropCheck)) - { - dropGhostPosition = dropGhostPosition.AddToY(1); - } - - if (dropGhostPosition != _currentPiecePosition) - { - var blox = _currentPiece.Blocks(dropGhostPosition, _currentRotation); - for (var i = 0; i < blox.Length; i++) - { - result.Add(new BlockGameBlock(blox[i].Position, BlockGameBlock.ToGhostBlockColor(blox[i].GameBlockColor))); - } - } - return result; - } - - private enum BlockGamePieceType - { - I, - L, - LInverted, - S, - SInverted, - T, - O - } - - private enum BlockGamePieceRotation - { - North, - East, - South, - West - } - - private static BlockGamePieceRotation Next(BlockGamePieceRotation rotation, bool inverted) - { - return rotation switch - { - BlockGamePieceRotation.North => inverted ? BlockGamePieceRotation.West : BlockGamePieceRotation.East, - BlockGamePieceRotation.East => inverted ? BlockGamePieceRotation.North : BlockGamePieceRotation.South, - BlockGamePieceRotation.South => inverted ? BlockGamePieceRotation.East : BlockGamePieceRotation.West, - BlockGamePieceRotation.West => inverted ? BlockGamePieceRotation.South : BlockGamePieceRotation.North, - _ => throw new ArgumentOutOfRangeException(nameof(rotation), rotation, null) - }; - } - - private readonly BlockGamePieceType[] _allBlockGamePieces; - - private List _blockGamePiecesBuffer = new(); - - private BlockGamePiece GetRandomBlockGamePiece(IRobustRandom random) - { - if (_blockGamePiecesBuffer.Count == 0) - { - _blockGamePiecesBuffer = _allBlockGamePieces.ToList(); - } - - var chosenPiece = random.Pick(_blockGamePiecesBuffer); - _blockGamePiecesBuffer.Remove(chosenPiece); - return BlockGamePiece.GetPiece(chosenPiece); - } - - private struct BlockGamePiece - { - public Vector2i[] Offsets; - private BlockGameBlock.BlockGameBlockColor _gameBlockColor; - public bool CanSpin; - - public Vector2i[] Positions(Vector2i center, - BlockGamePieceRotation rotation) - { - return RotatedOffsets(rotation).Select(v => center + v).ToArray(); - } - - private Vector2i[] RotatedOffsets(BlockGamePieceRotation rotation) - { - Vector2i[] rotatedOffsets = (Vector2i[])Offsets.Clone(); - //until i find a better algo - var amount = rotation switch - { - BlockGamePieceRotation.North => 0, - BlockGamePieceRotation.East => 1, - BlockGamePieceRotation.South => 2, - BlockGamePieceRotation.West => 3, - _ => 0 - }; - - for (var i = 0; i < amount; i++) - { - for (var j = 0; j < rotatedOffsets.Length; j++) - { - rotatedOffsets[j] = rotatedOffsets[j].Rotate90DegreesAsOffset(); - } - } - - return rotatedOffsets; - } - - public BlockGameBlock[] Blocks(Vector2i center, - BlockGamePieceRotation rotation) - { - var positions = Positions(center, rotation); - var result = new BlockGameBlock[positions.Length]; - var i = 0; - foreach (var position in positions) - { - result[i++] = position.ToBlockGameBlock(_gameBlockColor); - } - - return result; - } - - public BlockGameBlock[] BlocksForPreview() - { - var xOffset = 0; - var yOffset = 0; - foreach (var offset in Offsets) - { - if (offset.X < xOffset) xOffset = offset.X; - if (offset.Y < yOffset) yOffset = offset.Y; - } - - return Blocks(new Vector2i(-xOffset, -yOffset), BlockGamePieceRotation.North); - } - - public static BlockGamePiece GetPiece(BlockGamePieceType type) - { - //switch statement, hardcoded offsets - return type switch - { - BlockGamePieceType.I => new BlockGamePiece - { - Offsets = new[] - { - new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(0, 1), new Vector2i(0, 2), - }, - _gameBlockColor = BlockGameBlock.BlockGameBlockColor.LightBlue, - CanSpin = true - }, - BlockGamePieceType.L => new BlockGamePiece - { - Offsets = new[] - { - new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(0, 1), new Vector2i(1, 1), - }, - _gameBlockColor = BlockGameBlock.BlockGameBlockColor.Orange, - CanSpin = true - }, - BlockGamePieceType.LInverted => new BlockGamePiece - { - Offsets = new[] - { - new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(-1, 1), - new Vector2i(0, 1), - }, - _gameBlockColor = BlockGameBlock.BlockGameBlockColor.Blue, - CanSpin = true - }, - BlockGamePieceType.S => new BlockGamePiece - { - Offsets = new[] - { - new Vector2i(0, -1), new Vector2i(1, -1), new Vector2i(-1, 0), - new Vector2i(0, 0), - }, - _gameBlockColor = BlockGameBlock.BlockGameBlockColor.Green, - CanSpin = true - }, - BlockGamePieceType.SInverted => new BlockGamePiece - { - Offsets = new[] - { - new Vector2i(-1, -1), new Vector2i(0, -1), new Vector2i(0, 0), - new Vector2i(1, 0), - }, - _gameBlockColor = BlockGameBlock.BlockGameBlockColor.Red, - CanSpin = true - }, - BlockGamePieceType.T => new BlockGamePiece - { - Offsets = new[] - { - new Vector2i(0, -1), - new Vector2i(-1, 0), new Vector2i(0, 0), new Vector2i(1, 0), - }, - _gameBlockColor = BlockGameBlock.BlockGameBlockColor.Purple, - CanSpin = true - }, - BlockGamePieceType.O => new BlockGamePiece - { - Offsets = new[] - { - new Vector2i(0, -1), new Vector2i(1, -1), new Vector2i(0, 0), - new Vector2i(1, 0), - }, - _gameBlockColor = BlockGameBlock.BlockGameBlockColor.Yellow, - CanSpin = false - }, - _ => new BlockGamePiece {Offsets = new[] {new Vector2i(0, 0)}} - }; - } - } - } - } -} diff --git a/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs b/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs deleted file mode 100644 index 262fe2343b..0000000000 --- a/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs +++ /dev/null @@ -1,389 +0,0 @@ -using Content.Server.Power.Components; -using Content.Server.UserInterface; -using Content.Shared.Arcade; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -// TODO: ECS. - -namespace Content.Server.Arcade.Components -{ - [RegisterComponent] - public sealed class SpaceVillainArcadeComponent : SharedSpaceVillainArcadeComponent - { - [Dependency] private readonly IRobustRandom _random = null!; - - [Dependency] private readonly IEntityManager _entityManager = default!; - private bool Powered => _entityManager.TryGetComponent(Owner, out var powerReceiverComponent) && powerReceiverComponent.Powered; - - [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SpaceVillainArcadeUiKey.Key); - [ViewVariables] public bool OverflowFlag; - [ViewVariables] public bool PlayerInvincibilityFlag; - [ViewVariables] public bool EnemyInvincibilityFlag; - [ViewVariables] public SpaceVillainGame Game = null!; - - [DataField("newGameSound")] private SoundSpecifier _newGameSound = new SoundPathSpecifier("/Audio/Effects/Arcade/newgame.ogg"); - [DataField("playerAttackSound")] private SoundSpecifier _playerAttackSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_attack.ogg"); - [DataField("playerHealSound")] private SoundSpecifier _playerHealSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_heal.ogg"); - [DataField("playerChargeSound")] private SoundSpecifier _playerChargeSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_charge.ogg"); - [DataField("winSound")] private SoundSpecifier _winSound = new SoundPathSpecifier("/Audio/Effects/Arcade/win.ogg"); - [DataField("gameOverSound")] private SoundSpecifier _gameOverSound = new SoundPathSpecifier("/Audio/Effects/Arcade/gameover.ogg"); - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("possibleFightVerbs")] - private List _possibleFightVerbs = new List() - {"Defeat", "Annihilate", "Save", "Strike", "Stop", "Destroy", "Robust", "Romance", "Pwn", "Own"}; - [ViewVariables(VVAccess.ReadWrite)] - [DataField("possibleFirstEnemyNames")] - private List _possibleFirstEnemyNames = new List(){ - "the Automatic", "Farmer", "Lord", "Professor", "the Cuban", "the Evil", "the Dread King", - "the Space", "Lord", "the Great", "Duke", "General" - }; - [ViewVariables(VVAccess.ReadWrite)] - [DataField("possibleLastEnemyNames")] - private List _possibleLastEnemyNames = new List() - { - "Melonoid", "Murdertron", "Sorcerer", "Ruin", "Jeff", "Ectoplasm", "Crushulon", "Uhangoid", - "Vhakoid", "Peteoid", "slime", "Griefer", "ERPer", "Lizard Man", "Unicorn" - }; - [ViewVariables(VVAccess.ReadWrite)] - [DataField("possibleRewards", customTypeSerializer:typeof(PrototypeIdListSerializer))] - private List _possibleRewards = new List() - { - "ToyMouse", "ToyAi", "ToyNuke", "ToyAssistant", "ToyGriffin", "ToyHonk", "ToyIan", - "ToyMarauder", "ToyMauler", "ToyGygax", "ToyOdysseus", "ToyOwlman", "ToyDeathRipley", - "ToyPhazon", "ToyFireRipley", "ToyReticence", "ToyRipley", "ToySeraph", "ToyDurand", "ToySkeleton", - "FoamCrossbow", "RevolverCapGun", "PlushieHampter", "PlushieLizard", "PlushieAtmosian", "PlushieSpaceLizard", - "PlushieNuke", "PlushieCarp", "PlushieRatvar", "PlushieNar", "PlushieSnake", "Basketball", "Football", - "PlushieRouny", "PlushieBee", "PlushieSlime", "BalloonCorgi", "ToySword", "CrayonBox", "BoxDonkSoftBox", "BoxCartridgeCap", - "HarmonicaInstrument", "OcarinaInstrument", "RecorderInstrument", "GunpetInstrument", "BirdToyInstrument", "PlushieXeno" - }; - - [DataField("rewardMinAmount")] - public int _rewardMinAmount; - - [DataField("rewardMaxAmount")] - public int _rewardMaxAmount; - - [ViewVariables(VVAccess.ReadWrite)] - public int _rewardAmount = 0; - - protected override void Initialize() - { - base.Initialize(); - - if (UserInterface != null) - { - UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage; - } - - // Random amount of prizes - _rewardAmount = new Random().Next(_rewardMinAmount, _rewardMaxAmount + 1); - - } - - public void OnPowerStateChanged(PowerChangedEvent e) - { - if (e.Powered) return; - - UserInterface?.CloseAll(); - } - - private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg) - { - if (!Powered) - return; - - if (serverMsg.Message is not SpaceVillainArcadePlayerActionMessage msg) return; - - switch (msg.PlayerAction) - { - case PlayerAction.Attack: - Game?.ExecutePlayerAction(msg.PlayerAction); - break; - case PlayerAction.Heal: - Game?.ExecutePlayerAction(msg.PlayerAction); - break; - case PlayerAction.Recharge: - Game?.ExecutePlayerAction(msg.PlayerAction); - break; - case PlayerAction.NewGame: - SoundSystem.Play(_newGameSound.GetSound(), Filter.Pvs(Owner), Owner, AudioParams.Default.WithVolume(-4f)); - - Game = new SpaceVillainGame(this); - UserInterface?.SendMessage(Game.GenerateMetaDataMessage()); - break; - case PlayerAction.RequestData: - UserInterface?.SendMessage(Game.GenerateMetaDataMessage()); - break; - } - } - - /// - /// Called when the user wins the game. - /// - public void ProcessWin() - { - if (_rewardAmount > 0) - { - _entityManager.SpawnEntity(_random.Pick(_possibleRewards), _entityManager.GetComponent(Owner).Coordinates); - _rewardAmount--; - } - } - - /// - /// Picks a fight-verb from the list of possible Verbs. - /// - /// A fight-verb. - public string GenerateFightVerb() - { - return _random.Pick(_possibleFightVerbs); - } - - /// - /// Generates an enemy-name comprised of a first- and last-name. - /// - /// An enemy-name. - public string GenerateEnemyName() - { - return $"{_random.Pick(_possibleFirstEnemyNames)} {_random.Pick(_possibleLastEnemyNames)}"; - } - - /// - /// A Class to handle all the game-logic of the SpaceVillain-game. - /// - public sealed class SpaceVillainGame - { - [Dependency] private readonly IRobustRandom _random = default!; - - [ViewVariables] private readonly SpaceVillainArcadeComponent _owner; - - [ViewVariables] public string Name => $"{_fightVerb} {_enemyName}"; - [ViewVariables(VVAccess.ReadWrite)] private int _playerHp = 30; - [ViewVariables(VVAccess.ReadWrite)] private int _playerHpMax = 30; - [ViewVariables(VVAccess.ReadWrite)] private int _playerMp = 10; - [ViewVariables(VVAccess.ReadWrite)] private int _playerMpMax = 10; - [ViewVariables(VVAccess.ReadWrite)] private int _enemyHp = 45; - [ViewVariables(VVAccess.ReadWrite)] private int _enemyHpMax = 45; - [ViewVariables(VVAccess.ReadWrite)] private int _enemyMp = 20; - [ViewVariables(VVAccess.ReadWrite)] private int _enemyMpMax = 20; - [ViewVariables(VVAccess.ReadWrite)] private int _turtleTracker; - - [ViewVariables(VVAccess.ReadWrite)] private readonly string _fightVerb; - [ViewVariables(VVAccess.ReadWrite)] private readonly string _enemyName; - - [ViewVariables] private bool _running = true; - - private string _latestPlayerActionMessage = ""; - private string _latestEnemyActionMessage = ""; - - public SpaceVillainGame(SpaceVillainArcadeComponent owner) : this(owner, owner.GenerateFightVerb(), owner.GenerateEnemyName()) { } - - public SpaceVillainGame(SpaceVillainArcadeComponent owner, string fightVerb, string enemyName) - { - IoCManager.InjectDependencies(this); - _owner = owner; - //todo defeat the curse secret game mode - _fightVerb = fightVerb; - _enemyName = enemyName; - } - - /// - /// Validates all vars incase they overshoot their max-values. - /// Does not check if vars surpass 0. - /// - private void ValidateVars() - { - if (_owner.OverflowFlag) return; - - if (_playerHp > _playerHpMax) _playerHp = _playerHpMax; - if (_playerMp > _playerMpMax) _playerMp = _playerMpMax; - if (_enemyHp > _enemyHpMax) _enemyHp = _enemyHpMax; - if (_enemyMp > _enemyMpMax) _enemyMp = _enemyMpMax; - } - - /// - /// Called by the SpaceVillainArcadeComponent when Userinput is received. - /// - /// The action the user picked. - public void ExecutePlayerAction(PlayerAction action) - { - if (!_running) return; - - switch (action) - { - case PlayerAction.Attack: - var attackAmount = _random.Next(2, 6); - _latestPlayerActionMessage = Loc.GetString("space-villain-game-player-attack-message", - ("enemyName", _enemyName), - ("attackAmount", attackAmount)); - SoundSystem.Play(_owner._playerAttackSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f)); - if (!_owner.EnemyInvincibilityFlag) - _enemyHp -= attackAmount; - _turtleTracker -= _turtleTracker > 0 ? 1 : 0; - break; - case PlayerAction.Heal: - var pointAmount = _random.Next(1, 3); - var healAmount = _random.Next(6, 8); - _latestPlayerActionMessage = Loc.GetString("space-villain-game-player-heal-message", - ("magicPointAmount", pointAmount), - ("healAmount", healAmount)); - SoundSystem.Play(_owner._playerHealSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f)); - if (!_owner.PlayerInvincibilityFlag) - _playerMp -= pointAmount; - _playerHp += healAmount; - _turtleTracker++; - break; - case PlayerAction.Recharge: - var chargeAmount = _random.Next(4, 7); - _latestPlayerActionMessage = Loc.GetString("space-villain-game-player-recharge-message", ("regainedPoints", chargeAmount)); - SoundSystem.Play(_owner._playerChargeSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f)); - _playerMp += chargeAmount; - _turtleTracker -= _turtleTracker > 0 ? 1 : 0; - break; - } - - if (!CheckGameConditions()) - { - return; - } - - ValidateVars(); - ExecuteAiAction(); - - if (!CheckGameConditions()) - { - return; - } - ValidateVars(); - UpdateUi(); - } - - /// - /// Checks the Game conditions and Updates the Ui & Plays a sound accordingly. - /// - /// A bool indicating if the game should continue. - private bool CheckGameConditions() - { - if ((_playerHp > 0 && _playerMp > 0) && (_enemyHp <= 0 || _enemyMp <= 0)) - { - _running = false; - UpdateUi(Loc.GetString("space-villain-game-player-wins-message"), - Loc.GetString("space-villain-game-enemy-dies-message", ("enemyName", _enemyName)), - true); - SoundSystem.Play(_owner._winSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f)); - _owner.ProcessWin(); - return false; - } - - if (_playerHp > 0 && _playerMp > 0) return true; - - if ((_enemyHp > 0 && _enemyMp > 0)) - { - _running = false; - UpdateUi(Loc.GetString("space-villain-game-player-loses-message"), - Loc.GetString("space-villain-game-enemy-cheers-message", ("enemyName", _enemyName)), - true); - SoundSystem.Play(_owner._gameOverSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f)); - return false; - } - if (_enemyHp <= 0 || _enemyMp <= 0) - { - _running = false; - UpdateUi(Loc.GetString("space-villain-game-player-loses-message"), - Loc.GetString("space-villain-game-enemy-dies-with-player-message ", ("enemyName", _enemyName)), - true); - SoundSystem.Play(_owner._gameOverSound.GetSound(), Filter.Pvs(_owner.Owner), _owner.Owner, AudioParams.Default.WithVolume(-4f)); - return false; - } - - return true; - } - - /// - /// Updates the UI. - /// - private void UpdateUi(bool metadata = false) - { - _owner.UserInterface?.SendMessage(metadata ? GenerateMetaDataMessage() : GenerateUpdateMessage()); - } - - private void UpdateUi(string message1, string message2, bool metadata = false) - { - _latestPlayerActionMessage = message1; - _latestEnemyActionMessage = message2; - UpdateUi(metadata); - } - - /// - /// Handles the logic of the AI - /// - /// An Enemyaction-message. - private void ExecuteAiAction() - { - if (_turtleTracker >= 4) - { - var boomAmount = _random.Next(5, 10); - _latestEnemyActionMessage = Loc.GetString("space-villain-game-enemy-throws-bomb-message", - ("enemyName", _enemyName), - ("damageReceived", boomAmount)); - if (_owner.PlayerInvincibilityFlag) return; - _playerHp -= boomAmount; - _turtleTracker--; - } - else if (_enemyMp <= 5 && _random.Prob(0.7f)) - { - var stealAmount = _random.Next(2, 3); - _latestEnemyActionMessage = Loc.GetString("space-villain-game-enemy-steals-player-power-message", - ("enemyName", _enemyName), - ("stolenAmount", stealAmount)); - if (_owner.PlayerInvincibilityFlag) return; - _playerMp -= stealAmount; - _enemyMp += stealAmount; - } - else if (_enemyHp <= 10 && _enemyMp > 4) - { - _enemyHp += 4; - _enemyMp -= 4; - _latestEnemyActionMessage = Loc.GetString("space-villain-game-enemy-heals-message", - ("enemyName", _enemyName), - ("healedAmount", 4)); - } - else - { - var attackAmount = _random.Next(3, 6); - _latestEnemyActionMessage = - Loc.GetString("space-villain-game-enemy-attacks-message", - ("enemyName", _enemyName), - ("damageDealt", attackAmount)); - if (_owner.PlayerInvincibilityFlag) return; - _playerHp -= attackAmount; - } - } - - /// - /// Generates a Metadata-message based on the objects values. - /// - /// A Metadata-message. - public SpaceVillainArcadeMetaDataUpdateMessage GenerateMetaDataMessage() - { - return new(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage, _latestEnemyActionMessage, Name, _enemyName, !_running); - } - - /// - /// Creates an Update-message based on the objects values. - /// - /// An Update-Message. - public SpaceVillainArcadeDataUpdateMessage - GenerateUpdateMessage() - { - return new(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage, - _latestEnemyActionMessage); - } - } - } -} diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeComponent.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeComponent.cs new file mode 100644 index 0000000000..c11851af57 --- /dev/null +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeComponent.cs @@ -0,0 +1,124 @@ +using Content.Shared.Arcade; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +// TODO: ECS. + +namespace Content.Server.Arcade.SpaceVillain; + +[RegisterComponent] +public sealed class SpaceVillainArcadeComponent : SharedSpaceVillainArcadeComponent +{ + /// + /// Unused flag that can be hacked via wires. + /// Name suggests that it was intended to either make the health/mana values underflow while playing the game or turn the arcade machine into an infinite prize fountain. + /// + [ViewVariables] + public bool OverflowFlag; + + /// + /// The current session of the SpaceVillain game for this arcade machine. + /// + [ViewVariables] + public SpaceVillainGame? Game = null; + + /// + /// The sound played when a new session of the SpaceVillain game is begun. + /// + [DataField("newGameSound")] + public SoundSpecifier NewGameSound = new SoundPathSpecifier("/Audio/Effects/Arcade/newgame.ogg"); + + /// + /// The sound played when the player chooses to attack. + /// + [DataField("playerAttackSound")] + public SoundSpecifier PlayerAttackSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_attack.ogg"); + + /// + /// The sound played when the player chooses to heal. + /// + [DataField("playerHealSound")] + public SoundSpecifier PlayerHealSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_heal.ogg"); + + /// + /// The sound played when the player chooses to regain mana. + /// + [DataField("playerChargeSound")] + public SoundSpecifier PlayerChargeSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_charge.ogg"); + + /// + /// The sound played when the player wins. + /// + [DataField("winSound")] + public SoundSpecifier WinSound = new SoundPathSpecifier("/Audio/Effects/Arcade/win.ogg"); + + /// + /// The sound played when the player loses. + /// + [DataField("gameOverSound")] + public SoundSpecifier GameOverSound = new SoundPathSpecifier("/Audio/Effects/Arcade/gameover.ogg"); + + /// + /// The prefixes that can be used to create the game name. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("possibleFightVerbs")] + public List PossibleFightVerbs = new() + {"Defeat", "Annihilate", "Save", "Strike", "Stop", "Destroy", "Robust", "Romance", "Pwn", "Own"}; + + /// + /// The first names/titles that can be used to construct the name of the villain. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("possibleFirstEnemyNames")] + public List PossibleFirstEnemyNames = new(){ + "the Automatic", "Farmer", "Lord", "Professor", "the Cuban", "the Evil", "the Dread King", + "the Space", "Lord", "the Great", "Duke", "General" + }; + + /// + /// The last names that can be used to construct the name of the villain. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("possibleLastEnemyNames")] + public List PossibleLastEnemyNames = new() + { + "Melonoid", "Murdertron", "Sorcerer", "Ruin", "Jeff", "Ectoplasm", "Crushulon", "Uhangoid", + "Vhakoid", "Peteoid", "slime", "Griefer", "ERPer", "Lizard Man", "Unicorn" + }; + + /// + /// The prototypes that can be dispensed as a reward for winning the game. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("possibleRewards", customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List PossibleRewards = new() + { + "ToyMouse", "ToyAi", "ToyNuke", "ToyAssistant", "ToyGriffin", "ToyHonk", "ToyIan", + "ToyMarauder", "ToyMauler", "ToyGygax", "ToyOdysseus", "ToyOwlman", "ToyDeathRipley", + "ToyPhazon", "ToyFireRipley", "ToyReticence", "ToyRipley", "ToySeraph", "ToyDurand", "ToySkeleton", + "FoamCrossbow", "RevolverCapGun", "PlushieHampter", "PlushieLizard", "PlushieAtmosian", "PlushieSpaceLizard", + "PlushieNuke", "PlushieCarp", "PlushieRatvar", "PlushieNar", "PlushieSnake", "Basketball", "Football", + "PlushieRouny", "PlushieBee", "PlushieSlime", "BalloonCorgi", "ToySword", "CrayonBox", "BoxDonkSoftBox", "BoxCartridgeCap", + "HarmonicaInstrument", "OcarinaInstrument", "RecorderInstrument", "GunpetInstrument", "BirdToyInstrument", "PlushieXeno" + }; + + /// + /// The minimum number of prizes the arcade machine can have. + /// + [DataField("rewardMinAmount")] + public int RewardMinAmount; + + /// + /// The maximum number of prizes the arcade machine can have. + /// + [DataField("rewardMaxAmount")] + public int RewardMaxAmount; + + /// + /// The remaining number of prizes the arcade machine can dispense. + /// + [ViewVariables(VVAccess.ReadWrite)] + public int RewardAmount = 0; +} diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs new file mode 100644 index 0000000000..5e4d7d5ec6 --- /dev/null +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs @@ -0,0 +1,109 @@ +using Content.Server.Power.Components; +using Content.Server.UserInterface; +using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Random; + +namespace Content.Server.Arcade.SpaceVillain; + +public sealed partial class SpaceVillainArcadeSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnAfterUIOpenSV); + SubscribeLocalEvent(OnSVPlayerAction); + SubscribeLocalEvent(OnSVillainPower); + } + + /// + /// Called when the user wins the game. + /// Dispenses a prize if the arcade machine has any left. + /// + /// + /// + /// + public void ProcessWin(EntityUid uid, SpaceVillainArcadeComponent? arcade = null, TransformComponent? xform = null) + { + if (!Resolve(uid, ref arcade, ref xform)) + return; + if (arcade.RewardAmount <= 0) + return; + + EntityManager.SpawnEntity(_random.Pick(arcade.PossibleRewards), xform.Coordinates); + arcade.RewardAmount--; + } + + /// + /// Picks a fight-verb from the list of possible Verbs. + /// + /// A fight-verb. + public string GenerateFightVerb(SpaceVillainArcadeComponent arcade) + { + return _random.Pick(arcade.PossibleFightVerbs); + } + + /// + /// Generates an enemy-name comprised of a first- and last-name. + /// + /// An enemy-name. + public string GenerateEnemyName(SpaceVillainArcadeComponent arcade) + { + return $"{_random.Pick(arcade.PossibleFirstEnemyNames)} {_random.Pick(arcade.PossibleLastEnemyNames)}"; + } + + private void OnComponentInit(EntityUid uid, SpaceVillainArcadeComponent component, ComponentInit args) + { + // Random amount of prizes + component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1); + } + + private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SpaceVillainArcadePlayerActionMessage msg) + { + if (component.Game == null) + return; + if (!TryComp(uid, out var power) || !power.Powered) + return; + + switch (msg.PlayerAction) + { + case PlayerAction.Attack: + case PlayerAction.Heal: + case PlayerAction.Recharge: + component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component); + break; + case PlayerAction.NewGame: + _audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f)); + + component.Game = new SpaceVillainGame(uid, component, this); + if (_uiSystem.TryGetUi(uid, SpaceVillainArcadeUiKey.Key, out var bui)) + _uiSystem.SendUiMessage(bui, component.Game.GenerateMetaDataMessage()); + break; + case PlayerAction.RequestData: + if (_uiSystem.TryGetUi(uid, SpaceVillainArcadeUiKey.Key, out bui)) + _uiSystem.SendUiMessage(bui, component.Game.GenerateMetaDataMessage()); + break; + } + } + + private void OnAfterUIOpenSV(EntityUid uid, SpaceVillainArcadeComponent component, AfterActivatableUIOpenEvent args) + { + component.Game ??= new(uid, component, this); + } + + private void OnSVillainPower(EntityUid uid, SpaceVillainArcadeComponent component, ref PowerChangedEvent args) + { + if (TryComp(uid, out var power) && power.Powered) + return; + + if (_uiSystem.TryGetUi(uid, SpaceVillainArcadeUiKey.Key, out var bui)) + _uiSystem.CloseAll(bui); + } +} diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.Fighter.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.Fighter.cs new file mode 100644 index 0000000000..50d4c4efe5 --- /dev/null +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.Fighter.cs @@ -0,0 +1,68 @@ +namespace Content.Server.Arcade.SpaceVillain; + +public sealed partial class SpaceVillainGame +{ + /// + /// A state holder for the fighters in the SpaceVillain game. + /// + public sealed class Fighter + { + /// + /// The current hit point total of the fighter. + /// + [ViewVariables(VVAccess.ReadWrite)] + public int Hp + { + get => _hp; + set => _hp = MathHelper.Clamp(value, 0, HpMax); + } + private int _hp; + + /// + /// The maximum hit point total of the fighter. + /// + [ViewVariables(VVAccess.ReadWrite)] + public int HpMax + { + get => _hpMax; + set + { + _hpMax = Math.Max(value, 0); + Hp = MathHelper.Clamp(Hp, 0, HpMax); + } + } + private int _hpMax; + + /// + /// The current mana total of the fighter. + /// + [ViewVariables(VVAccess.ReadWrite)] + public int Mp + { + get => _mp; + set => _mp = MathHelper.Clamp(value, 0, MpMax); + } + private int _mp; + + /// + /// The maximum mana total of the fighter. + /// + [ViewVariables(VVAccess.ReadWrite)] + public int MpMax + { + get => _mpMax; + set + { + _mpMax = Math.Max(value, 0); + Mp = MathHelper.Clamp(Mp, 0, MpMax); + } + } + private int _mpMax; + + /// + /// Whether the given fighter can take damage/lose mana. + /// + [ViewVariables(VVAccess.ReadWrite)] + public bool Invincible = false; + } +} diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.Ui.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.Ui.cs new file mode 100644 index 0000000000..890e9888a7 --- /dev/null +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.Ui.cs @@ -0,0 +1,53 @@ +using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent; + +namespace Content.Server.Arcade.SpaceVillain; + +public sealed partial class SpaceVillainGame +{ + /// + /// Updates the UI. + /// + private void UpdateUi(EntityUid uid, bool metadata = false) + { + if (_uiSystem.TryGetUi(uid, SpaceVillainArcadeUiKey.Key, out var bui)) + _uiSystem.SendUiMessage(bui, metadata ? GenerateMetaDataMessage() : GenerateUpdateMessage()); + } + + private void UpdateUi(EntityUid uid, string message1, string message2, bool metadata = false) + { + _latestPlayerActionMessage = message1; + _latestEnemyActionMessage = message2; + UpdateUi(uid, metadata); + } + + /// + /// Generates a Metadata-message based on the objects values. + /// + /// A Metadata-message. + public SpaceVillainArcadeMetaDataUpdateMessage GenerateMetaDataMessage() + { + return new( + PlayerChar.Hp, PlayerChar.Mp, + VillainChar.Hp, VillainChar.Mp, + _latestPlayerActionMessage, + _latestEnemyActionMessage, + Name, + _villainName, + !_running + ); + } + + /// + /// Creates an Update-message based on the objects values. + /// + /// An Update-Message. + public SpaceVillainArcadeDataUpdateMessage GenerateUpdateMessage() + { + return new( + PlayerChar.Hp, PlayerChar.Mp, + VillainChar.Hp, VillainChar.Mp, + _latestPlayerActionMessage, + _latestEnemyActionMessage + ); + } +} diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs new file mode 100644 index 0000000000..18dc32282b --- /dev/null +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs @@ -0,0 +1,252 @@ +using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Random; + +namespace Content.Server.Arcade.SpaceVillain; + + +/// +/// A Class to handle all the game-logic of the SpaceVillain-game. +/// +public sealed partial class SpaceVillainGame +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + private readonly SharedAudioSystem _audioSystem = default!; + private readonly UserInterfaceSystem _uiSystem = default!; + private readonly SpaceVillainArcadeSystem _svArcade = default!; + + + [ViewVariables] + private readonly EntityUid _owner = default!; + + [ViewVariables] + private bool _running = true; + + [ViewVariables] + public string Name => $"{_fightVerb} {_villainName}"; + + [ViewVariables] + private readonly string _fightVerb; + + [ViewVariables] + public readonly Fighter PlayerChar; + + [ViewVariables] + private readonly string _villainName; + + [ViewVariables] + public readonly Fighter VillainChar; + + [ViewVariables] + private int _turtleTracker = 0; + + [ViewVariables] + private string _latestPlayerActionMessage = ""; + + [ViewVariables] + private string _latestEnemyActionMessage = ""; + + public SpaceVillainGame(EntityUid owner, SpaceVillainArcadeComponent arcade, SpaceVillainArcadeSystem arcadeSystem) + : this(owner, arcade, arcadeSystem, arcadeSystem.GenerateFightVerb(arcade), arcadeSystem.GenerateEnemyName(arcade)) + { + } + + public SpaceVillainGame(EntityUid owner, SpaceVillainArcadeComponent arcade, SpaceVillainArcadeSystem arcadeSystem, string fightVerb, string enemyName) + { + IoCManager.InjectDependencies(this); + _audioSystem = _entityManager.System(); + _uiSystem = _entityManager.System(); + _svArcade = _entityManager.System(); + + _owner = owner; + //todo defeat the curse secret game mode + _fightVerb = fightVerb; + _villainName = enemyName; + + PlayerChar = new() + { + HpMax = 30, + Hp = 30, + MpMax = 10, + Mp = 10 + }; + + VillainChar = new() + { + HpMax = 45, + Hp = 45, + MpMax = 20, + Mp = 20 + }; + } + + /// + /// Called by the SpaceVillainArcadeComponent when Userinput is received. + /// + /// The action the user picked. + /// The action the user picked. + /// The action the user picked. + public void ExecutePlayerAction(EntityUid uid, PlayerAction action, SpaceVillainArcadeComponent arcade) + { + if (!_running) + return; + + switch (action) + { + case PlayerAction.Attack: + var attackAmount = _random.Next(2, 6); + _latestPlayerActionMessage = Loc.GetString( + "space-villain-game-player-attack-message", + ("enemyName", _villainName), + ("attackAmount", attackAmount) + ); + _audioSystem.PlayPvs(arcade.PlayerAttackSound, uid, AudioParams.Default.WithVolume(-4f)); + if (!VillainChar.Invincible) + VillainChar.Hp -= attackAmount; + _turtleTracker -= _turtleTracker > 0 ? 1 : 0; + break; + case PlayerAction.Heal: + var pointAmount = _random.Next(1, 3); + var healAmount = _random.Next(6, 8); + _latestPlayerActionMessage = Loc.GetString( + "space-villain-game-player-heal-message", + ("magicPointAmount", pointAmount), + ("healAmount", healAmount) + ); + _audioSystem.PlayPvs(arcade.PlayerHealSound, uid, AudioParams.Default.WithVolume(-4f)); + if (!PlayerChar.Invincible) + PlayerChar.Mp -= pointAmount; + PlayerChar.Hp += healAmount; + _turtleTracker++; + break; + case PlayerAction.Recharge: + var chargeAmount = _random.Next(4, 7); + _latestPlayerActionMessage = Loc.GetString( + "space-villain-game-player-recharge-message", + ("regainedPoints", chargeAmount) + ); + _audioSystem.PlayPvs(arcade.PlayerChargeSound, uid, AudioParams.Default.WithVolume(-4f)); + PlayerChar.Mp += chargeAmount; + _turtleTracker -= _turtleTracker > 0 ? 1 : 0; + break; + } + + if (!CheckGameConditions(uid, arcade)) + return; + + ExecuteAiAction(); + + if (!CheckGameConditions(uid, arcade)) + return; + + UpdateUi(uid); + } + + /// + /// Handles the logic of the AI + /// + private void ExecuteAiAction() + { + if (_turtleTracker >= 4) + { + var boomAmount = _random.Next(5, 10); + _latestEnemyActionMessage = Loc.GetString( + "space-villain-game-enemy-throws-bomb-message", + ("enemyName", _villainName), + ("damageReceived", boomAmount) + ); + if (PlayerChar.Invincible) + return; + PlayerChar.Hp -= boomAmount; + _turtleTracker--; + return; + } + + if (VillainChar.Mp <= 5 && _random.Prob(0.7f)) + { + var stealAmount = _random.Next(2, 3); + _latestEnemyActionMessage = Loc.GetString( + "space-villain-game-enemy-steals-player-power-message", + ("enemyName", _villainName), + ("stolenAmount", stealAmount) + ); + if (PlayerChar.Invincible) + return; + PlayerChar.Mp -= stealAmount; + VillainChar.Mp += stealAmount; + return; + } + + if (VillainChar.Hp <= 10 && VillainChar.Mp > 4) + { + VillainChar.Hp += 4; + VillainChar.Mp -= 4; + _latestEnemyActionMessage = Loc.GetString( + "space-villain-game-enemy-heals-message", + ("enemyName", _villainName), + ("healedAmount", 4) + ); + return; + } + + var attackAmount = _random.Next(3, 6); + _latestEnemyActionMessage = + Loc.GetString( + "space-villain-game-enemy-attacks-message", + ("enemyName", _villainName), + ("damageDealt", attackAmount) + ); + if (PlayerChar.Invincible) + return; + PlayerChar.Hp -= attackAmount; + } + + /// + /// Checks the Game conditions and Updates the Ui & Plays a sound accordingly. + /// + /// A bool indicating if the game should continue. + private bool CheckGameConditions(EntityUid uid, SpaceVillainArcadeComponent arcade) + { + switch ( + PlayerChar.Hp > 0 && PlayerChar.Mp > 0, + VillainChar.Hp > 0 && VillainChar.Mp > 0 + ) + { + case (true, true): + return true; + case (true, false): + _running = false; + UpdateUi( + uid, + Loc.GetString("space-villain-game-player-wins-message"), + Loc.GetString("space-villain-game-enemy-dies-message", ("enemyName", _villainName)), + true + ); + _audioSystem.PlayPvs(arcade.WinSound, uid, AudioParams.Default.WithVolume(-4f)); + _svArcade.ProcessWin(uid, arcade); + return false; + case (false, true): + _running = false; + UpdateUi( + uid, + Loc.GetString("space-villain-game-player-loses-message"), + Loc.GetString("space-villain-game-enemy-cheers-message", ("enemyName", _villainName)), + true + ); + _audioSystem.PlayPvs(arcade.GameOverSound, uid, AudioParams.Default.WithVolume(-4f)); + return false; + case (false, false): + _running = false; + UpdateUi( + uid, + Loc.GetString("space-villain-game-player-loses-message"), + Loc.GetString("space-villain-game-enemy-dies-with-player-message ", ("enemyName", _villainName)), + true + ); + _audioSystem.PlayPvs(arcade.GameOverSound, uid, AudioParams.Default.WithVolume(-4f)); + return false; + } + } +} diff --git a/Content.Server/Arcade/WireActions/ArcadeInvincibilityWireActions.cs b/Content.Server/Arcade/WireActions/ArcadeInvincibilityWireActions.cs index 72ae200620..84d7a15480 100644 --- a/Content.Server/Arcade/WireActions/ArcadeInvincibilityWireActions.cs +++ b/Content.Server/Arcade/WireActions/ArcadeInvincibilityWireActions.cs @@ -1,4 +1,4 @@ -using Content.Server.Arcade.Components; +using Content.Server.Arcade.SpaceVillain; using Content.Server.Wires; using Content.Shared.Arcade; using Content.Shared.Wires; @@ -15,23 +15,26 @@ public sealed class ArcadePlayerInvincibleWireAction : BaseToggleWireAction public override void ToggleValue(EntityUid owner, bool setting) { - if (EntityManager.TryGetComponent(owner, out var arcade)) + if (EntityManager.TryGetComponent(owner, out var arcade) + && arcade.Game != null) { - arcade.PlayerInvincibilityFlag = !setting; + arcade.Game.PlayerChar.Invincible = !setting; } } public override bool GetValue(EntityUid owner) { return EntityManager.TryGetComponent(owner, out var arcade) - && !arcade.PlayerInvincibilityFlag; + && arcade.Game != null + && !arcade.Game.PlayerChar.Invincible; } public override StatusLightState? GetLightState(Wire wire) { - if (EntityManager.TryGetComponent(wire.Owner, out var arcade)) + if (EntityManager.TryGetComponent(wire.Owner, out var arcade) + && arcade.Game != null) { - return arcade.PlayerInvincibilityFlag || arcade.EnemyInvincibilityFlag + return arcade.Game.PlayerChar.Invincible || arcade.Game.VillainChar.Invincible ? StatusLightState.BlinkingSlow : StatusLightState.On; } @@ -49,19 +52,24 @@ public sealed class ArcadeEnemyInvincibleWireAction : BaseToggleWireAction public override void ToggleValue(EntityUid owner, bool setting) { - if (EntityManager.TryGetComponent(owner, out var arcade)) + if (EntityManager.TryGetComponent(owner, out var arcade) + && arcade.Game != null) { - arcade.PlayerInvincibilityFlag = !setting; + arcade.Game.VillainChar.Invincible = !setting; } } public override bool GetValue(EntityUid owner) { return EntityManager.TryGetComponent(owner, out var arcade) - && !arcade.PlayerInvincibilityFlag; + && arcade.Game != null + && !arcade.Game.VillainChar.Invincible; } - public override StatusLightData? GetStatusLightData(Wire wire) => null; + public override StatusLightData? GetStatusLightData(Wire wire) + { + return null; + } } public enum ArcadeInvincibilityWireActionKeys : short diff --git a/Content.Server/Arcade/WireActions/ArcadeOverflowWireAction.cs b/Content.Server/Arcade/WireActions/ArcadeOverflowWireAction.cs index 2b672824cf..1d15e03e64 100644 --- a/Content.Server/Arcade/WireActions/ArcadeOverflowWireAction.cs +++ b/Content.Server/Arcade/WireActions/ArcadeOverflowWireAction.cs @@ -1,4 +1,4 @@ -using Content.Server.Arcade.Components; +using Content.Server.Arcade.SpaceVillain; using Content.Server.Wires; using Content.Shared.Arcade; using Content.Shared.Wires; @@ -9,7 +9,7 @@ public sealed class ArcadeOverflowWireAction : BaseToggleWireAction { public override Color Color { get; set; } = Color.Red; public override string Name { get; set; } = "wire-name-arcade-overflow"; - + public override object? StatusKey { get; } = SharedSpaceVillainArcadeComponent.Indicators.HealthLimiter; public override void ToggleValue(EntityUid owner, bool setting) diff --git a/Content.Shared/Arcade/BlockGameBlock.cs b/Content.Shared/Arcade/BlockGameBlock.cs index b47f5224ad..012950cda7 100644 --- a/Content.Shared/Arcade/BlockGameBlock.cs +++ b/Content.Shared/Arcade/BlockGameBlock.cs @@ -71,7 +71,8 @@ namespace Content.Shared.Arcade } } - public static class BlockGameVector2Extensions{ + public static class BlockGameVector2Extensions + { public static BlockGameBlock ToBlockGameBlock(this Vector2i vector2, BlockGameBlock.BlockGameBlockColor gameBlockColor) { return new(vector2, gameBlockColor); @@ -90,6 +91,5 @@ namespace Content.Shared.Arcade { return new(-vector.Y, vector.X); } - } }