From 7493daf00471ed90b485af6bee8ed023d0f06a04 Mon Sep 17 00:00:00 2001 From: dr20ervin Date: Thu, 21 May 2026 17:29:18 +0300 Subject: [PATCH] Refactor game architecture and enhance features --- include/game.h | 61 +++- include/menu.h | 13 +- src/game.cpp | 298 +++++++++++++++- src/main.cpp | 907 ++++++------------------------------------------- src/menu.cpp | 232 ++++++++++++- 5 files changed, 678 insertions(+), 833 deletions(-) diff --git a/include/game.h b/include/game.h index b06055c..8d6059a 100644 --- a/include/game.h +++ b/include/game.h @@ -4,7 +4,7 @@ #include #include -// Game States & Configurations +// Game states and configurations enum class GameState { MainMenu, DifficultySelect, @@ -28,8 +28,60 @@ constexpr float CPU_SPEED_HARD = 480.0f; constexpr float PLAYER_SPEED = 360.0f; constexpr float BALL_SPEED = 420.0f; -// --- Base Entities --- +// Game state contexts +struct ScoreBoard { + int player_score = 0; + int player2_score = 0; + int cpu_score = 0; +}; +struct GameConfig { + int resolutionOption = 0; // 0 = 1280x800, 1 = 1600x900, 2 = 1920x1080 + int framerateOption = 0; // 0 = 60 FPS, 1 = 144 FPS, 2 = VSync + bool isFullscreen = false; + int maxScoreOption = 0; // 0 = 5, 1 = 11, 2 = 15, 3 = 21 + int maxScore = 5; + bool sfxEnabled = true; + int selectedSettingLine = 0; // 0 = Resolution, 1 = Framerate, 2 = Screen Mode, 3 = Score Limit, 4 = Sound, 5 = Back +}; + +struct GameContext { + GameState currentState = GameState::MainMenu; + float sessionPlayTime = 0.0f; + bool isPaused = false; + bool shouldQuit = false; + bool isMultiplayer = false; + bool p1Ready = false; + bool p2Ready = false; + ScoreBoard score; + GameConfig config; + + // Asset textures and sound handles + Texture2D courtBackground = { 0 }; + Texture2D wallsTexture = { 0 }; + Texture2D lineTexture = { 0 }; + Sound paddleHitSound = { 0 }; + Sound wallHitSound = { 0 }; + Sound scoreSound = { 0 }; +}; + +// Forward declarations +class Ball; +class Paddle; +class CpuPaddle; + +// Game state update and draw routines +void ResetBall(Ball& ball); +void DrawCourt(const GameContext& ctx, int screenWidth, int screenHeight); +void UpdatePlayingState(GameContext& ctx, Ball& ball, Paddle& player, CpuPaddle& cpu); +void DrawPlayingState(const GameContext& ctx, Ball& ball, Paddle& player, CpuPaddle& cpu, int screenWidth, int screenHeight); +void UpdateMultiplayerState(GameContext& ctx, Ball& ball, Paddle& player, CpuPaddle& cpu); +void DrawMultiplayerState(const GameContext& ctx, Ball& ball, Paddle& player, CpuPaddle& cpu, int screenWidth, int screenHeight); +void UpdateGameOverState(GameContext& ctx); +void DrawGameOverState(const GameContext& ctx, int screenWidth, int screenHeight); + + +// Base entity representation class GameObject { public: Vector2 position; @@ -41,15 +93,12 @@ public: virtual void Draw() = 0; }; -// --- Game Objects --- - +// Entity classes class Paddle : public GameObject { public: float width; float height; Texture2D texture; - -public: Paddle(Vector2 pos, Color c, float w, float h, const std::string& texturePath = "") : GameObject(pos, c), width(w), height(h) { if (!texturePath.empty()) { diff --git a/include/menu.h b/include/menu.h index 07c247d..6582ea4 100644 --- a/include/menu.h +++ b/include/menu.h @@ -3,8 +3,7 @@ #include #include -// --- UI / Systems --- - +// Menu UI controller class Menu { private: std::string title; @@ -18,4 +17,12 @@ public: int Update(); void Draw(); -}; \ No newline at end of file +}; + +// Settings and lobby routines +void ApplyResolution(int width, int height); +void ApplyFramerate(int option); +void UpdateSettingsState(GameContext& ctx); +void DrawSettingsState(const GameContext& ctx, int screenWidth, int screenHeight); +void UpdateLobbyState(GameContext& ctx); +void DrawLobbyState(const GameContext& ctx, int screenWidth, int screenHeight); diff --git a/src/game.cpp b/src/game.cpp index c68fdb7..b54c815 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -1,7 +1,6 @@ #include "game.h" -// --- Paddle Implementation --- - +// Paddle implementation bool Paddle::Update() { float dt = GetFrameTime(); @@ -12,7 +11,7 @@ bool Paddle::Update() { position.y += PLAYER_SPEED * dt; } - // Limit movement + // Keep paddle within screen boundaries if (position.y <= 20.0f) { position.y = 20.0f; } @@ -20,7 +19,7 @@ bool Paddle::Update() { position.y = GetScreenHeight() - 20.0f - height; } - // Dynamic X position + // Align paddle relative to right screen edge position.x = GetScreenWidth() - 20.0f - 10.0f - width; return false; @@ -42,8 +41,7 @@ void Paddle::Draw() { } -// --- Ball Implementation --- - +// Ball implementation bool Ball::Update() { float dt = GetFrameTime(); position.x += velocity.x * dt; @@ -67,7 +65,291 @@ void Ball::Draw() { WHITE ); } else { - // Cast to int as DrawCircle expects integers for coordinates DrawCircle((int)position.x, (int)position.y, radius, color); } -} \ No newline at end of file +} + + +// Game state routines and helpers +void ResetBall(Ball& ball) { + ball.position.x = GetScreenWidth() / 2.0f; + ball.position.y = GetScreenHeight() / 2.0f; + + int speed_choices[2] = { -1, 1 }; + ball.velocity.x = BALL_SPEED * speed_choices[GetRandomValue(0, 1)]; + ball.velocity.y = BALL_SPEED * speed_choices[GetRandomValue(0, 1)]; +} + +void DrawCourt(const GameContext& ctx, int screenWidth, int screenHeight) { + // Left court field background + DrawTexturePro( + ctx.courtBackground, + Rectangle{ 0.0f, 0.0f, (float)ctx.courtBackground.width, (float)ctx.courtBackground.height }, + Rectangle{ 20.0f, 20.0f, (float)screenWidth / 2.0f - 70.0f, (float)screenHeight - 40.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Right court field background + DrawTexturePro( + ctx.courtBackground, + Rectangle{ 0.0f, 0.0f, (float)ctx.courtBackground.width, (float)ctx.courtBackground.height }, + Rectangle{ (float)screenWidth / 2.0f + 50.0f, 20.0f, (float)screenWidth / 2.0f - 70.0f, (float)screenHeight - 40.0f }, + Vector2{ 0.0f, 0.0f }, + 0.0f, + WHITE + ); + + // Center circle decoration + DrawCircle(screenWidth / 2, screenHeight / 2, 50.0f, Color{ 102, 51, 153, 100 }); + DrawCircleLines(screenWidth / 2, screenHeight / 2, 50.0f, Color{ 50, 25, 75, 250 }); + + // Center dotted divider + int lineY = 20; + while (lineY < screenHeight - 20) { + DrawTexture(ctx.lineTexture, screenWidth / 2 - ctx.lineTexture.width / 2, lineY, WHITE); + lineY += ctx.lineTexture.height; + } + + // Border walls (top, bottom, left, right) + DrawTexturePro(ctx.wallsTexture, Rectangle{ 0.0f, 0.0f, (float)ctx.wallsTexture.width, (float)ctx.wallsTexture.height }, Rectangle{ 0.0f, 0.0f, (float)screenWidth, 20.0f }, Vector2{ 0.0f, 0.0f }, 0.0f, WHITE); + DrawTexturePro(ctx.wallsTexture, Rectangle{ 0.0f, 0.0f, (float)ctx.wallsTexture.width, (float)ctx.wallsTexture.height }, Rectangle{ 0.0f, (float)screenHeight - 20.0f, (float)screenWidth, 20.0f }, Vector2{ 0.0f, 0.0f }, 0.0f, WHITE); + DrawTexturePro(ctx.wallsTexture, Rectangle{ 0.0f, 0.0f, (float)ctx.wallsTexture.width, (float)ctx.wallsTexture.height }, Rectangle{ 0.0f, 0.0f, 20.0f, (float)screenHeight }, Vector2{ 0.0f, 0.0f }, 0.0f, WHITE); + DrawTexturePro(ctx.wallsTexture, Rectangle{ 0.0f, 0.0f, (float)ctx.wallsTexture.width, (float)ctx.wallsTexture.height }, Rectangle{ (float)screenWidth - 20.0f, 0.0f, 20.0f, (float)screenHeight }, Vector2{ 0.0f, 0.0f }, 0.0f, WHITE); +} + +void UpdatePlayingState(GameContext& ctx, Ball& ball, Paddle& player, CpuPaddle& cpu) { + if (IsKeyPressed(KEY_P)) { + ctx.isPaused = !ctx.isPaused; + } + if (IsKeyPressed(KEY_SPACE)) { + ctx.currentState = GameState::GameOver; + } + + if (!ctx.isPaused) { + ctx.sessionPlayTime += GetFrameTime(); + + if (ball.Update() && ctx.config.sfxEnabled) { + PlaySound(ctx.wallHitSound); + } + player.Update(); + cpu.Update(ball.position.y); + + // Paddle collisions + if (CheckCollisionCircleRec(ball.position, ball.radius, Rectangle{ player.position.x, player.position.y, player.width, player.height })) { + ball.velocity.x *= -1; + if (ctx.config.sfxEnabled) PlaySound(ctx.paddleHitSound); + } + if (CheckCollisionCircleRec(ball.position, ball.radius, Rectangle{ cpu.position.x, cpu.position.y, cpu.width, cpu.height })) { + ball.velocity.x *= -1; + if (ctx.config.sfxEnabled) PlaySound(ctx.paddleHitSound); + } + + // Scoring rules + int screen_width = GetScreenWidth(); + if (ball.position.x + ball.radius >= screen_width - 20.0f) { + ctx.score.cpu_score++; + if (ctx.config.sfxEnabled) PlaySound(ctx.scoreSound); + if (ctx.score.cpu_score >= ctx.config.maxScore) { + ctx.currentState = GameState::GameOver; + } else { + ResetBall(ball); + } + } + if (ball.position.x - ball.radius <= 20.0f) { + ctx.score.player_score++; + if (ctx.config.sfxEnabled) PlaySound(ctx.scoreSound); + if (ctx.score.player_score >= ctx.config.maxScore) { + ctx.currentState = GameState::GameOver; + } else { + ResetBall(ball); + } + } + } +} + +void DrawPlayingState(const GameContext& ctx, Ball& ball, Paddle& player, CpuPaddle& cpu, int screenWidth, int screenHeight) { + DrawCourt(ctx, screenWidth, screenHeight); + + // Header scores + DrawText(TextFormat("%i", ctx.score.cpu_score), screenWidth / 4 - 20, 20, 80, WHITE); + DrawText(TextFormat("%i", ctx.score.player_score), 3 * screenWidth / 4 - 20, 20, 80, WHITE); + + // Playtime tracker display + int minutes = (int)ctx.sessionPlayTime / 60; + int seconds = (int)ctx.sessionPlayTime % 60; + int timeTextWidth = MeasureText(TextFormat("%02i:%02i", minutes, seconds), 32); + + DrawRectangle(screenWidth / 2 - timeTextWidth / 2 - 15, screenHeight - 85, timeTextWidth + 30, 44, Color{ 15, 15, 15, 220 }); + DrawRectangleLines(screenWidth / 2 - timeTextWidth / 2 - 15, screenHeight - 85, timeTextWidth + 30, 44, Color{ 100, 100, 100, 255 }); + DrawText(TextFormat("%02i:%02i", minutes, seconds), screenWidth / 2 - timeTextWidth / 2, screenHeight - 79, 32, YELLOW); + + ball.Draw(); + cpu.Draw(); + player.Draw(); + + // Pause interface overlay + if (ctx.isPaused) { + DrawRectangle(20, 20, screenWidth - 40, screenHeight - 40, Color{ 0, 0, 0, 150 }); + int pausedTextWidth = MeasureText("PAUSED", 60); + DrawText("PAUSED", screenWidth / 2 - pausedTextWidth / 2, screenHeight / 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", screenWidth / 2 - hintTextWidth / 2, screenHeight / 2 + 20, 20, WHITE); + } +} + +void UpdateMultiplayerState(GameContext& ctx, Ball& ball, Paddle& player, CpuPaddle& cpu) { + if (IsKeyPressed(KEY_P)) { + ctx.isPaused = !ctx.isPaused; + } + if (IsKeyPressed(KEY_SPACE)) { + ctx.currentState = GameState::GameOver; + } + + if (!ctx.isPaused) { + ctx.sessionPlayTime += GetFrameTime(); + + if (ball.Update() && ctx.config.sfxEnabled) { + PlaySound(ctx.wallHitSound); + } + player.Update(); + + // Player 2 controls (Left 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; + + // Paddle collisions + if (CheckCollisionCircleRec(ball.position, ball.radius, Rectangle{ player.position.x, player.position.y, player.width, player.height })) { + ball.velocity.x *= -1; + if (ctx.config.sfxEnabled) PlaySound(ctx.paddleHitSound); + } + if (CheckCollisionCircleRec(ball.position, ball.radius, Rectangle{ cpu.position.x, cpu.position.y, cpu.width, cpu.height })) { + ball.velocity.x *= -1; + if (ctx.config.sfxEnabled) PlaySound(ctx.paddleHitSound); + } + + // Scoring rules + int screen_width = GetScreenWidth(); + if (ball.position.x + ball.radius >= screen_width - 20.0f) { + ctx.score.player2_score++; + if (ctx.config.sfxEnabled) PlaySound(ctx.scoreSound); + if (ctx.score.player2_score >= ctx.config.maxScore) { + ctx.currentState = GameState::GameOver; + } else { + ResetBall(ball); + } + } + if (ball.position.x - ball.radius <= 20.0f) { + ctx.score.player_score++; + if (ctx.config.sfxEnabled) PlaySound(ctx.scoreSound); + if (ctx.score.player_score >= ctx.config.maxScore) { + ctx.currentState = GameState::GameOver; + } else { + ResetBall(ball); + } + } + } +} + +void DrawMultiplayerState(const GameContext& ctx, Ball& ball, Paddle& player, CpuPaddle& cpu, int screenWidth, int screenHeight) { + DrawCourt(ctx, screenWidth, screenHeight); + + // Header scores + DrawText(TextFormat("%i", ctx.score.player2_score), screenWidth / 4 - 20, 20, 80, WHITE); + DrawText(TextFormat("%i", ctx.score.player_score), 3 * screenWidth / 4 - 20, 20, 80, WHITE); + + // Playtime tracker display + int minutes = (int)ctx.sessionPlayTime / 60; + int seconds = (int)ctx.sessionPlayTime % 60; + int timeTextWidth = MeasureText(TextFormat("%02i:%02i", minutes, seconds), 32); + + DrawRectangle(screenWidth / 2 - timeTextWidth / 2 - 15, screenHeight - 85, timeTextWidth + 30, 44, Color{ 15, 15, 15, 220 }); + DrawRectangleLines(screenWidth / 2 - timeTextWidth / 2 - 15, screenHeight - 85, timeTextWidth + 30, 44, Color{ 100, 100, 100, 255 }); + DrawText(TextFormat("%02i:%02i", minutes, seconds), screenWidth / 2 - timeTextWidth / 2, screenHeight - 79, 32, YELLOW); + + ball.Draw(); + cpu.Draw(); + player.Draw(); + + // Pause interface overlay + if (ctx.isPaused) { + DrawRectangle(20, 20, screenWidth - 40, screenHeight - 40, Color{ 0, 0, 0, 150 }); + int pausedTextWidth = MeasureText("PAUSED", 60); + DrawText("PAUSED", screenWidth / 2 - pausedTextWidth / 2, screenHeight / 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", screenWidth / 2 - hintTextWidth / 2, screenHeight / 2 + 20, 20, WHITE); + } +} + +void UpdateGameOverState(GameContext& ctx) { + if (IsKeyPressed(KEY_SPACE)) { + ctx.currentState = GameState::MainMenu; + } +} + +void DrawGameOverState(const GameContext& ctx, int screenWidth, int screenHeight) { + int panelWidth = 600; + int panelHeight = 400; + int panelX = screenWidth / 2 - panelWidth / 2; + int panelY = screenHeight / 2 - panelHeight / 2; + + DrawRectangle(panelX, panelY, panelWidth, panelHeight, Color{ 15, 15, 15, 230 }); + DrawRectangleLines(panelX, panelY, panelWidth, panelHeight, Color{ 120, 120, 120, 255 }); + + int gameOverWidth = MeasureText("GAME OVER", 60); + DrawText("GAME OVER", screenWidth / 2 - gameOverWidth / 2, panelY + 40, 60, RED); + + std::string winnerText = "GAME ENDED"; + Color winnerColor = YELLOW; + std::string score1Str, score2Str; + + if (ctx.isMultiplayer) { + if (ctx.score.player_score > ctx.score.player2_score) { + winnerText = "PLAYER 1 WINS!"; + winnerColor = Color{ 38, 185, 154, 255 }; + } + else if (ctx.score.player2_score > ctx.score.player_score) { + winnerText = "PLAYER 2 WINS!"; + winnerColor = RED; + } + score1Str = "Player 1 Score: " + std::to_string(ctx.score.player_score); + score2Str = "Player 2 Score: " + std::to_string(ctx.score.player2_score); + } + else { + if (ctx.score.player_score > ctx.score.cpu_score) { + winnerText = "YOU WIN!"; + winnerColor = Color{ 38, 185, 154, 255 }; + } + else if (ctx.score.cpu_score > ctx.score.player_score) { + winnerText = "CPU WINS!"; + winnerColor = RED; + } + score1Str = "Player Score: " + std::to_string(ctx.score.player_score); + score2Str = "CPU Score: " + std::to_string(ctx.score.cpu_score); + } + + int winnerWidth = MeasureText(winnerText.c_str(), 40); + DrawText(winnerText.c_str(), screenWidth / 2 - winnerWidth / 2, panelY + 120, 40, winnerColor); + + int playerTextWidth = MeasureText(score1Str.c_str(), 30); + int cpuTextWidth = MeasureText(score2Str.c_str(), 30); + DrawText(score1Str.c_str(), screenWidth / 2 - playerTextWidth / 2, panelY + 200, 30, WHITE); + DrawText(score2Str.c_str(), screenWidth / 2 - cpuTextWidth / 2, panelY + 250, 30, WHITE); + + int minutes = (int)ctx.sessionPlayTime / 60; + int seconds = (int)ctx.sessionPlayTime % 60; + std::string timeStr = "Playtime: " + std::to_string(minutes) + "m " + std::to_string(seconds) + "s"; + int timeTextWidth = MeasureText(timeStr.c_str(), 20); + DrawText(timeStr.c_str(), screenWidth / 2 - timeTextWidth / 2, panelY + 300, 20, LIGHTGRAY); + + int instWidth = MeasureText("Press SPACEBAR to return to Main Menu", 20); + DrawText("Press SPACEBAR to return to Main Menu", screenWidth / 2 - instWidth / 2, panelY + 340, 20, YELLOW); +} diff --git a/src/main.cpp b/src/main.cpp index b726242..b0726a6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,77 +2,41 @@ #include "game.h" #include "menu.h" -// Colors +// Global colors Color Green = Color{ 38, 185, 154, 255 }; Color Dark_Green = Color{ 20, 160, 133, 255 }; Color Light_Green = Color{ 129, 204, 184, 255 }; Color Yellow = Color{ 243, 213, 91, 255 }; -struct ScoreBoard -{ - int player_score = 0; - int player2_score = 0; - int cpu_score = 0; -}; - - -void ApplyResolution(int width, int height) { - int monitor = GetCurrentMonitor(); - int monitorWidth = GetMonitorWidth(monitor); - int monitorHeight = GetMonitorHeight(monitor); - - if (width >= monitorWidth || height >= monitorHeight) { - SetWindowState(FLAG_WINDOW_MAXIMIZED | FLAG_WINDOW_RESIZABLE); - } - else { - ClearWindowState(FLAG_WINDOW_MAXIMIZED); - SetWindowSize(width, height); - // Center the window on the active monitor - SetWindowPosition(monitorWidth / 2 - width / 2, monitorHeight / 2 - height / 2); - } -} - -void ApplyFramerate(int option) { - if (option == 0) { - ClearWindowState(FLAG_VSYNC_HINT); - SetTargetFPS(60); - } - else if (option == 1) { - ClearWindowState(FLAG_VSYNC_HINT); - SetTargetFPS(144); - } - else if (option == 2) { - SetWindowState(FLAG_VSYNC_HINT); - SetTargetFPS(0); - } -} - -// Helper function to reset the ball -void ResetBall(Ball& ball) { - ball.position.x = GetScreenWidth() / 2.0f; - ball.position.y = GetScreenHeight() / 2.0f; - - int speed_choices[2] = { -1, 1 }; - ball.velocity.x = BALL_SPEED * speed_choices[GetRandomValue(0, 1)]; - ball.velocity.y = BALL_SPEED * speed_choices[GetRandomValue(0, 1)]; -} - int main() { - std::cout << "Starting the game" << std::endl; + std::cout << "Starting game session" << std::endl; int screen_width = 1280; int screen_height = 800; + InitWindow(screen_width, screen_height, "Pong Reloaded"); SetTargetFPS(60); InitAudioDevice(); - // --- Instantiate Objects using the new Constructors --- + // App state context + GameContext ctx; + // Asset textures loading + ctx.courtBackground = LoadTexture("assets/textures/spaces/basic_space.png"); + ctx.wallsTexture = LoadTexture("assets/textures/spaces/walls.png"); + ctx.lineTexture = LoadTexture("assets/textures/hud/line.png"); + + // Audio assets loading + ctx.paddleHitSound = LoadSound("assets/audio/paddle_hit.ogg"); + ctx.wallHitSound = LoadSound("assets/audio/wall_hit.ogg"); + ctx.scoreSound = LoadSound("assets/audio/score.ogg"); + + // Entities instantiation Ball ball( Vector2{ screen_width / 2.0f, screen_height / 2.0f }, Yellow, 20.0f, "assets/textures/ball/basic_ball_5.png" ); - ball.velocity = Vector2{ BALL_SPEED, BALL_SPEED }; + ResetBall(ball); Paddle player( Vector2{ screen_width - 20.0f - 10.0f - 25.0f, screen_height / 2.0f - 60.0f }, @@ -87,798 +51,122 @@ int main() { "assets/textures/paddles/basic_paddle_2.png" ); - // --- Setup Menu and Game State --- - - GameState currentState = GameState::MainMenu; - 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 - bool isFullscreen = false; - int maxScoreOption = 0; // 0 = 5, 1 = 11, 2 = 15, 3 = 21 - int maxScore = 5; - bool sfxEnabled = true; - int selectedSettingLine = 0; // 0 = Resolution, 1 = Framerate, 2 = Screen Mode, 3 = Score Limit, 4 = Sound, 5 = Back - + // Menu instantiation Menu mainMenu("PONG RELOADED", { "Singleplayer", "Multiplayer", "Settings", "Quit" }); Menu difficultyMenu("SELECT DIFFICULTY", { "Easy", "Normal", "Hard", "Back" }); - ScoreBoard score; - // --- Load Background and Wall Textures --- - Texture2D courtBackground = LoadTexture("assets/textures/spaces/basic_space.png"); - Texture2D wallsTexture = LoadTexture("assets/textures/spaces/walls.png"); - Texture2D lineTexture = LoadTexture("assets/textures/hud/line.png"); - - // --- Load Sound Effects --- - Sound paddleHitSound = LoadSound("assets/audio/paddle_hit.ogg"); - Sound wallHitSound = LoadSound("assets/audio/wall_hit.ogg"); - Sound scoreSound = LoadSound("assets/audio/score.ogg"); - - // --- Main Game Loop --- - - while (WindowShouldClose() == false && !shouldQuit) { + // Main loop + while (WindowShouldClose() == false && !ctx.shouldQuit) { screen_width = GetScreenWidth(); screen_height = GetScreenHeight(); - BeginDrawing(); - ClearBackground(Dark_Green); - - switch (currentState) { + // Update loop + switch (ctx.currentState) { case GameState::MainMenu: { int selected = mainMenu.Update(); if (selected == 0) { - isMultiplayer = false; - currentState = GameState::DifficultySelect; + ctx.isMultiplayer = false; + ctx.currentState = GameState::DifficultySelect; } else if (selected == 1) { - isMultiplayer = true; - score.player_score = 0; - score.player2_score = 0; - score.cpu_score = 0; - sessionPlayTime = 0.0f; - isPaused = false; - p1Ready = false; - p2Ready = false; + ctx.isMultiplayer = true; + ctx.score.player_score = 0; + ctx.score.player2_score = 0; + ctx.score.cpu_score = 0; + ctx.sessionPlayTime = 0.0f; + ctx.isPaused = false; + ctx.p1Ready = false; + ctx.p2Ready = false; ResetBall(ball); - currentState = GameState::MultiplayerLobby; + ctx.currentState = GameState::MultiplayerLobby; } else if (selected == 2) { - currentState = GameState::Settings; + ctx.currentState = GameState::Settings; } else if (selected == 3) { - shouldQuit = true; + ctx.shouldQuit = true; } - mainMenu.Draw(); + break; + } + case GameState::DifficultySelect: + { + int selected = difficultyMenu.Update(); + if (selected >= 0 && selected <= 2) { + if (selected == 0) cpu.SetDifficulty(Difficulty::Easy); + else if (selected == 1) cpu.SetDifficulty(Difficulty::Normal); + else if (selected == 2) cpu.SetDifficulty(Difficulty::Hard); - // Controls helper + ctx.score.player_score = 0; + ctx.score.player2_score = 0; + ctx.score.cpu_score = 0; + ctx.sessionPlayTime = 0.0f; + ctx.isPaused = false; + ResetBall(ball); + ctx.currentState = GameState::Playing; + } + else if (selected == 3) { + ctx.currentState = GameState::MainMenu; + } + break; + } + case GameState::MultiplayerLobby: + UpdateLobbyState(ctx); + break; + case GameState::Multiplayer: + UpdateMultiplayerState(ctx, ball, player, cpu); + break; + case GameState::Settings: + UpdateSettingsState(ctx); + break; + case GameState::Playing: + UpdatePlayingState(ctx, ball, player, cpu); + break; + case GameState::GameOver: + UpdateGameOverState(ctx); + break; + default: + break; + } + + // Draw loop + BeginDrawing(); + ClearBackground(Dark_Green); + + switch (ctx.currentState) { + case GameState::MainMenu: + { + mainMenu.Draw(); 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; } - case GameState::DifficultySelect: { - int selected = difficultyMenu.Update(); - if (selected >= 0 && selected <= 2) { - if (selected == 0) { - cpu.SetDifficulty(Difficulty::Easy); - } - else if (selected == 1) { - cpu.SetDifficulty(Difficulty::Normal); - } - else if (selected == 2) { - cpu.SetDifficulty(Difficulty::Hard); - } - - // Reset game session stats - score.player_score = 0; - score.player2_score = 0; - score.cpu_score = 0; - sessionPlayTime = 0.0f; - isPaused = false; - ResetBall(ball); - currentState = GameState::Playing; - } - else if (selected == 3) { - 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); + int gameHintWidth = MeasureText("Player: 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); - + DrawLobbyState(ctx, screen_width, screen_height); 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); - } - + DrawMultiplayerState(ctx, ball, player, cpu, screen_width, screen_height); break; - } - case GameState::Settings: - { - // Navigation - if (IsKeyPressed(KEY_UP)) { - selectedSettingLine--; - if (selectedSettingLine < 0) selectedSettingLine = 5; - } - if (IsKeyPressed(KEY_DOWN)) { - selectedSettingLine++; - if (selectedSettingLine > 5) selectedSettingLine = 0; - } - - // Option selection (LEFT/RIGHT or ENTER) - if (selectedSettingLine == 0) { - // Resolution - if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_ENTER)) { - resolutionOption = (resolutionOption + 1) % 3; - if (resolutionOption == 0) ApplyResolution(1280, 800); - else if (resolutionOption == 1) ApplyResolution(1600, 900); - else if (resolutionOption == 2) ApplyResolution(1920, 1080); - } - else if (IsKeyPressed(KEY_LEFT)) { - resolutionOption = (resolutionOption - 1 + 3) % 3; - if (resolutionOption == 0) ApplyResolution(1280, 800); - else if (resolutionOption == 1) ApplyResolution(1600, 900); - else if (resolutionOption == 2) ApplyResolution(1920, 1080); - } - } - else if (selectedSettingLine == 1) { - // Framerate - if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_ENTER)) { - framerateOption = (framerateOption + 1) % 3; - ApplyFramerate(framerateOption); - } - else if (IsKeyPressed(KEY_LEFT)) { - framerateOption = (framerateOption - 1 + 3) % 3; - ApplyFramerate(framerateOption); - } - } - else if (selectedSettingLine == 2) { - // Screen Mode - if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_ENTER)) { - ToggleFullscreen(); - isFullscreen = !isFullscreen; - } - } - else if (selectedSettingLine == 3) { - // Score Limit - if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_ENTER)) { - maxScoreOption = (maxScoreOption + 1) % 4; - } - else if (IsKeyPressed(KEY_LEFT)) { - maxScoreOption = (maxScoreOption - 1 + 4) % 4; - } - - if (maxScoreOption == 0) maxScore = 5; - else if (maxScoreOption == 1) maxScore = 11; - else if (maxScoreOption == 2) maxScore = 15; - else if (maxScoreOption == 3) maxScore = 21; - } - else if (selectedSettingLine == 4) { - // Sound Effects - if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_ENTER)) { - sfxEnabled = !sfxEnabled; - } - } - else if (selectedSettingLine == 5) { - // Back - if (IsKeyPressed(KEY_ENTER)) { - currentState = GameState::MainMenu; - } - } - - // Title - int titleWidth = MeasureText("SETTINGS", 60); - DrawText("SETTINGS", screen_width / 2 - titleWidth / 2, screen_height / 4 - 40, 60, WHITE); - - // Construct resolution string - std::string resStr = "Resolution: "; - if (resolutionOption == 0) resStr += "1280x800"; - else if (resolutionOption == 1) resStr += "1600x900"; - else if (resolutionOption == 2) resStr += "1920x1080 (Windowed)"; - - // Construct framerate string - std::string fpsStr = "Framerate: "; - if (framerateOption == 0) fpsStr += "60 FPS"; - else if (framerateOption == 1) fpsStr += "144 FPS"; - else if (framerateOption == 2) fpsStr += "VSync"; - - // Construct screen mode string - std::string modeStr = "Screen Mode: "; - if (isFullscreen) modeStr += "Fullscreen"; - else modeStr += "Windowed"; - - // Construct score limit string - std::string scoreLimitStr = "Score Limit: " + std::to_string(maxScore); - - // Construct sound effects string - std::string sfxStr = "Sound Effects: "; - if (sfxEnabled) sfxStr += "ON"; - else sfxStr += "OFF"; - - // Draw option lines - Color resColor = (selectedSettingLine == 0) ? YELLOW : WHITE; - Color fpsColor = (selectedSettingLine == 1) ? YELLOW : WHITE; - Color modeColor = (selectedSettingLine == 2) ? YELLOW : WHITE; - Color scoreColor = (selectedSettingLine == 3) ? YELLOW : WHITE; - Color sfxColor = (selectedSettingLine == 4) ? YELLOW : WHITE; - Color backColor = (selectedSettingLine == 5) ? YELLOW : WHITE; - - 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 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); - + DrawSettingsState(ctx, screen_width, screen_height); break; - } - case GameState::Playing: - { - // 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(); - cpu.Update(ball.position.y); - - 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.cpu_score++; - if (sfxEnabled) PlaySound(scoreSound); - if (score.cpu_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 strip (Dark Green background area) - already cleared by ClearBackground - - // 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 - ); - - DrawText(TextFormat("%i", score.cpu_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(); - player.Draw(); - - // Draw Pause Overlay and Text - if (isPaused) { - // Semi-transparent overlay inside the borders - 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("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); - } - + DrawPlayingState(ctx, ball, player, cpu, screen_width, screen_height); break; - } case GameState::GameOver: - { - if (IsKeyPressed(KEY_SPACE)) { - currentState = GameState::MainMenu; - } - - // Draw a nice centered dark panel for game over info - int panelWidth = 600; - int panelHeight = 400; - 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 }); - - // Draw "GAME OVER" header - int gameOverWidth = MeasureText("GAME OVER", 60); - DrawText("GAME OVER", screen_width / 2 - gameOverWidth / 2, panelY + 40, 60, RED); - - // Determine winner text and color - std::string winnerText = "GAME ENDED"; - Color winnerColor = YELLOW; - 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.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 - int playerTextWidth = MeasureText(score1Str.c_str(), 30); - int cpuTextWidth = MeasureText(score2Str.c_str(), 30); - - 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; - int seconds = (int)sessionPlayTime % 60; - std::string timeStr = "Playtime: " + std::to_string(minutes) + "m " + std::to_string(seconds) + "s"; - int timeTextWidth = MeasureText(timeStr.c_str(), 20); - DrawText(timeStr.c_str(), screen_width / 2 - timeTextWidth / 2, panelY + 300, 20, LIGHTGRAY); - - // Instruction to return - int instWidth = MeasureText("Press SPACEBAR to return to Main Menu", 20); - DrawText("Press SPACEBAR to return to Main Menu", screen_width / 2 - instWidth / 2, panelY + 340, 20, YELLOW); - + DrawGameOverState(ctx, screen_width, screen_height); break; - } default: break; } @@ -886,16 +174,15 @@ int main() { EndDrawing(); } - UnloadTexture(courtBackground); - UnloadTexture(wallsTexture); - UnloadTexture(lineTexture); - - UnloadSound(paddleHitSound); - UnloadSound(wallHitSound); - UnloadSound(scoreSound); + // Asset unloading and cleanup + UnloadTexture(ctx.courtBackground); + UnloadTexture(ctx.wallsTexture); + UnloadTexture(ctx.lineTexture); + UnloadSound(ctx.paddleHitSound); + UnloadSound(ctx.wallHitSound); + UnloadSound(ctx.scoreSound); CloseAudioDevice(); - CloseWindow(); return 0; } \ No newline at end of file diff --git a/src/menu.cpp b/src/menu.cpp index 0876d78..7e7b32d 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -1,7 +1,7 @@ #include "menu.h" int Menu::Update() { - // Handle Navigation + // Menu navigation if (IsKeyPressed(KEY_DOWN)) { selectedIndex++; if (selectedIndex >= static_cast(options.size())) selectedIndex = 0; @@ -11,7 +11,7 @@ int Menu::Update() { if (selectedIndex < 0) selectedIndex = static_cast(options.size()) - 1; } - // Handle Selection + // Option selection if (IsKeyPressed(KEY_ENTER)) { return selectedIndex; } @@ -22,14 +22,234 @@ void Menu::Draw() { int screenWidth = GetScreenWidth(); int screenHeight = GetScreenHeight(); - // Draw Title + // Menu title and elements rendering DrawText(title.c_str(), screenWidth / 2 - MeasureText(title.c_str(), 60) / 2, screenHeight / 4, 60, WHITE); - // Draw Options for (int i = 0; i < static_cast(options.size()); i++) { Color textColor = (i == selectedIndex) ? YELLOW : WHITE; DrawText(options[i].c_str(), screenWidth / 2 - MeasureText(options[i].c_str(), 40) / 2, screenHeight / 2 + (i * 60), 40, textColor); } +} - // Draw Control keys -} \ No newline at end of file +// State routines for settings and multiplayer lobby + +void ApplyResolution(int width, int height) { + int monitor = GetCurrentMonitor(); + int monitorWidth = GetMonitorWidth(monitor); + int monitorHeight = GetMonitorHeight(monitor); + + if (width >= monitorWidth || height >= monitorHeight) { + SetWindowState(FLAG_WINDOW_MAXIMIZED | FLAG_WINDOW_RESIZABLE); + } + else { + ClearWindowState(FLAG_WINDOW_MAXIMIZED); + SetWindowSize(width, height); + SetWindowPosition(monitorWidth / 2 - width / 2, monitorHeight / 2 - height / 2); + } +} + +void ApplyFramerate(int option) { + if (option == 0) { + ClearWindowState(FLAG_VSYNC_HINT); + SetTargetFPS(60); + } + else if (option == 1) { + ClearWindowState(FLAG_VSYNC_HINT); + SetTargetFPS(144); + } + else if (option == 2) { + SetWindowState(FLAG_VSYNC_HINT); + SetTargetFPS(0); + } +} + +void UpdateSettingsState(GameContext& ctx) { + // Menu navigation + if (IsKeyPressed(KEY_UP)) { + ctx.config.selectedSettingLine--; + if (ctx.config.selectedSettingLine < 0) ctx.config.selectedSettingLine = 5; + } + if (IsKeyPressed(KEY_DOWN)) { + ctx.config.selectedSettingLine++; + if (ctx.config.selectedSettingLine > 5) ctx.config.selectedSettingLine = 0; + } + + // Settings adjustments (LEFT/RIGHT or ENTER) + if (ctx.config.selectedSettingLine == 0) { + if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_ENTER)) { + ctx.config.resolutionOption = (ctx.config.resolutionOption + 1) % 3; + if (ctx.config.resolutionOption == 0) ApplyResolution(1280, 800); + else if (ctx.config.resolutionOption == 1) ApplyResolution(1600, 900); + else if (ctx.config.resolutionOption == 2) ApplyResolution(1920, 1080); + } + else if (IsKeyPressed(KEY_LEFT)) { + ctx.config.resolutionOption = (ctx.config.resolutionOption - 1 + 3) % 3; + if (ctx.config.resolutionOption == 0) ApplyResolution(1280, 800); + else if (ctx.config.resolutionOption == 1) ApplyResolution(1600, 900); + else if (ctx.config.resolutionOption == 2) ApplyResolution(1920, 1080); + } + } + else if (ctx.config.selectedSettingLine == 1) { + if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_ENTER)) { + ctx.config.framerateOption = (ctx.config.framerateOption + 1) % 3; + ApplyFramerate(ctx.config.framerateOption); + } + else if (IsKeyPressed(KEY_LEFT)) { + ctx.config.framerateOption = (ctx.config.framerateOption - 1 + 3) % 3; + ApplyFramerate(ctx.config.framerateOption); + } + } + else if (ctx.config.selectedSettingLine == 2) { + if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_ENTER)) { + ToggleFullscreen(); + ctx.config.isFullscreen = !ctx.config.isFullscreen; + } + } + else if (ctx.config.selectedSettingLine == 3) { + if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_ENTER)) { + ctx.config.maxScoreOption = (ctx.config.maxScoreOption + 1) % 4; + } + else if (IsKeyPressed(KEY_LEFT)) { + ctx.config.maxScoreOption = (ctx.config.maxScoreOption - 1 + 4) % 4; + } + + if (ctx.config.maxScoreOption == 0) ctx.config.maxScore = 5; + else if (ctx.config.maxScoreOption == 1) ctx.config.maxScore = 11; + else if (ctx.config.maxScoreOption == 2) ctx.config.maxScore = 15; + else if (ctx.config.maxScoreOption == 3) ctx.config.maxScore = 21; + } + else if (ctx.config.selectedSettingLine == 4) { + if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_ENTER)) { + ctx.config.sfxEnabled = !ctx.config.sfxEnabled; + } + } + else if (ctx.config.selectedSettingLine == 5) { + if (IsKeyPressed(KEY_ENTER)) { + ctx.currentState = GameState::MainMenu; + } + } +} + +void DrawSettingsState(const GameContext& ctx, int screenWidth, int screenHeight) { + int titleWidth = MeasureText("SETTINGS", 60); + DrawText("SETTINGS", screenWidth / 2 - titleWidth / 2, screenHeight / 4 - 40, 60, WHITE); + + // Option labels formatting + std::string resStr = "Resolution: "; + if (ctx.config.resolutionOption == 0) resStr += "1280x800"; + else if (ctx.config.resolutionOption == 1) resStr += "1600x900"; + else if (ctx.config.resolutionOption == 2) resStr += "1920x1080 (Windowed)"; + + std::string fpsStr = "Framerate: "; + if (ctx.config.framerateOption == 0) fpsStr += "60 FPS"; + else if (ctx.config.framerateOption == 1) fpsStr += "144 FPS"; + else if (ctx.config.framerateOption == 2) fpsStr += "VSync"; + + std::string modeStr = "Screen Mode: "; + if (ctx.config.isFullscreen) modeStr += "Fullscreen"; + else modeStr += "Windowed"; + + std::string scoreLimitStr = "Score Limit: " + std::to_string(ctx.config.maxScore); + + std::string sfxStr = "Sound Effects: "; + if (ctx.config.sfxEnabled) sfxStr += "ON"; + else sfxStr += "OFF"; + + // Menu options highlighting and drawing + Color resColor = (ctx.config.selectedSettingLine == 0) ? YELLOW : WHITE; + Color fpsColor = (ctx.config.selectedSettingLine == 1) ? YELLOW : WHITE; + Color modeColor = (ctx.config.selectedSettingLine == 2) ? YELLOW : WHITE; + Color scoreColor = (ctx.config.selectedSettingLine == 3) ? YELLOW : WHITE; + Color sfxColor = (ctx.config.selectedSettingLine == 4) ? YELLOW : WHITE; + Color backColor = (ctx.config.selectedSettingLine == 5) ? YELLOW : WHITE; + + int startY = screenHeight / 2 - 90; + DrawText(resStr.c_str(), screenWidth / 2 - MeasureText(resStr.c_str(), 30) / 2, startY, 30, resColor); + DrawText(fpsStr.c_str(), screenWidth / 2 - MeasureText(fpsStr.c_str(), 30) / 2, startY + 45, 30, fpsColor); + DrawText(modeStr.c_str(), screenWidth / 2 - MeasureText(modeStr.c_str(), 30) / 2, startY + 90, 30, modeColor); + DrawText(scoreLimitStr.c_str(), screenWidth / 2 - MeasureText(scoreLimitStr.c_str(), 30) / 2, startY + 135, 30, scoreColor); + DrawText(sfxStr.c_str(), screenWidth / 2 - MeasureText(sfxStr.c_str(), 30) / 2, startY + 180, 30, sfxColor); + DrawText("Back", screenWidth / 2 - MeasureText("Back", 30) / 2, startY + 225, 30, backColor); + + 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", + screenWidth / 2 - settingsHintWidth / 2, + screenHeight - 50, 20, WHITE); +} + +void UpdateLobbyState(GameContext& ctx) { + if (IsKeyPressed(KEY_W)) { + ctx.p1Ready = !ctx.p1Ready; + if (ctx.config.sfxEnabled) PlaySound(ctx.paddleHitSound); + } + if (IsKeyPressed(KEY_UP)) { + ctx.p2Ready = !ctx.p2Ready; + if (ctx.config.sfxEnabled) PlaySound(ctx.paddleHitSound); + } + if (IsKeyPressed(KEY_B)) { + ctx.currentState = GameState::MainMenu; + } + + if (ctx.p1Ready && ctx.p2Ready) { + ctx.currentState = GameState::Multiplayer; + } +} + +void DrawLobbyState(const GameContext& ctx, int screenWidth, int screenHeight) { + // Draw background field + DrawCourt(ctx, screenWidth, screenHeight); + + // Render lobby overlay card + int panelWidth = 800; + int panelHeight = 450; + int panelX = screenWidth / 2 - panelWidth / 2; + int panelY = screenHeight / 2 - panelHeight / 2; + + DrawRectangle(panelX, panelY, panelWidth, panelHeight, Color{ 15, 15, 15, 230 }); + DrawRectangleLines(panelX, panelY, panelWidth, panelHeight, Color{ 120, 120, 120, 255 }); + + int titleWidth = MeasureText("MULTIPLAYER LOBBY", 40); + DrawText("MULTIPLAYER LOBBY", screenWidth / 2 - titleWidth / 2, panelY + 30, 40, WHITE); + + // Player 1 section + 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 (ctx.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 section + 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 (ctx.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); + } + + // Ready status message + if (ctx.p1Ready && ctx.p2Ready) { + int startTextWidth = MeasureText("BOTH READY! STARTING GAME...", 24); + DrawText("BOTH READY! STARTING GAME...", screenWidth / 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...", screenWidth / 2 - waitTextWidth / 2, panelY + 315, 20, LIGHTGRAY); + } + + 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", screenWidth / 2 - backTextWidth / 2, panelY + 385, 20, YELLOW); +}