From fad1ed64962e02af23b0c0448068b47ebb23e271 Mon Sep 17 00:00:00 2001 From: dr20ervin Date: Thu, 21 May 2026 00:40:54 +0300 Subject: [PATCH] Add multiplayer lobby and gameplay enhancements --- include/game.h | 1 + src/main.cpp | 422 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 394 insertions(+), 29 deletions(-) diff --git a/include/game.h b/include/game.h index 68742a9..b06055c 100644 --- a/include/game.h +++ b/include/game.h @@ -8,6 +8,7 @@ enum class GameState { MainMenu, DifficultySelect, + MultiplayerLobby, Multiplayer, Settings, Playing, diff --git a/src/main.cpp b/src/main.cpp index 7d23283..b726242 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -93,6 +93,9 @@ int main() { float sessionPlayTime = 0.0f; bool isPaused = false; bool shouldQuit = false; + bool isMultiplayer = false; + bool p1Ready = false; + bool p2Ready = false; int resolutionOption = 0; // 0 = 1280x800, 1 = 1600x900, 2 = 1920x1080 int framerateOption = 0; // 0 = 60 FPS, 1 = 144 FPS, 2 = VSync @@ -130,10 +133,20 @@ int main() { { int selected = mainMenu.Update(); if (selected == 0) { + isMultiplayer = false; currentState = GameState::DifficultySelect; } else if (selected == 1) { - currentState = GameState::Multiplayer; + isMultiplayer = true; + score.player_score = 0; + score.player2_score = 0; + score.cpu_score = 0; + sessionPlayTime = 0.0f; + isPaused = false; + p1Ready = false; + p2Ready = false; + ResetBall(ball); + currentState = GameState::MultiplayerLobby; } else if (selected == 2) { currentState = GameState::Settings; @@ -142,6 +155,12 @@ int main() { shouldQuit = true; } mainMenu.Draw(); + + // Controls helper + int menuHintWidth = MeasureText("Use UP/DOWN to navigate | ENTER to select", 20); + DrawText("Use UP/DOWN to navigate | ENTER to select", + screen_width / 2 - menuHintWidth / 2, + screen_height - 80, 20, WHITE); break; } @@ -161,6 +180,7 @@ int main() { // Reset game session stats score.player_score = 0; + score.player2_score = 0; score.cpu_score = 0; sessionPlayTime = 0.0f; isPaused = false; @@ -171,6 +191,332 @@ int main() { currentState = GameState::MainMenu; } difficultyMenu.Draw(); + + // Controls helper + int gameHintWidth = MeasureText("Game: UP/DOWN Arrows to move | P to Pause | SPACE to Force Game Over", 20); + DrawText("Player: UP/DOWN Arrows to move | P to Pause | SPACE to Force Game Over", + screen_width / 2 - gameHintWidth / 2, + screen_height - 50, 20, WHITE); + + break; + } + + case GameState::MultiplayerLobby: + { + // Input checks + if (IsKeyPressed(KEY_W)) { + p1Ready = !p1Ready; + if (sfxEnabled) PlaySound(paddleHitSound); + } + if (IsKeyPressed(KEY_UP)) { + p2Ready = !p2Ready; + if (sfxEnabled) PlaySound(paddleHitSound); + } + if (IsKeyPressed(KEY_B)) { + currentState = GameState::MainMenu; + } + + if (p1Ready && p2Ready) { + currentState = GameState::Multiplayer; + } + + // --- Draw Court Background Behind Panel --- + // Left Court (basic_space.png) + DrawTexturePro( + courtBackground, + Rectangle{ 0.0f, 0.0f, (float)courtBackground.width, (float)courtBackground.height }, + Rectangle{ 20.0f, 20.0f, (float)screen_width / 2.0f - 70.0f, (float)screen_height - 40.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Right Court (basic_space.png) + DrawTexturePro( + courtBackground, + Rectangle{ 0.0f, 0.0f, (float)courtBackground.width, (float)courtBackground.height }, + Rectangle{ (float)screen_width / 2.0f + 50.0f, 20.0f, (float)screen_width / 2.0f - 70.0f, (float)screen_height - 40.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Center Circle + DrawCircle(screen_width / 2, screen_height / 2, 50.0f, Color{ 102, 51, 153, 100 }); + DrawCircleLines(screen_width / 2, screen_height / 2, 50.0f, Color{ 50, 25, 75, 250 }); + + // Tiled Center Dashed Line (line.png) + int lineY = 20; + while (lineY < screen_height - 20) { + DrawTexture(lineTexture, screen_width / 2 - lineTexture.width / 2, lineY, WHITE); + lineY += lineTexture.height; + } + + // Top Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ 0.0f, 0.0f, (float)screen_width, 20.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Bottom Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ 0.0f, (float)screen_height - 20.0f, (float)screen_width, 20.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Left Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ 0.0f, 0.0f, 20.0f, (float)screen_height }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Right Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ (float)screen_width - 20.0f, 0.0f, 20.0f, (float)screen_height }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Draw Lobby Panel + int panelWidth = 800; + int panelHeight = 450; + int panelX = screen_width / 2 - panelWidth / 2; + int panelY = screen_height / 2 - panelHeight / 2; + + // Semi-transparent background panel + DrawRectangle(panelX, panelY, panelWidth, panelHeight, Color{ 15, 15, 15, 230 }); + DrawRectangleLines(panelX, panelY, panelWidth, panelHeight, Color{ 120, 120, 120, 255 }); + + // Title + int titleWidth = MeasureText("MULTIPLAYER LOBBY", 40); + DrawText("MULTIPLAYER LOBBY", screen_width / 2 - titleWidth / 2, panelY + 30, 40, WHITE); + + // Player 1 Details (Left Paddle controlled by W/S keys) + int p1X = panelX + 50; + int p1Y = panelY + 110; + DrawText("PLAYER 1 (LEFT)", p1X, p1Y, 24, LIGHTGRAY); + DrawText("Controls: W / S", p1X, p1Y + 45, 20, WHITE); + DrawText("Press 'W' to toggle ready", p1X, p1Y + 75, 18, GRAY); + + if (p1Ready) { + DrawRectangle(p1X, p1Y + 115, 220, 50, GREEN); + DrawText("READY", p1X + 110 - MeasureText("READY", 20)/2, p1Y + 130, 20, BLACK); + } else { + DrawRectangle(p1X, p1Y + 115, 220, 50, RED); + DrawText("NOT READY", p1X + 110 - MeasureText("NOT READY", 20)/2, p1Y + 130, 20, WHITE); + } + + // Player 2 Details (Right Paddle controlled by UP/DOWN arrow keys) + int p2X = panelX + panelWidth - 270; + int p2Y = panelY + 110; + DrawText("PLAYER 2 (RIGHT)", p2X, p2Y, 24, LIGHTGRAY); + DrawText("Controls: UP / DOWN", p2X, p2Y + 45, 20, WHITE); + DrawText("Press 'UP' to toggle ready", p2X, p2Y + 75, 18, GRAY); + + if (p2Ready) { + DrawRectangle(p2X, p2Y + 115, 220, 50, GREEN); + DrawText("READY", p2X + 110 - MeasureText("READY", 20)/2, p2Y + 130, 20, BLACK); + } else { + DrawRectangle(p2X, p2Y + 115, 220, 50, RED); + DrawText("NOT READY", p2X + 110 - MeasureText("NOT READY", 20)/2, p2Y + 130, 20, WHITE); + } + + // Lobby Status Message + if (p1Ready && p2Ready) { + int startTextWidth = MeasureText("BOTH READY! STARTING GAME...", 24); + DrawText("BOTH READY! STARTING GAME...", screen_width / 2 - startTextWidth / 2, panelY + 310, 24, YELLOW); + } else { + int waitTextWidth = MeasureText("Waiting for both players to be ready...", 20); + DrawText("Waiting for both players to be ready...", screen_width / 2 - waitTextWidth / 2, panelY + 315, 20, LIGHTGRAY); + } + + // Back Instruction + int backTextWidth = MeasureText("Press 'B' to return to Main Menu | P: Pause | SPACE: Force Game Over", 20); + DrawText("Press 'B' to return to Main Menu | P: Pause | SPACE: Force Game Over", screen_width / 2 - backTextWidth / 2, panelY + 385, 20, YELLOW); + + break; + } + + case GameState::Multiplayer: + { + // Toggle pause state with 'P' key + if (IsKeyPressed(KEY_P)) { + isPaused = !isPaused; + } + + // Force GameOver with SPACEBAR key + if (IsKeyPressed(KEY_SPACE)) { + currentState = GameState::GameOver; + } + + if (!isPaused) { + sessionPlayTime += GetFrameTime(); + + if (ball.Update() && sfxEnabled) { + PlaySound(wallHitSound); + } + player.Update(); + + // Player 2 controls (Left Paddle - Player 2 paddle) + float dt = GetFrameTime(); + if (IsKeyDown(KEY_W)) { + cpu.position.y -= PLAYER_SPEED * dt; + } + if (IsKeyDown(KEY_S)) { + cpu.position.y += PLAYER_SPEED * dt; + } + cpu.LimitMovement(); + cpu.position.x = 20.0f + 10.0f; // Keep on left track + + if (CheckCollisionCircleRec(ball.position, ball.radius, Rectangle{ player.position.x, player.position.y, player.width, player.height })) { + ball.velocity.x *= -1; + if (sfxEnabled) PlaySound(paddleHitSound); + } + + if (CheckCollisionCircleRec(ball.position, ball.radius, Rectangle{ cpu.position.x, cpu.position.y, cpu.width, cpu.height })) { + ball.velocity.x *= -1; + if (sfxEnabled) PlaySound(paddleHitSound); + } + + if (ball.position.x + ball.radius >= screen_width - 20.0f) { + score.player2_score++; + if (sfxEnabled) PlaySound(scoreSound); + if (score.player2_score >= maxScore) { + currentState = GameState::GameOver; + } + else { + ResetBall(ball); + } + } + if (ball.position.x - ball.radius <= 20.0f) { + score.player_score++; + if (sfxEnabled) PlaySound(scoreSound); + if (score.player_score >= maxScore) { + currentState = GameState::GameOver; + } + else { + ResetBall(ball); + } + } + } + + // --- Draw Textured Court Background --- + // Left Court (basic_space.png) + DrawTexturePro( + courtBackground, + Rectangle{ 0.0f, 0.0f, (float)courtBackground.width, (float)courtBackground.height }, + Rectangle{ 20.0f, 20.0f, (float)screen_width / 2.0f - 70.0f, (float)screen_height - 40.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Right Court (basic_space.png) + DrawTexturePro( + courtBackground, + Rectangle{ 0.0f, 0.0f, (float)courtBackground.width, (float)courtBackground.height }, + Rectangle{ (float)screen_width / 2.0f + 50.0f, 20.0f, (float)screen_width / 2.0f - 70.0f, (float)screen_height - 40.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Center Circle + DrawCircle(screen_width / 2, screen_height / 2, 50.0f, Color{ 102, 51, 153, 100 }); + DrawCircleLines(screen_width / 2, screen_height / 2, 50.0f, Color{ 50, 25, 75, 250 }); + + // Tiled Center Dashed Line (line.png) + int lineY = 20; + while (lineY < screen_height - 20) { + DrawTexture(lineTexture, screen_width / 2 - lineTexture.width / 2, lineY, WHITE); + lineY += lineTexture.height; + } + + // Top Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ 0.0f, 0.0f, (float)screen_width, 20.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Bottom Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ 0.0f, (float)screen_height - 20.0f, (float)screen_width, 20.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Left Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ 0.0f, 0.0f, 20.0f, (float)screen_height }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Right Wall (walls.png) + DrawTexturePro( + wallsTexture, + Rectangle{ 0.0f, 0.0f, (float)wallsTexture.width, (float)wallsTexture.height }, + Rectangle{ (float)screen_width - 20.0f, 0.0f, 20.0f, (float)screen_height }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Scores: Left shows Player 2 score, Right shows Player 1 score + DrawText(TextFormat("%i", score.player2_score), screen_width / 4 - 20, 20, 80, WHITE); + DrawText(TextFormat("%i", score.player_score), 3 * screen_width / 4 - 20, 20, 80, WHITE); + + // Draw Playtime Counter + int minutes = (int)sessionPlayTime / 60; + int seconds = (int)sessionPlayTime % 60; + int timeTextWidth = MeasureText(TextFormat("%02i:%02i", minutes, seconds), 32); + + // Draw background box to block out the center line and increase contrast + DrawRectangle(screen_width / 2 - timeTextWidth / 2 - 15, screen_height - 85, timeTextWidth + 30, 44, Color{ 15, 15, 15, 220 }); + DrawRectangleLines(screen_width / 2 - timeTextWidth / 2 - 15, screen_height - 85, timeTextWidth + 30, 44, Color{ 100, 100, 100, 255 }); + DrawText(TextFormat("%02i:%02i", minutes, seconds), screen_width / 2 - timeTextWidth / 2, screen_height - 79, 32, YELLOW); + + ball.Draw(); + cpu.Draw(); // Left paddle (reused cpu object) + player.Draw(); // Right paddle + + // Draw Pause Overlay and Text + if (isPaused) { + DrawRectangle(20, 20, screen_width - 40, screen_height - 40, Color{ 0, 0, 0, 150 }); + + int pausedTextWidth = MeasureText("PAUSED", 60); + DrawText("PAUSED", screen_width / 2 - pausedTextWidth / 2, screen_height / 2 - 60, 60, YELLOW); + + int hintTextWidth = MeasureText("P1 (Right): UP/DOWN | P2 (Left): W/S | SPACE: Force Game Over", 20); + DrawText("P1 (Right): UP/DOWN | P2 (Left): W/S | SPACE: Force Game Over", + screen_width / 2 - hintTextWidth / 2, screen_height / 2 + 20, 20, WHITE); + } + break; } @@ -247,13 +593,9 @@ int main() { } } - // Draw Settings Menu - int sw = GetScreenWidth(); - int sh = GetScreenHeight(); - // Title int titleWidth = MeasureText("SETTINGS", 60); - DrawText("SETTINGS", sw / 2 - titleWidth / 2, sh / 4 - 40, 60, WHITE); + DrawText("SETTINGS", screen_width / 2 - titleWidth / 2, screen_height / 4 - 40, 60, WHITE); // Construct resolution string std::string resStr = "Resolution: "; @@ -288,17 +630,19 @@ int main() { Color sfxColor = (selectedSettingLine == 4) ? YELLOW : WHITE; Color backColor = (selectedSettingLine == 5) ? YELLOW : WHITE; - int startY = sh / 2 - 90; - DrawText(resStr.c_str(), sw / 2 - MeasureText(resStr.c_str(), 30) / 2, startY, 30, resColor); - DrawText(fpsStr.c_str(), sw / 2 - MeasureText(fpsStr.c_str(), 30) / 2, startY + 45, 30, fpsColor); - DrawText(modeStr.c_str(), sw / 2 - MeasureText(modeStr.c_str(), 30) / 2, startY + 90, 30, modeColor); - DrawText(scoreLimitStr.c_str(), sw / 2 - MeasureText(scoreLimitStr.c_str(), 30) / 2, startY + 135, 30, scoreColor); - DrawText(sfxStr.c_str(), sw / 2 - MeasureText(sfxStr.c_str(), 30) / 2, startY + 180, 30, sfxColor); - DrawText("Back", sw / 2 - MeasureText("Back", 30) / 2, startY + 225, 30, backColor); + int startY = screen_height / 2 - 90; + DrawText(resStr.c_str(), screen_width / 2 - MeasureText(resStr.c_str(), 30) / 2, startY, 30, resColor); + DrawText(fpsStr.c_str(), screen_width / 2 - MeasureText(fpsStr.c_str(), 30) / 2, startY + 45, 30, fpsColor); + DrawText(modeStr.c_str(), screen_width / 2 - MeasureText(modeStr.c_str(), 30) / 2, startY + 90, 30, modeColor); + DrawText(scoreLimitStr.c_str(), screen_width / 2 - MeasureText(scoreLimitStr.c_str(), 30) / 2, startY + 135, 30, scoreColor); + DrawText(sfxStr.c_str(), screen_width / 2 - MeasureText(sfxStr.c_str(), 30) / 2, startY + 180, 30, sfxColor); + DrawText("Back", screen_width / 2 - MeasureText("Back", 30) / 2, startY + 225, 30, backColor); // Controls helper - int helperWidth = MeasureText("Use UP/DOWN to navigate, LEFT/RIGHT to change settings, ENTER to select", 20); - DrawText("Use UP/DOWN to navigate, LEFT/RIGHT to change settings, ENTER to select", sw / 2 - helperWidth / 2, sh - 60, 20, GRAY); + int settingsHintWidth = MeasureText("UP/DOWN to navigate | LEFT/RIGHT to change settings | ENTER to select", 20); + DrawText("UP/DOWN to navigate | LEFT/RIGHT to change settings | ENTER to select", + screen_width / 2 - settingsHintWidth / 2, + screen_height - 50, 20, WHITE); break; } @@ -453,7 +797,11 @@ int main() { DrawRectangle(20, 20, screen_width - 40, screen_height - 40, Color{ 0, 0, 0, 150 }); int pausedTextWidth = MeasureText("PAUSED", 60); - DrawText("PAUSED", screen_width / 2 - pausedTextWidth / 2, screen_height / 2 - 30, 60, YELLOW); + DrawText("PAUSED", screen_width / 2 - pausedTextWidth / 2, screen_height / 2 - 60, 60, YELLOW); + + int hintTextWidth = MeasureText("UP/DOWN Arrows to move | SPACE: Force Game Over", 20); + DrawText("UP/DOWN Arrows to move | SPACE: Force Game Over", + screen_width / 2 - hintTextWidth / 2, screen_height / 2 + 20, 20, WHITE); } break; @@ -481,26 +829,42 @@ int main() { // Determine winner text and color std::string winnerText = "GAME ENDED"; Color winnerColor = YELLOW; - if (score.player_score > score.cpu_score) { - winnerText = "YOU WIN!"; - winnerColor = Green; + std::string score1Str, score2Str; + + if (isMultiplayer) { + if (score.player_score > score.player2_score) { + winnerText = "PLAYER 1 WINS!"; + winnerColor = Green; + } + else if (score.player2_score > score.player_score) { + winnerText = "PLAYER 2 WINS!"; + winnerColor = RED; + } + score1Str = "Player 1 Score: " + std::to_string(score.player_score); + score2Str = "Player 2 Score: " + std::to_string(score.player2_score); } - else if (score.cpu_score > score.player_score) { - winnerText = "CPU WINS!"; - winnerColor = RED; + else { + if (score.player_score > score.cpu_score) { + winnerText = "YOU WIN!"; + winnerColor = Green; + } + else if (score.cpu_score > score.player_score) { + winnerText = "CPU WINS!"; + winnerColor = RED; + } + score1Str = "Player Score: " + std::to_string(score.player_score); + score2Str = "CPU Score: " + std::to_string(score.cpu_score); } + int winnerWidth = MeasureText(winnerText.c_str(), 40); DrawText(winnerText.c_str(), screen_width / 2 - winnerWidth / 2, panelY + 120, 40, winnerColor); // Draw final scores - std::string playerScoreStr = "Player Score: " + std::to_string(score.player_score); - std::string cpuScoreStr = "CPU Score: " + std::to_string(score.cpu_score); - - int playerTextWidth = MeasureText(playerScoreStr.c_str(), 30); - int cpuTextWidth = MeasureText(cpuScoreStr.c_str(), 30); + int playerTextWidth = MeasureText(score1Str.c_str(), 30); + int cpuTextWidth = MeasureText(score2Str.c_str(), 30); - DrawText(playerScoreStr.c_str(), screen_width / 2 - playerTextWidth / 2, panelY + 200, 30, WHITE); - DrawText(cpuScoreStr.c_str(), screen_width / 2 - cpuTextWidth / 2, panelY + 250, 30, WHITE); + DrawText(score1Str.c_str(), screen_width / 2 - playerTextWidth / 2, panelY + 200, 30, WHITE); + DrawText(score2Str.c_str(), screen_width / 2 - cpuTextWidth / 2, panelY + 250, 30, WHITE); // Draw playtime int minutes = (int)sessionPlayTime / 60;