diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b7b093..59a5f2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,87 +1,233 @@ -name: Windows Build and Test +name: Multi-Platform Build and Test on: push: branches: - - main + - '**' pull_request: jobs: - build: + build-windows: runs-on: windows-latest - steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Set Up Python - uses: actions/setup-python@v4 + - name: Setup vcpkg + uses: lukka/run-vcpkg@v11 with: - python-version: "3.x" + vcpkgGitCommitId: '5300a2a461a53b76405db4fcbe6aeb0eea43935d' - - name: Install SDL2 + - name: Cache vcpkg packages + uses: actions/cache@v4 + with: + path: | + build/vcpkg_installed + ${{ env.VCPKG_ROOT }}/installed + ${{ env.VCPKG_ROOT }}/packages + ${{ env.VCPKG_ROOT }}/buildtrees + ${{ env.VCPKG_ROOT }}/downloads + key: vcpkg-windows-${{ hashFiles('vcpkg.json') }}-${{ hashFiles('CMakeLists.txt') }} + restore-keys: | + vcpkg-windows-${{ hashFiles('vcpkg.json') }}- + vcpkg-windows- + + - name: Run cppcheck (static analysis - fail fast) + run: | + if (!(Get-Command cppcheck -ErrorAction SilentlyContinue)) { + choco install cppcheck -y + } + $cppcheck = if (Get-Command cppcheck -ErrorAction SilentlyContinue) { "cppcheck" } else { "C:\Program Files\Cppcheck\cppcheck.exe" } + & $cppcheck --enable=all --inconclusive --std=c++17 --quiet ` + -I include ` + -I "${{ env.VCPKG_ROOT }}/installed/x64-windows/include" ` + src + + - name: Cache CMake build directory + uses: actions/cache@v4 + with: + path: build + key: cmake-windows-${{ hashFiles('**/CMakeLists.txt', 'vcpkg.json') }}-${{ hashFiles('src/**', 'include/**') }} + restore-keys: | + cmake-windows-${{ hashFiles('**/CMakeLists.txt', 'vcpkg.json') }}- + cmake-windows- + + - name: Prepare build.ps1 from build.default.ps1 + shell: pwsh run: | - Invoke-WebRequest -Uri https://github.com/libsdl-org/SDL/releases/download/release-2.32.2/SDL2-devel-2.32.2-VC.zip -OutFile SDL2.zip - Expand-Archive -Path SDL2.zip -DestinationPath C:\SDL2_tmp - Move-Item -Path C:\SDL2_tmp\SDL2-2.32.2\* -Destination C:\SDL2 -Force - if (Test-Path "C:\SDL2\include\SDL.h") { echo "✅ Extraction Successful: SDL.h found" } else { echo "❌ Extraction Failed: SDL.h missing"; exit 1 } + Copy-Item build.default.ps1 build.ps1 + (Get-Content build.ps1) -replace '^\$VCPKG_PATH\s*=.*', '$VCPKG_PATH = "${{ env.VCPKG_ROOT }}"' | Set-Content build.ps1 - - name: Install SDL2_image + - name: Configure CMake for clang-tidy + shell: pwsh run: | - Invoke-WebRequest -Uri https://github.com/libsdl-org/SDL_image/releases/download/release-2.8.8/SDL2_image-devel-2.8.8-VC.zip -OutFile SDL2_image.zip - Expand-Archive -Path SDL2_image.zip -DestinationPath C:\SDL2_image_tmp - Move-Item -Path C:\SDL2_image_tmp\SDL2_image-2.8.8\* -Destination C:\SDL2_image -Force - if (Test-Path "C:\SDL2_image\include\SDL_image.h") { echo "✅ Extraction Successful: SDL_image.h found" } else { echo "❌ Extraction Failed: SDL_image.h missing"; exit 1 } + # Configure CMake to generate compile_commands.json for clang-tidy + cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE="${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Debug - - name: Install SDL2_mixer + - name: Run clang-tidy (static analysis after CMake configure) run: | - Invoke-WebRequest -Uri https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.8.1/SDL2_mixer-devel-2.8.1-VC.zip -OutFile SDL2_mixer.zip - Expand-Archive -Path SDL2_mixer.zip -DestinationPath C:\SDL2_mixer_tmp - Move-Item -Path C:\SDL2_mixer_tmp\SDL2_mixer-2.8.1\* -Destination C:\SDL2_mixer -Force - if (Test-Path "C:\SDL2_mixer\include\SDL_mixer.h") { echo "✅ Extraction Successful: SDL_mixer.h found" } else { echo "❌ Extraction Failed: SDL_mixer.h missing"; exit 1 } + if (!(Get-Command clang-tidy -ErrorAction SilentlyContinue)) { + choco install llvm -y + } + if (Test-Path "build/compile_commands.json") { + clang-tidy -p build $(Get-ChildItem -Path src -Filter *.cpp | ForEach-Object { $_.FullName }) + } else { + Write-Host "No compilation database found, skipping clang-tidy" + } + + - name: Build and Test via Script + shell: pwsh + run: | + # Run the test script which handles build, test execution, and CTest + ./test.ps1 + + - name: Upload Executable Artifact (main branch only) + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: meowstro-windows + path: build/bin/Debug/ - - name: Install SDL2_ttf + build-linux: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Cache APT packages + uses: actions/cache@v4 + with: + path: /var/cache/apt/archives + key: apt-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt') }} + restore-keys: | + apt-${{ runner.os }}- + + - name: Install SDL2 system packages and build tools + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + cmake \ + pkg-config \ + libsdl2-dev \ + libsdl2-image-dev \ + libsdl2-mixer-dev \ + libsdl2-ttf-dev \ + libgtest-dev \ + googletest \ + cppcheck \ + clang-tidy + + - name: Run cppcheck (static analysis - fail fast) run: | - Invoke-WebRequest -Uri https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.24.0/SDL2_ttf-devel-2.24.0-VC.zip -OutFile SDL2_ttf.zip - Expand-Archive -Path SDL2_ttf.zip -DestinationPath C:\SDL2_ttf_tmp - Move-Item -Path C:\SDL2_ttf_tmp\SDL2_ttf-2.24.0\* -Destination C:\SDL2_ttf -Force - if (Test-Path "C:\SDL2_ttf\include\SDL_ttf.h") { echo "✅ Extraction Successful: SDL_ttf.h found" } else { echo "❌ Extraction Failed: SDL_ttf.h missing"; exit 1 } + cppcheck --enable=all --inconclusive --std=c++17 --quiet \ + -I include \ + src - - name: Create config.json from Default + - name: Cache CMake build directory + uses: actions/cache@v4 + with: + path: build + key: cmake-linux-${{ hashFiles('**/CMakeLists.txt') }}-${{ hashFiles('src/**', 'include/**') }} + restore-keys: | + cmake-linux-${{ hashFiles('**/CMakeLists.txt') }}- + cmake-linux- + + - name: Configure CMake (using system packages, NOT vcpkg) run: | - copy config.default.json config.json - python -c "import json; f=open('config.json', 'r+'); d=json.load(f); \ - d.update({'SDL2_DIR': 'C:/SDL2', 'SDL2_IMAGE_DIR': 'C:/SDL2_image', 'SDL2_MIXER_DIR': 'C:/SDL2_mixer', 'SDL2_TTF_DIR': 'C:/SDL2_ttf'}); \ - f.seek(0); json.dump(d, f, indent=4); f.truncate(); f.close()" + # Explicitly avoid vcpkg by not setting CMAKE_TOOLCHAIN_FILE + cmake -B build -S . \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - # - name: Debug Config JSON - # run: type config.json + - name: Run clang-tidy (static analysis after CMake configure) + run: | + if [ -f "build/compile_commands.json" ]; then + find src -name '*.cpp' -exec clang-tidy -p build {} \; + else + echo "No compilation database found, skipping clang-tidy" + fi - - name: Generate CMakePresets.json - run: python generate_cmake_presets.py + - name: Build and Test via Script + run: | + # Make script executable and run it + chmod +x ./test.sh + ./test.sh - # - name: Debug CMakePresets.json - # run: type CMakePresets.json + - name: Upload Executable Artifact (main branch only) + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: meowstro-linux + path: build/bin/Debug/ - - name: Configure CMake - run: cmake --preset my-build + build-macos: + runs-on: macos-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 - # - name: Debug CMake Cache Variables - # run: cmake --preset my-build --debug-output + - name: Cache Homebrew packages + uses: actions/cache@v4 + with: + path: | + ~/Library/Caches/Homebrew + /opt/homebrew/var/homebrew/locks + key: brew-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt') }} + restore-keys: | + brew-${{ runner.os }}- + + - name: Install SDL2 via Homebrew + run: | + brew install \ + sdl2 \ + sdl2_image \ + sdl2_mixer \ + sdl2_ttf \ + googletest \ + cppcheck \ + llvm + + - name: Run cppcheck (static analysis - fail fast) + run: | + cppcheck --enable=all --inconclusive --std=c++17 --quiet \ + -I include \ + src + + - name: Cache CMake build directory + uses: actions/cache@v4 + with: + path: build + key: cmake-macos-${{ hashFiles('**/CMakeLists.txt') }}-${{ hashFiles('src/**', 'include/**') }} + restore-keys: | + cmake-macos-${{ hashFiles('**/CMakeLists.txt') }}- + cmake-macos- - - name: Build Project + - name: Configure CMake (Homebrew packages) run: | - cd "./build" - cmake --build . + HOMEBREW_PREFIX=$(brew --prefix) + cmake -B build -S . \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_PREFIX_PATH="$HOMEBREW_PREFIX" - - name: Copy DLLs to Executable Directory + - name: Run clang-tidy (static analysis after CMake configure) run: | - copy C:\SDL2\lib\x64\SDL2.dll build/bin/Debug/ - copy C:\SDL2_image\lib\x64\SDL2_image.dll build/bin/Debug/ - copy C:\SDL2_mixer\lib\x64\SDL2_mixer.dll build/bin/Debug/ - copy C:\SDL2_ttf\lib\x64\SDL2_ttf.dll build/bin/Debug/ + if [ -f "build/compile_commands.json" ]; then + $(brew --prefix llvm)/bin/clang-tidy -p build $(find src -name '*.cpp') + else + echo "No compilation database found, skipping clang-tidy" + fi - - name: Run Executable (Optional) - working-directory: build/bin/Debug + - name: Build and Test via Script run: | - ./meowstro.exe \ No newline at end of file + # Make script executable and run it + chmod +x ./test.sh + ./test.sh + + - name: Upload Executable Artifact (main branch only) + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: meowstro-macos + path: build/bin/Debug/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 88caf8e..550b983 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,20 @@ build/ .vscode/ # Ignore configuration files that contain user-specific paths -config.json +build.ps1 +build.bat +build.sh CMakePresets.json +config.json # Ignore log files and system files *.log *.tmp *.swp -/.vs \ No newline at end of file +.vs/ + +# Ignore vcpkg installation directory if local +vcpkg_installed/ + +# Ignore compile commands database (it gets copied to root) +compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 36f841c..8ae8eb5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,55 +1,138 @@ cmake_minimum_required(VERSION 3.10) -project(Meowstro VERSION 1.0.0) +project(Meowstro VERSION 2.0.0) -# Set C++ Standard set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) -# Set output path -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) - -# Read JSON Configuration File (if it exists) -if(EXISTS "${CMAKE_SOURCE_DIR}/config.json") - file(READ "${CMAKE_SOURCE_DIR}/config.json" CONFIG_JSON) - - # Extract paths from JSON - string(JSON SDL2_DIR GET ${CONFIG_JSON} SDL2_DIR) - string(JSON SDL2_IMAGE_DIR GET ${CONFIG_JSON} SDL2_IMAGE_DIR) - string(JSON SDL2_MIXER_DIR GET ${CONFIG_JSON} SDL2_MIXER_DIR) - string(JSON SDL2_TTF_DIR GET ${CONFIG_JSON} SDL2_TTF_DIR) +# Enable testing +enable_testing() +include(CTest) + +# Set output directory to bin/Debug or bin/Release depending on build type +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/bin/Debug) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/bin/Release) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # fallback + +# Tested versions - other versions use at your own risk +set(TESTED_SDL2_VERSION "2.30.0") +set(TESTED_SDL2_IMAGE_VERSION "2.8.2") +set(TESTED_SDL2_MIXER_VERSION "2.8.0") +set(TESTED_SDL2_TTF_VERSION "2.22.0") + +# Try vcpkg first, then fall back to system packages +find_package(SDL2 ${TESTED_SDL2_VERSION} CONFIG QUIET) +if(NOT SDL2_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(SDL2 REQUIRED sdl2>=${TESTED_SDL2_VERSION}) + pkg_check_modules(SDL2_IMAGE REQUIRED SDL2_image>=${TESTED_SDL2_IMAGE_VERSION}) + pkg_check_modules(SDL2_MIXER REQUIRED SDL2_mixer>=${TESTED_SDL2_MIXER_VERSION}) + pkg_check_modules(SDL2_TTF REQUIRED SDL2_ttf>=${TESTED_SDL2_TTF_VERSION}) + + message(STATUS "Using system SDL2 packages with minimum versions: SDL2 ${TESTED_SDL2_VERSION}, SDL2_image ${TESTED_SDL2_IMAGE_VERSION}, SDL2_mixer ${TESTED_SDL2_MIXER_VERSION}, SDL2_ttf ${TESTED_SDL2_TTF_VERSION}") +else() + find_package(SDL2_image ${TESTED_SDL2_IMAGE_VERSION} CONFIG REQUIRED) + find_package(SDL2_mixer ${TESTED_SDL2_MIXER_VERSION} CONFIG REQUIRED) + find_package(SDL2_ttf ${TESTED_SDL2_TTF_VERSION} CONFIG REQUIRED) +endif() - # Validate JSON Extraction - if (NOT SDL2_DIR OR NOT SDL2_IMAGE_DIR OR NOT SDL2_MIXER_DIR OR NOT SDL2_TTF_DIR) - message(FATAL_ERROR "Invalid config.json format! Ensure all paths are correctly set.") - endif() +# Find Google Test for unit testing +find_package(GTest CONFIG REQUIRED) - # Set Library and Include Paths - set(SDL2_LIBRARY "${SDL2_DIR}/lib/x64/SDL2.lib") - set(SDL2MAIN_LIBRARY "${SDL2_DIR}/lib/x64/SDL2main.lib") - set(SDL2_INCLUDE_DIR "${SDL2_DIR}/include") +set(SOURCES + src/meowstro.cpp + src/RenderWindow.cpp + src/Entity.cpp + src/Audio.cpp + src/AudioLogic.cpp + src/Font.cpp + src/Sprite.cpp + src/GameStats.cpp + src/ResourceManager.cpp + src/GameConfig.cpp + src/InputHandler.cpp + src/GameStateManager.cpp + src/RhythmGame.cpp + src/MenuSystem.cpp + src/AnimationSystem.cpp + src/Logger.cpp +) - set(SDL2_IMAGE_LIBRARY "${SDL2_IMAGE_DIR}/lib/x64/SDL2_image.lib") - set(SDL2_IMAGE_INCLUDE_DIR "${SDL2_IMAGE_DIR}/include") +set(HEADERS + include/RenderWindow.hpp + include/Entity.hpp + include/SDLTexture.hpp + include/Audio.hpp + include/AudioLogic.hpp + include/Font.hpp + include/Sprite.hpp + include/GameStats.hpp + include/ResourceManager.hpp + include/GameConfig.hpp + include/InputHandler.hpp + include/GameStateManager.hpp + include/RhythmGame.hpp + include/MenuSystem.hpp + include/AnimationSystem.hpp + include/Logger.hpp + include/Exceptions.hpp +) - set(SDL2_MIXER_LIBRARY "${SDL2_MIXER_DIR}/lib/x64/SDL2_mixer.lib") - set(SDL2_MIXER_INCLUDE_DIR "${SDL2_MIXER_DIR}/include") +add_executable(meowstro ${SOURCES} ${HEADERS}) + +# Link libraries based on which method was used +if(TARGET SDL2::SDL2) + # vcpkg targets + target_link_libraries(meowstro + PRIVATE + SDL2::SDL2 + SDL2::SDL2main + SDL2_image::SDL2_image + SDL2_mixer::SDL2_mixer + SDL2_ttf::SDL2_ttf + ) +else() + # System packages + target_link_libraries(meowstro + PRIVATE + ${SDL2_LIBRARIES} + ${SDL2_IMAGE_LIBRARIES} + ${SDL2_MIXER_LIBRARIES} + ${SDL2_TTF_LIBRARIES} + ) + target_include_directories(meowstro + PRIVATE + ${SDL2_INCLUDE_DIRS} + ${SDL2_IMAGE_INCLUDE_DIRS} + ${SDL2_MIXER_INCLUDE_DIRS} + ${SDL2_TTF_INCLUDE_DIRS} + ) +endif() - set(SDL2_TTF_LIBRARY "${SDL2_TTF_DIR}/lib/x64/SDL2_ttf.lib") - set(SDL2_TTF_INCLUDE_DIR "${SDL2_TTF_DIR}/include") +target_include_directories(meowstro PRIVATE include) -else() - message(WARNING "config.json not found! Falling back to find_package().") +# Copy assets folder to the output directory after build +add_custom_command(TARGET meowstro POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_SOURCE_DIR}/assets" + "$/assets" +) - # Find SDL2 Automatically if JSON is missing - find_package(SDL2 REQUIRED) - find_package(SDL2_image REQUIRED) - find_package(SDL2_mixer REQUIRED) - find_package(SDL2_ttf REQUIRED) +# Create a copy of compile_commands.json in the project root for IDE support +if(CMAKE_EXPORT_COMPILE_COMMANDS) + add_custom_target(copy_compile_commands ALL + COMMAND ${CMAKE_COMMAND} -Dsrc="${CMAKE_BINARY_DIR}/compile_commands.json" -Ddst="${CMAKE_SOURCE_DIR}/compile_commands.json" -P "${CMAKE_SOURCE_DIR}/cmake/copy_if_exists.cmake" + BYPRODUCTS ${CMAKE_SOURCE_DIR}/compile_commands.json + DEPENDS ${CMAKE_BINARY_DIR}/compile_commands.json + COMMENT "Copying compile_commands.json to project root if it exists" + VERBATIM + ) endif() -# Define Source Files -set(SOURCES - src/meowstro.cpp +# ==== TESTING CONFIGURATION ==== + +# Create a library target for the game logic (without main.cpp) +# This allows tests to link against game code without the main function +set(GAME_LIB_SOURCES src/RenderWindow.cpp src/Entity.cpp src/Audio.cpp @@ -57,51 +140,92 @@ set(SOURCES src/Font.cpp src/Sprite.cpp src/GameStats.cpp + src/ResourceManager.cpp + src/GameConfig.cpp + src/InputHandler.cpp + src/GameStateManager.cpp + src/RhythmGame.cpp + src/MenuSystem.cpp + src/AnimationSystem.cpp + src/Logger.cpp ) -# Define Header Files -set(HEADERS +set(GAME_LIB_HEADERS include/RenderWindow.hpp include/Entity.hpp + include/SDLTexture.hpp include/Audio.hpp include/AudioLogic.hpp include/Font.hpp include/Sprite.hpp include/GameStats.hpp + include/ResourceManager.hpp + include/GameConfig.hpp + include/InputHandler.hpp + include/GameStateManager.hpp + include/RhythmGame.hpp + include/MenuSystem.hpp + include/AnimationSystem.hpp + include/Logger.hpp + include/Exceptions.hpp ) -# Ensure at least one source file exists -if (NOT SOURCES) - message(FATAL_ERROR "No source files found in src/ directory.") +add_library(meowstro_lib STATIC ${GAME_LIB_SOURCES} ${GAME_LIB_HEADERS}) + +# Link libraries to the game library +if(TARGET SDL2::SDL2) + # vcpkg targets + target_link_libraries(meowstro_lib + PUBLIC + SDL2::SDL2 + SDL2::SDL2main + SDL2_image::SDL2_image + SDL2_mixer::SDL2_mixer + SDL2_ttf::SDL2_ttf + ) +else() + # System packages + target_link_libraries(meowstro_lib + PUBLIC + ${SDL2_LIBRARIES} + ${SDL2_IMAGE_LIBRARIES} + ${SDL2_MIXER_LIBRARIES} + ${SDL2_TTF_LIBRARIES} + ) + target_include_directories(meowstro_lib + PUBLIC + ${SDL2_INCLUDE_DIRS} + ${SDL2_IMAGE_INCLUDE_DIRS} + ${SDL2_MIXER_INCLUDE_DIRS} + ${SDL2_TTF_INCLUDE_DIRS} + ) endif() -# Create the Executable -set(BIN_NAME meowstro) -if(DEFINED ENV{GITHUB_ACTIONS}) - add_definitions(-DCI_BUILD) -endif() -cmake_policy(SET CMP0156 NEW) -add_executable(${BIN_NAME} ${SOURCES} ${HEADERS} "src/AudioLogic.cpp") - -# Include Directories -target_include_directories(${BIN_NAME} PUBLIC include) -target_include_directories(${BIN_NAME} PUBLIC ${SDL2_INCLUDE_DIR} ${SDL2_IMAGE_INCLUDE_DIR} ${SDL2_MIXER_INCLUDE_DIR} ${SDL2_TTF_INCLUDE_DIR}) - -# Ensure SDL2main is linked -target_link_libraries(${BIN_NAME} ${SDL2_LIBRARY} ${SDL2MAIN_LIBRARY} ${SDL2_IMAGE_LIBRARY} ${SDL2_MIXER_LIBRARY} ${SDL2_TTF_LIBRARY}) - -cmake_policy(SET CMP0112 NEW) - -# Move DLLs to the same location as the executable -add_custom_command(TARGET ${BIN_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${SDL2_DIR}/lib/x64/SDL2.dll" - "${SDL2_IMAGE_DIR}/lib/x64/SDL2_image.dll" - "${SDL2_MIXER_DIR}/lib/x64/SDL2_mixer.dll" - "${SDL2_TTF_DIR}/lib/x64/SDL2_ttf.dll" - "$" +target_include_directories(meowstro_lib PUBLIC include) + +# Update main executable to use the library +target_link_libraries(meowstro PRIVATE meowstro_lib) + +# Test executable +add_executable(meowstro_tests + tests/main.cpp + tests/unit/test_GameStats.cpp + tests/unit/test_Logger.cpp + tests/unit/test_GameConfig.cpp + tests/unit/test_Entity.cpp + tests/unit/test_Sprite.cpp + tests/unit/test_ResourceManager.cpp + tests/unit/test_InputHandler.cpp +) + +target_link_libraries(meowstro_tests + PRIVATE + meowstro_lib + GTest::gtest + GTest::gtest_main ) -# Explicitly tell Visual Studio that headers belong to the project -source_group("Header Files" FILES ${HEADERS}) -source_group("Source Files" FILES ${SOURCES}) +target_include_directories(meowstro_tests PRIVATE include) + +# Register tests with CTest +add_test(NAME unit_tests COMMAND meowstro_tests) \ No newline at end of file diff --git a/CMakePresets.default.json b/CMakePresets.default.json deleted file mode 100644 index 2f55c39..0000000 --- a/CMakePresets.default.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": 3, - "cmakeMinimumRequired": { - "major": 3, - "minor": 10 - }, - "configurePresets": [ - { - "name": "my-build", - "description": "Will store the lib and include directories into variables for CMakeLists.txt", - "generator": "Visual Studio 17 2022", - "binaryDir": "${sourceDir}/build", - "cacheVariables": { - "SDL2_INCLUDE_DIR": "path-to-your-SDL2/include", - "SDL2_LIBRARY": "path-to-your-SDL2/lib/x64/SDL2.lib", - "SDL2MAIN_LIBRARY": "path-to-your-SDL2/lib/x64/SDL2main.lib", - "SDL2_IMAGE_INCLUDE_DIR": "path-to-your-SDL2_image/include", - "SDL2_IMAGE_LIBRARY": "path-to-your-SDL2_image/lib/x64/SDL2_image.lib", - "SDL2_MIXER_INCLUDE_DIR": "path-to-your-SDL2_mixer/include", - "SDL2_MIXER_LIBRARY": "path-to-your-SDL2_mixer/lib/x64/SDL2_mixer.lib", - "SDL2_TTF_INCLUDE_DIR": "path-to-your-SDL2_ttf/include", - "SDL2_TTF_LIBRARY": "path-to-your-SDL2_ttf/lib/x64/SDL2_ttf.lib" - } - } - ] -} diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 0000000..54d7bea --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,16 @@ +# Dependency Requirements + +This project has been tested with the following dependencies: + +## Core Dependencies +- CMake >= 3.10 +- C++17 compatible compiler +- SDL2 >= 2.30.0 +- SDL2_image >= 2.8.2 +- SDL2_mixer >= 2.8.0 +- SDL2_ttf >= 2.22.0 + +## Notes +- These are the minimum tested versions +- Newer versions should work fine +- Older versions may work but are not guaranteed \ No newline at end of file diff --git a/LICENSE b/LICENSE index 2be0553..527a61a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 +Copyright (c) 2025 Hugo, Jaime, Jay, Leo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 79eccf3..69fac97 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,118 @@ # Meowstro - SDL2 Game Project ## Overview -This is a game project using SDL2 and its related libraries. The project is configured using CMake and follows a structured build process to make it easy for developers to set up and contribute. +Meowstro is a C++ game project using SDL2 and related libraries. The project uses CMake for building with platform-specific dependency management to optimize setup and development across different systems. ## Prerequisites -Before setting up the project, ensure you have the following installed: -- **Visual Studio 2022** (or any C++ compiler supporting C++17) -- **[CMake](https://cmake.org/download/)** (minimum version 3.10 | I recommend not getting the Release Candidate) -- **(Optional) [Python](https://www.python.org/downloads/)** (Reduces amount of copy pasting SDL paths) - -## Setup Instructions - -### 1. Install Dependencies -Download and extract the following libraries: -- **[SDL2](https://github.com/libsdl-org/SDL/releases/download/release-2.32.2/SDL2-devel-2.32.2-VC.zip)** -- **[SDL2_image](https://github.com/libsdl-org/SDL_image/releases/download/release-2.8.8/SDL2_image-devel-2.8.8-VC.zip)** -- **[SDL2_mixer](https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.8.1/SDL2_mixer-devel-2.8.1-VC.zip)** -- **[SDL2_ttf](https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.24.0/SDL2_ttf-devel-2.24.0-VC.zip)** - -Make sure to note their extracted paths as you will need them for configuration. - -### 2. Configure the Project -Before running CMake, you must configure the project by setting the correct paths in the configuration files: - -#### Editing Configuration Files -Create a copy of the default configuration file `config.default.json` and update it with your correct paths: -- `config.default.json` → `config.json` - -Edit `config.json` and update the paths for SDL2 and its related libraries: -```json -{ - "SDL2_DIR": "path-to-directory/SDL2-2.32.2", - "SDL2_IMAGE_DIR": "path-to-directory/SDL2_image-2.8.8", - "SDL2_MIXER_DIR": "path-to-directory/SDL2_mixer-2.8.1", - "SDL2_TTF_DIR": "path-to-directory/SDL2_ttf-2.24.0" -} -``` -The `CMakePresets.json` will be updated with your path after running the Python Script. +- **C++17 compiler** (Visual Studio 2022, GCC, Clang) +- **[CMake](https://cmake.org/download/)** (version 3.10 or higher) +- **Platform-specific dependencies** (see below and in DEPENDENCIES.md) -The Python File `generate_cmake_presets.py` can be run with the following command: +## Platform-Specific Setup -```sh -python generate_cmake_presets.py -``` +All of my group uses Windows so that's the recommended OS but I will try to make Linux and MacOS work. I made it so those OS get tested in the GitHub Actions too so hopefully everything works with that. -### 3. Creating Files (Skip if You're Not Creating a New File) +### Windows Setup -> ⚠️ **IMPORTANT: YOU MUST DO THIS EVERY TIME YOU CREATE A NEW `.cpp` OR `.hpp` FILE.** -> If you don’t do this, the file will not be included in the project build. You must also rebuild the project after adding new files. +Install [vcpkg](https://github.com/microsoft/vcpkg) and integrate it: -Depending on how you created your new file, you may need to manually move it to the correct directory. Visual Studio, for example, might place new files inside the `./build` directory. +```powershell +git clone https://github.com/Microsoft/vcpkg.git +cd vcpkg +.\bootstrap-vcpkg.bat +.\vcpkg integrate install +``` -If you used **Add Class** or **New Item** in Visual Studio: -- Move the generated `.cpp` files to the `src/` directory. -- Move the generated `.hpp` files to the `include/` directory. +Copy `build.default.bat` to `build.bat` (Command Prompt) or `build.default.ps1` to `build.ps1` (PowerShell), update the vcpkg path, then: -Alternatively, you can just **create the files directly inside the `src/` and `include/` folders** before building. +```cmd +.\build.bat +``` +or +```ps1 +.\build.ps1 +``` -After the files are correctly placed, you must update the `CMakeLists.txt` file so that they are included in the build: +Both scripts will: +- Configure the project using the Visual Studio generator (`-G "Visual Studio 17 2022" -A x64`) +- Use the vcpkg toolchain file +- Build the Debug configuration + +> The output directory is always `build/bin/Debug` or `build/bin/Release` depending on the build type, on all platforms. + +### Ubuntu Linux + +Install SDL2 and development tools from the system package manager: + +```bash +sudo apt-get update +sudo apt-get install -y \ + build-essential \ + cmake \ + pkg-config \ + libsdl2-dev \ + libsdl2-image-dev \ + libsdl2-mixer-dev \ + libsdl2-ttf-dev + +# Note: Requires SDL2 2.30.0+, SDL2_image 2.8.2+, SDL2_mixer 2.8.0+, SDL2_ttf 2.22.0+ +``` -```cmake -# Define Source Files -set(SOURCES - src/file_1.cpp - src/file_2.cpp - ... - src/file_X.cpp -) +Then build directly: +```bash +cmake -B build -S . -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +cmake --build build +``` -# Define Header Files -set(HEADERS - include/file_1.hpp - ... - include/file_X.hpp -) +### macOS + +Homebrew: +```bash +brew install sdl2 sdl2_image sdl2_mixer sdl2_ttf +cmake -B build -S . -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +cmake --build build ``` -### 4. Build the Project +## Build Scripts -First you will want to configure the CMake after configuring files +For convenience, platform-specific build scripts are provided: -#### Creates the Make File -```sh -cmake --preset my-build -``` +- **Windows**: `build.bat` or `build.ps1` (copy from `build.default.bat` and `build.default.ps1`) +- **Linux/macOS**: `build.sh` (copy from `build.default.sh`) -You have two common ways to build and run the project: **Visual Studio** or the **Command Line**. +Personal build scripts contain system-specific paths and are ignored in `.gitignore`. ---- +## Running the Game -#### 🔷 Method 1: Using Visual Studio +After building, run the executable: -1. Open the `.sln` file located inside the `build/` directory. -2. In the **Solution Explorer**, right-click on the project named `meowstro` and select **"Set as Startup Project"**. -3. Press `Ctrl + F5` or click **Debug → Start Without Debugging** to run the project. +- **Debug build (default):** `./build/bin/Debug/meowstro` (add `.exe` on Windows) +- **Release build:** `./build/bin/Release/meowstro` (add `.exe` on Windows) -✅ That’s it! Visual Studio will automatically build and run the project. +> The output directory is always `build/bin/Debug` or `build/bin/Release` depending on the build type, on all platforms. ---- +## Additional Tools Used in GitHub Actions -#### ⚙️ Method 2: Using Command Line +- **cppcheck** +- **clang-tidy** -Make sure you're in the root of the project and then run the following: +## Adding Files -#### Change to the Build Directory -```sh -cd "./build" -``` +Update `CMakeLists.txt` when adding new source files: -#### Build the Project -```sh -cmake --build . -``` +```cmake +set(SOURCES + src/existing_file.cpp + src/new_file.cpp # Add here + # ... +) -#### Run the Built Executable -```sh -./bin/Debug/meowstro.exe +set(HEADERS + include/existing_file.hpp + include/new_file.hpp # Add here + # ... +) ``` ## Directory Structure @@ -126,12 +122,13 @@ cmake --build . │ │-- workflows/ # GitHub Actions workflows │ │ │-- build.yml # Workflow for building the project │ │-- PULL_REQUEST_TEMPLATE.md # Pull request guidelines -|-- aseperite-imgs # Aseperite files to modify image assets +|-- aseperite-imgs # Aseperite files to modify image assets │-- assets/ # Game assets (textures, sounds, etc.) │ │-- audio/ # Audio files │ │-- fonts/ # Font files │ │-- images/ # Image files -│-- build/ # CMake build files (includes build/bin/Debug) +│-- build/ # CMake build files (includes build/bin/Debug and build/bin/Release) +|-- cmake/ # copy_if_exists.cmake location │-- docs/ # Project documentation │ │-- design_notes.md │ │-- how_to_play.md @@ -139,17 +136,13 @@ cmake --build . │-- src/ # Source files │-- .gitattributes # Git attributes file │-- .gitignore # Git ignore file +│-- build.default.bat # Windows batch default build script +│-- build.default.ps1 # Windows PowerShell default build script +│-- build.default.sh # macOS/Linux default build script │-- CMakeLists.txt # Main CMake build script -│-- CMakePresets.default.json # Default CMake preset -│-- CMakePresets.json # Active CMake preset configuration (created by py script) -│-- config.default.json # Default configuration file (rename to use) -│-- config.json # Active configuration file -|-- generate_cmake_presets.py # Generates the CMakePresets.json based on config.json +|-- DEPENDENCIES.md # List of dependencies/versions used │-- LICENSE # Project LICENSE │-- README.md # Project README -``` +|-- vcpkg.json # JSON file w/ dependencies & versions used in vcpkg install -## Contribution Guidelines -- **DON'T** commit `config.json` or `CMakePresets.json`. These files contain personal paths should be local. -- If modifying build configuration, ensure compatibility with Windows. -- Follow C++ best practices and maintain code clarity. +``` \ No newline at end of file diff --git a/TEST_RUNNERS.md b/TEST_RUNNERS.md new file mode 100644 index 0000000..ecf9877 --- /dev/null +++ b/TEST_RUNNERS.md @@ -0,0 +1,56 @@ +# Test Runner Scripts + +Quick reference for running tests in Meowstro. + +## Windows + +### Option 1: Batch File (Recommended) +```cmd +test.bat +``` + +### Option 2: PowerShell +```powershell +.\test.ps1 +``` + +## Linux/macOS + +### Shell Script +```bash +./test.sh +``` + +## What the Scripts Do + +1. **Build the project** (including tests) +2. **Run Google Test executable** with detailed output +3. **Show CTest summary** for CI compatibility +4. **Display colored status** (✅ pass / ❌ fail) +5. **Exit with proper codes** for CI integration + +## Manual Commands + +If you prefer to run commands manually: + +```bash +# Build everything +cmake --build build --config Debug + +# Run tests directly (detailed output) +./build/bin/Debug/meowstro_tests.exe + +# Run tests with CTest (CI-style) +cd build && ctest --output-on-failure -C Debug + +# Run specific test patterns +./build/bin/Debug/meowstro_tests.exe --gtest_filter="GameStatsTest.*" +``` + +## Adding New Tests + +1. Create new test files in `tests/unit/`, `tests/component/`, etc. +2. Add the test file to `CMakeLists.txt` in the `meowstro_tests` target +3. Rebuild and run tests + +The test runners will automatically pick up new tests! \ No newline at end of file diff --git a/assets/audio/March.wav b/assets/audio/March.wav deleted file mode 100644 index 46a72b7..0000000 Binary files a/assets/audio/March.wav and /dev/null differ diff --git a/assets/audio/fake.mp4 b/assets/audio/fake.mp4 deleted file mode 100644 index e69de29..0000000 diff --git a/assets/audio/mymarch.mp3 b/assets/audio/mymarch.mp3 deleted file mode 100644 index 065a1ec..0000000 Binary files a/assets/audio/mymarch.mp3 and /dev/null differ diff --git a/assets/audio/song_for_meowstro.mp3 b/assets/audio/song_for_meowstro.mp3 deleted file mode 100644 index 07ca6fa..0000000 Binary files a/assets/audio/song_for_meowstro.mp3 and /dev/null differ diff --git a/assets/fonts/fake.ttf b/assets/fonts/fake.ttf deleted file mode 100644 index e69de29..0000000 diff --git a/build.default.bat b/build.default.bat new file mode 100644 index 0000000..580790b --- /dev/null +++ b/build.default.bat @@ -0,0 +1,10 @@ +@echo off +REM build.default.bat +REM Edit VCPKG_PATH to match your path +set VCPKG_PATH=C:/path/to/vcpkg + +REM Only run this if you add/change dependencies or set up vcpkg for the first time: +REM "%VCPKG_PATH%/vcpkg.exe" install sdl2 sdl2-image sdl2-mixer[mpg123] sdl2-ttf --triplet=x64-windows + +cmake -B build -S . -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="%VCPKG_PATH%/scripts/buildsystems/vcpkg.cmake" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +cmake --build build --config Debug \ No newline at end of file diff --git a/build.default.ps1 b/build.default.ps1 new file mode 100644 index 0000000..15c64e4 --- /dev/null +++ b/build.default.ps1 @@ -0,0 +1,9 @@ +<# ===== build.default.ps1 ===== #> +# Edit VCPKG_PATH to match your path +$VCPKG_PATH = "C:/path/to/vcpkg" + +# Only run this if you add/change dependencies or set up vcpkg for the first time: +# & "$VCPKG_PATH/vcpkg.exe" install sdl2 sdl2-image sdl2-mixer[mpg123] sdl2-ttf --triplet=x64-windows + +cmake -B build -S . -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="$VCPKG_PATH/scripts/buildsystems/vcpkg.cmake" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +cmake --build build --config Debug \ No newline at end of file diff --git a/build.default.sh b/build.default.sh new file mode 100644 index 0000000..d2c1389 --- /dev/null +++ b/build.default.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# build.default.sh - Cross-platform build script + +# Detect platform and use appropriate method +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + echo "Linux detected - using system packages (recommended)" + echo "Make sure SDL2 development packages are installed:" + echo "sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev" + echo "" + + # Build without vcpkg - CMake will find system packages + cmake -B build -S . -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Debug + +elif [[ "$OSTYPE" == "darwin"* ]]; then + echo "macOS detected - checking for Homebrew SDL2..." + + if brew list sdl2 &>/dev/null; then + echo "Using Homebrew SDL2 packages" + cmake -B build -S . -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Debug + else + echo "SDL2 not found via Homebrew. Install with:" + echo "brew install sdl2 sdl2_image sdl2_mixer sdl2_ttf" + echo "" + echo "Or use vcpkg by setting VCPKG_PATH below and uncommenting vcpkg section" + # Uncomment and set path if using vcpkg: + # VCPKG_PATH="$HOME/vcpkg" + # cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE="$VCPKG_PATH/scripts/buildsystems/vcpkg.cmake" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + exit 1 + fi + +else + echo "Unknown platform, trying vcpkg method..." + echo "Edit VCPKG_PATH below to match your vcpkg installation:" + VCPKG_PATH="$HOME/vcpkg" + cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE="$VCPKG_PATH/scripts/buildsystems/vcpkg.cmake" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Debug +fi + +# Build the project +if [ $? -eq 0 ]; then + echo "Configuration successful, building..." + cmake --build build + + if [ $? -eq 0 ]; then + echo "Build completed successfully!" + echo "Executable: build/bin/Debug/meowstro" + echo "Compile commands: build/compile_commands.json" + else + echo "Build failed!" + exit 1 + fi +else + echo "CMake configuration failed!" + exit 1 +fi \ No newline at end of file diff --git a/cmake/copy_if_exists.cmake b/cmake/copy_if_exists.cmake new file mode 100644 index 0000000..e61d2a1 --- /dev/null +++ b/cmake/copy_if_exists.cmake @@ -0,0 +1,3 @@ +if(EXISTS "${src}") + file(COPY "${src}" DESTINATION "${dst}" FILE_PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ) +endif() diff --git a/config.default.json b/config.default.json deleted file mode 100644 index 9f2cef2..0000000 --- a/config.default.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "SDL2_DIR": "path-to-directory/SDL2-2.32.2", - "SDL2_IMAGE_DIR": "path-to-directory/SDL2_image-2.8.8", - "SDL2_MIXER_DIR": "path-to-directory/SDL2_mixer-2.8.1", - "SDL2_TTF_DIR": "path-to-directory/SDL2_ttf-2.24.0" -} \ No newline at end of file diff --git a/generate_cmake_presets.py b/generate_cmake_presets.py deleted file mode 100644 index 955591d..0000000 --- a/generate_cmake_presets.py +++ /dev/null @@ -1,46 +0,0 @@ -import json -import os - -# Paths to JSON files -config_path = "config.json" -presets_template_path = "CMakePresets.default.json" -output_path = "CMakePresets.json" - -# Ensure config.json exists -if not os.path.exists(config_path): - print(f"Error: {config_path} not found. Please create it from config.default.json and set the correct paths.") - exit(1) - -# Read config.json -with open(config_path, "r") as file: - config = json.load(file) - -# Ensure CMakePresets.default.json exists -if not os.path.exists(presets_template_path): - print(f"Error: {presets_template_path} not found.") - exit(1) - -# Read CMakePresets.default.json -with open(presets_template_path, "r") as file: - presets = json.load(file) - -# Modify cacheVariables with paths from config.json -preset = presets["configurePresets"][0]["cacheVariables"] -preset["SDL2_INCLUDE_DIR"] = f"{config['SDL2_DIR']}/include" -preset["SDL2_LIBRARY"] = f"{config['SDL2_DIR']}/lib/x64/SDL2.lib" -preset["SDL2MAIN_LIBRARY"] = f"{config['SDL2_DIR']}/lib/x64/SDL2main.lib" - -preset["SDL2_IMAGE_INCLUDE_DIR"] = f"{config['SDL2_IMAGE_DIR']}/include" -preset["SDL2_IMAGE_LIBRARY"] = f"{config['SDL2_IMAGE_DIR']}/lib/x64/SDL2_image.lib" - -preset["SDL2_MIXER_INCLUDE_DIR"] = f"{config['SDL2_MIXER_DIR']}/include" -preset["SDL2_MIXER_LIBRARY"] = f"{config['SDL2_MIXER_DIR']}/lib/x64/SDL2_mixer.lib" - -preset["SDL2_TTF_INCLUDE_DIR"] = f"{config['SDL2_TTF_DIR']}/include" -preset["SDL2_TTF_LIBRARY"] = f"{config['SDL2_TTF_DIR']}/lib/x64/SDL2_ttf.lib" - -# Write updated CMakePresets.json -with open(output_path, "w") as file: - json.dump(presets, file, indent=4) - -print(f"Generated {output_path} successfully.") diff --git a/include/AnimationSystem.hpp b/include/AnimationSystem.hpp new file mode 100644 index 0000000..5afa442 --- /dev/null +++ b/include/AnimationSystem.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "Sprite.hpp" +#include "Entity.hpp" + +#include +#include +#include + +// Animation state for hook throwing +struct HookAnimationState { + bool isThrowing; + bool isReturning; + Uint32 throwStartTime; + int throwDuration; + int hookStartX; + int hookStartY; + int hookTargetX; + int hookTargetY; + + HookAnimationState() + : isThrowing(false), isReturning(false), throwStartTime(0) + , throwDuration(0), hookStartX(0), hookStartY(0) + , hookTargetX(0), hookTargetY(0) {} +}; + +// Animation state for fisher +struct FisherAnimationState { + bool thrown; + int thrownTimer; + + FisherAnimationState() : thrown(false), thrownTimer(2) {} +}; + +class AnimationSystem { +public: + AnimationSystem(); + ~AnimationSystem() = default; + + // Initialize animation system with timing + void initialize(); + + // Update animation timing (call once per frame) + void updateTiming(); + + // Hook throwing animation + void startHookThrow(Sprite& hook, int handX, int handY, int throwDuration); + void updateHookAnimation(Sprite& hook, HookAnimationState& state); + bool isHookThrowing(const HookAnimationState& state) const; + + // Fisher animation + void startFisherThrow(FisherAnimationState& state); + void updateFisherAnimation(Sprite& fisher, FisherAnimationState& state); + + // Sway effects for sprites + void updateSwayEffects(std::vector& fish, + const std::vector>& fishBasePositions); + void updateSwayEffects(Sprite& sprite, const std::pair& basePosition); + + // Specialized sway update for hook (only when not throwing) + void updateHookSway(Sprite& hook, const std::pair& basePosition, + const HookAnimationState& hookState); + + // Fish animation frames + void updateFishFrames(std::vector& fish, int numBeats); + + // Get current time counter for external use if needed + float getTimeCounter() const { return m_timeCounter; } + +private: + float m_timeCounter; + + // Helper methods for sway calculations + int calculateSway(float timeOffset = 0.0f) const; + int calculateBob(float timeOffset = 0.0f) const; +}; \ No newline at end of file diff --git a/include/Audio.hpp b/include/Audio.hpp index 8d8b543..aa0e5dc 100644 --- a/include/Audio.hpp +++ b/include/Audio.hpp @@ -10,5 +10,10 @@ class Audio { ~Audio(); void playBackgroundMusic(const std::string& filePath); void stopBackgroundMusic(); + bool isValid() const { return m_valid; } + +private: + Mix_Music* bgMusic; + bool m_valid; }; diff --git a/include/Entity.hpp b/include/Entity.hpp index ce5df9a..126f0fe 100644 --- a/include/Entity.hpp +++ b/include/Entity.hpp @@ -1,10 +1,15 @@ #pragma once #include #include +#include "SDLTexture.hpp" +#include class Entity { public: + // Constructor that takes shared ownership of texture + Entity(float x, float y, SharedSDLTexture texture); + // Constructor for raw SDL_Texture* (creates shared ownership) Entity(float x, float y, SDL_Texture* texture); inline float getX() const { @@ -14,28 +19,41 @@ class Entity { return y; } - inline SDL_Texture *getTexture() + // Get raw texture pointer for SDL API calls + inline SDL_Texture* getTexture() const { - return texture; + return texture_ ? texture_->get() : nullptr; + } + // Get shared texture for ownership transfer + inline SharedSDLTexture getSharedTexture() const + { + return texture_; } SDL_Rect getCurrentFrame(); void setCurrentFrameW(int w); void setCurrentFrameH(int h); + // Set texture with shared ownership + inline void setTexture(SharedSDLTexture texture) + { + texture_ = texture; + } + // Set texture from raw pointer (creates shared ownership) inline void setTexture(SDL_Texture* texture) { - this->texture = texture; + texture_ = texture ? makeSharedSDLTexture(texture) : nullptr; } protected: - inline void setX(int x) + // Fixed type consistency - use float to match member variables + inline void setX(float x) { this->x = x; } - inline void setY(int y) + inline void setY(float y) { this->y = y; } float x; float y; SDL_Rect currentFrame; - SDL_Texture* texture; + SharedSDLTexture texture_; }; diff --git a/include/Exceptions.hpp b/include/Exceptions.hpp new file mode 100644 index 0000000..d57b808 --- /dev/null +++ b/include/Exceptions.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +class InitializationException : public std::runtime_error { +public: + explicit InitializationException(const std::string& message) + : std::runtime_error("Initialization failed: " + message) {} +}; + +class ResourceLoadException : public std::runtime_error { +public: + explicit ResourceLoadException(const std::string& message) + : std::runtime_error("Resource load failed: " + message) {} +}; + +class InvalidStateException : public std::logic_error { +public: + explicit InvalidStateException(const std::string& message) + : std::logic_error("Invalid state: " + message) {} +}; \ No newline at end of file diff --git a/include/Font.hpp b/include/Font.hpp index b356e06..12993f0 100644 --- a/include/Font.hpp +++ b/include/Font.hpp @@ -5,28 +5,21 @@ Description: Font class, handles the logic when rendering a true type font text */ #ifndef FONT_H #define FONT_H -/* -It is very important to note that I didn't download the necessary library (ttf) in order to work. -Nor have I downloaded the font that we decided to use for the project, that being Comic Sans. -As I was literally passing out while writing this because it was so late and rarely have time. -This also explains why there is no driver file to test, again, passing out, so hypothetically this should work. -*/ -#include // Not sure if this needs to be .hpp, again, verge of passing out + +#include #include #include -using namespace std; - class Font { public: Font(); // Basic Constructor - ~Font(); // Destructor (fancy) - bool load(const string& fontPath, int fontSz); // Allows us to add a font file + ~Font(); // Destructor + bool load(const std::string& fontPath, int fontSz); // Allows us to add a font file void unload(); // Clean up (or unload) the font // This allows us to make the switch from text (ttf or any font file) to texture (SDL) - SDL_Texture* renderText(SDL_Renderer* renderer, const string& txt, SDL_Color color); + SDL_Texture* renderText(SDL_Renderer* renderer, const std::string& txt, SDL_Color color); private: TTF_Font* font; // Internal pointer, which points to the loaded font }; diff --git a/include/GameConfig.hpp b/include/GameConfig.hpp new file mode 100644 index 0000000..da70f49 --- /dev/null +++ b/include/GameConfig.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include + +class GameConfig +{ +public: + // Singleton pattern for global configuration access + static GameConfig& getInstance(); + + // Game window settings + struct WindowConfig { + const char *title = "Meowstro"; + int width = 1920; + int height = 1080; + Uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE; + }; + + // Audio and timing settings + struct AudioConfig { + int bpm = 147; + double travelDuration = 2000.0; // ms before beat to start moving + std::string backgroundMusicPath = "./assets/audio/meowstro_short_ver.mp3"; + }; + + // Visual settings + struct VisualConfig { + const SDL_Color YELLOW = { 255, 255, 100, 255 }; + const SDL_Color BLACK = { 0, 0, 0, 255 }; + const SDL_Color RED = { 255, 0, 0, 255 }; + int frameDelay = 75; // SDL_Delay value + }; + + // Asset paths + struct AssetPaths { + std::string fontPath = "./assets/fonts/Comic Sans MS.ttf"; + + // Image paths + std::string oceanTexture = "./assets/images/Ocean.png"; + std::string boatTexture = "./assets/images/boat.png"; + std::string fisherTexture = "./assets/images/fisher.png"; + std::string hookTexture = "./assets/images/hook.png"; + std::string menuCatTexture = "./assets/images/menu_cat.png"; + std::string selectCatTexture = "./assets/images/select_cat.png"; + + // Fish textures + std::string blueFishTexture = "./assets/images/blue_fish.png"; + std::string greenFishTexture = "./assets/images/green_fish.png"; + std::string goldFishTexture = "./assets/images/gold_fish.png"; + }; + + // Game mechanics + struct GameplayConfig { + int numBeats = 25; + int numFishTextures = 3; + int throwDuration = 200; // hook animation duration + int hookTargetX = 650; + int hookTargetY = 625; + int fishTargetX = 660; + + // Fish spawn locations + std::vector fishStartXLocations = { + 1352, 2350, 2465, 2800, 3145, 3330, 3480, 3663, 4175, 4560, + 4816, 5245, 6059, 6260, 6644, 6885, 7100, 7545, 7801, 8230, + 8775, 9145, 9531, 9829, 10160 + }; + + // Beat timing data - will be initialized from AudioLogic conversion + std::vector noteBeats; + }; + + // Font sizes + struct FontSizes { + int menuLogo = 75; + int menuButtons = 55; + int quitButton = 65; + int gameScore = 40; + int gameNumbers = 35; + int gameStats = 55; + int hitFeedback = 30; + }; + + // Initialization method for beat timings + void initializeBeatTimings(); + + // Getter methods + const WindowConfig& getWindowConfig() const { return windowConfig; } + const AudioConfig& getAudioConfig() const { return audioConfig; } + const VisualConfig& getVisualConfig() const { return visualConfig; } + const AssetPaths& getAssetPaths() const { return assetPaths; } + const GameplayConfig& getGameplayConfig() const { return gameplayConfig; } + const FontSizes& getFontSizes() const { return fontSizes; } + +private: + GameConfig() = default; + ~GameConfig() = default; + GameConfig(const GameConfig&) = delete; + GameConfig& operator=(const GameConfig&) = delete; + + WindowConfig windowConfig; + AudioConfig audioConfig; + VisualConfig visualConfig; + AssetPaths assetPaths; + GameplayConfig gameplayConfig; + FontSizes fontSizes; +}; \ No newline at end of file diff --git a/include/GameStateManager.hpp b/include/GameStateManager.hpp new file mode 100644 index 0000000..f516097 --- /dev/null +++ b/include/GameStateManager.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "InputHandler.hpp" +#include "GameStats.hpp" +#include "RhythmGame.hpp" +#include "MenuSystem.hpp" + +#include + +// Forward declarations +class RenderWindow; +class ResourceManager; + +class GameStateManager +{ +public: + GameStateManager(RenderWindow& window, ResourceManager& resourceManager, InputHandler& inputHandler); + ~GameStateManager() = default; + + // Main game loop - runs until quit + void run(); + + // Get current state + GameState getCurrentState() const { return currentState; } + + // Check if game should continue running + bool isRunning() const { return currentState != GameState::Quit; } + +private: + // State management + GameState currentState; + GameState nextState; + + // Game systems + RenderWindow& window; + ResourceManager& resourceManager; + InputHandler& inputHandler; + GameStats gameStats; + SDL_Event event; + RhythmGame rhythmGame; + MenuSystem menuSystem; + + // State transition methods + void transitionTo(GameState newState); + void updateState(); + + // State execution methods + void runMainMenu(); + void runGameplay(); + void runEndScreen(); + + // Helper methods + void resetGameStats(); +}; \ No newline at end of file diff --git a/include/GameStats.hpp b/include/GameStats.hpp index cfcb921..b6d69e3 100644 --- a/include/GameStats.hpp +++ b/include/GameStats.hpp @@ -2,7 +2,6 @@ //Desc: .hpp file for displaying game statistics #pragma once #include -using namespace std; class GameStats { @@ -24,7 +23,7 @@ class GameStats void increaseScore(int score); void resetStats(); //others - friend ostream& operator << (ostream& out, const GameStats& s); + friend std::ostream& operator << (std::ostream& out, const GameStats& s); GameStats operator++ (int); //adds to hits and score GameStats operator-- (int); //adds to misses private: diff --git a/include/InputHandler.hpp b/include/InputHandler.hpp new file mode 100644 index 0000000..75cdb86 --- /dev/null +++ b/include/InputHandler.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include + +enum class InputAction { + None, + Quit, + Select, // SPACE key + MenuUp, // UP arrow (menu navigation) + MenuDown, // DOWN arrow (menu navigation) + Escape // ESC key +}; + +enum class GameState { + MainMenu, + Playing, + EndScreen, + Quit +}; + +class InputHandler +{ +public: + InputHandler(); + ~InputHandler() = default; + + // Process SDL events and return the appropriate action + InputAction processInput(SDL_Event& event, GameState currentState); + + // Check if a key is currently pressed (for game state) + bool isKeyPressed(SDL_Scancode key) const; + + // Check if space key is being held down (for rhythm game timing) + bool isSpaceHeld() const; + +private: + const Uint8* keyboardState; + bool spaceKeyDown; // Track space key state for rhythm timing + + // Helper methods for different game states + InputAction processMenuInput(const SDL_Event& event); + InputAction processGameInput(const SDL_Event& event); + InputAction processEndScreenInput(const SDL_Event& event); +}; \ No newline at end of file diff --git a/include/Logger.hpp b/include/Logger.hpp new file mode 100644 index 0000000..e52d511 --- /dev/null +++ b/include/Logger.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +enum class LogLevel { + ERROR, + WARNING, + INFO, + DEBUG +}; + +class Logger { +public: + static void log(LogLevel level, const std::string& message); + static void logSDLError(LogLevel level, const std::string& context); + static void logSDLImageError(LogLevel level, const std::string& context); + static void logSDLTTFError(LogLevel level, const std::string& context); + static void logSDLMixerError(LogLevel level, const std::string& context); + + // Convenience methods + static void error(const std::string& message) { log(LogLevel::ERROR, message); } + static void warning(const std::string& message) { log(LogLevel::WARNING, message); } + static void info(const std::string& message) { log(LogLevel::INFO, message); } + static void debug(const std::string& message) { log(LogLevel::DEBUG, message); } + + // Template method for objects with operator<< overloaded + template + static void logObject(LogLevel level, const T& obj) { + std::ostringstream oss; + oss << obj; + log(level, oss.str()); + } + +private: + static std::string levelToString(LogLevel level); + static std::ostream& getOutputStream(LogLevel level); +}; \ No newline at end of file diff --git a/include/MenuSystem.hpp b/include/MenuSystem.hpp new file mode 100644 index 0000000..97bf614 --- /dev/null +++ b/include/MenuSystem.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "RenderWindow.hpp" +#include "ResourceManager.hpp" +#include "InputHandler.hpp" +#include "GameStats.hpp" +#include "Entity.hpp" +#include "Sprite.hpp" + +#include + +// Menu result types for different menu outcomes +enum class MenuResult { + None, // Still in menu + StartGame, // Start new game + RetryGame, // Retry current game + QuitGame, // Quit to desktop + GoToMainMenu // Return to main menu +}; + +// Menu types for future extensibility +enum class MenuType { + MainMenu, + EndScreen, + PauseMenu, // Future menu + SettingsMenu, // Future menu + CreditsMenu // Future menu +}; + +class MenuSystem { +public: + MenuSystem(); + ~MenuSystem() = default; + + // Main menu interface + MenuResult runMainMenu(RenderWindow& window, ResourceManager& resourceManager, InputHandler& inputHandler); + + // End screen interface + MenuResult runEndScreen(RenderWindow& window, ResourceManager& resourceManager, GameStats& stats, InputHandler& inputHandler); + + // Future extensibility methods + MenuResult runPauseMenu(RenderWindow& window, ResourceManager& resourceManager, InputHandler& inputHandler); + MenuResult runSettingsMenu(RenderWindow& window, ResourceManager& resourceManager, InputHandler& inputHandler); + +private: + // Current menu state + MenuType currentMenuType; + int currentOption; + bool menuActive; + + // Helper methods for menu management + void resetMenuState(MenuType type); + void handleMenuNavigation(InputAction action, int maxOptions); + + // Rendering helpers + void renderMainMenuContent(RenderWindow& window, ResourceManager& resourceManager); + void renderEndScreenContent(RenderWindow& window, ResourceManager& resourceManager, GameStats& stats); + + // Menu option management + void updateSelectorPosition(Sprite& selector, MenuType menuType); + + // Utility for score formatting (moved from meowstro.cpp) + std::string formatScore(int score); +}; \ No newline at end of file diff --git a/include/RenderWindow.hpp b/include/RenderWindow.hpp index d89448b..28ec307 100644 --- a/include/RenderWindow.hpp +++ b/include/RenderWindow.hpp @@ -1,13 +1,11 @@ #pragma once #include -#include #include "Entity.hpp" class RenderWindow { public: RenderWindow(const char *title, int w, int h, Uint32 windowFlags = SDL_WINDOW_SHOWN); - SDL_Texture *loadTexture(const char *filePath); void clear(); void render(Entity& entity); void display(); @@ -17,9 +15,13 @@ class RenderWindow { return renderer; } + + bool isValid() const { return m_valid; } + private: SDL_Window *window; SDL_Renderer *renderer; + bool m_valid; }; diff --git a/include/ResourceManager.hpp b/include/ResourceManager.hpp new file mode 100644 index 0000000..80f0d1c --- /dev/null +++ b/include/ResourceManager.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "Font.hpp" + +class ResourceManager { +public: + ResourceManager(SDL_Renderer* renderer); + ~ResourceManager(); + + // Texture management + SDL_Texture* loadTexture(const std::string& filePath); + SDL_Texture* createTextTexture(const std::string& fontPath, int fontSize, const std::string& text, SDL_Color color); + + // Font management + Font* getFont(const std::string& fontPath, int fontSize); + + // Manual cleanup (called automatically in destructor) + void cleanup(); + + // Validity checking + bool isValid() const { return m_valid; } + +private: + SDL_Renderer* renderer; + std::unordered_map textures; + std::unordered_map> fonts; + bool m_valid; + + // Helper to generate unique keys + std::string generateFontKey(const std::string& fontPath, int fontSize) const; + std::string generateTextKey(const std::string& fontPath, int fontSize, const std::string& text, SDL_Color color) const; +}; \ No newline at end of file diff --git a/include/RhythmGame.hpp b/include/RhythmGame.hpp new file mode 100644 index 0000000..d5ea489 --- /dev/null +++ b/include/RhythmGame.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include "RenderWindow.hpp" +#include "ResourceManager.hpp" +#include "GameStats.hpp" +#include "InputHandler.hpp" +#include "Entity.hpp" +#include "Sprite.hpp" +#include "Audio.hpp" +#include "AudioLogic.hpp" +#include "AnimationSystem.hpp" + +#include +#include +#include +#include + + +class RhythmGame { +public: + RhythmGame(); + ~RhythmGame(); + + // Initialize the game with required dependencies + void initialize(RenderWindow& window, ResourceManager& resourceManager, GameStats& stats); + + // Main game update - returns true if game should continue, false if ended + bool update(InputAction action, InputHandler& inputHandler); + + // Render the game + void render(RenderWindow& window); + + // Check if game is over (music stopped) + bool isGameOver(bool exitEarly = false) const; + + // Clean up resources when exiting gameplay + void cleanup(); + +private: + // Game dependencies + ResourceManager* m_resourceManager; + GameStats* m_gameStats; + + // Audio system + Audio m_audioPlayer; + AudioLogic m_rhythmLogic; + + // Animation system + AnimationSystem m_animationSystem; + HookAnimationState m_hookAnimationState; + FisherAnimationState m_fisherAnimationState; + + // Game timing + Uint32 m_songStartTime; + std::vector m_noteHitFlags; + + // Game entities + Entity m_ocean; + Entity m_scoreLabel; + Entity m_scoreNumber; + Sprite m_fisher; + Sprite m_boat; + Sprite m_hook; + std::vector m_fish; + + // Base positions for sway effect (to avoid accumulating position changes) + std::vector> m_fishBasePositions; + std::pair m_fisherBasePosition; + std::pair m_boatBasePosition; + std::pair m_hookBasePosition; + + // Hit tracking + std::unordered_set m_fishHits; + std::unordered_map m_fishHitTimes; + std::unordered_map m_fishHitTypes; // false = Good, true = Perfect + + // Animation parameters + int m_throwDuration; + int m_hookTargetX; + int m_hookTargetY; + + // Textures + SDL_Texture* m_fishTextures[3]; + SDL_Texture* m_perfectHitTexture; + SDL_Texture* m_goodHitTexture; + + // Last score for texture updating + int m_lastScore; + + // Private helper methods + void initializeTextures(); + void initializeEntities(); + void initializeFish(); + void handleRhythmInput(double currentTime); + void updateAnimations(); + void updateFishMovement(); + void checkMissedNotes(double currentTime); + void renderFish(RenderWindow& window, Uint32 currentTicks); + void updateScore(); + + // Format score helper + std::string formatScore(int score); +}; \ No newline at end of file diff --git a/include/SDLTexture.hpp b/include/SDLTexture.hpp new file mode 100644 index 0000000..03f1705 --- /dev/null +++ b/include/SDLTexture.hpp @@ -0,0 +1,79 @@ +#pragma once +#include +#include + +// RAII wrapper for SDL_Texture with proper ownership semantics +class SDLTexture { +public: + // Constructor that takes ownership of an SDL_Texture + explicit SDLTexture(SDL_Texture* texture = nullptr) : texture_(texture) {} + + // Destructor automatically cleans up the texture + ~SDLTexture() { + if (texture_) { + SDL_DestroyTexture(texture_); + } + } + + // Move constructor + SDLTexture(SDLTexture&& other) noexcept : texture_(other.texture_) { + other.texture_ = nullptr; + } + + // Move assignment operator + SDLTexture& operator=(SDLTexture&& other) noexcept { + if (this != &other) { + if (texture_) { + SDL_DestroyTexture(texture_); + } + texture_ = other.texture_; + other.texture_ = nullptr; + } + return *this; + } + + // Disable copy constructor and copy assignment (non-copyable resource) + SDLTexture(const SDLTexture&) = delete; + SDLTexture& operator=(const SDLTexture&) = delete; + + // Get raw pointer for SDL API calls + SDL_Texture* get() const noexcept { return texture_; } + + // Release ownership and return raw pointer + SDL_Texture* release() noexcept { + SDL_Texture* temp = texture_; + texture_ = nullptr; + return temp; + } + + // Reset with new texture (destroys current texture if it exists) + void reset(SDL_Texture* texture = nullptr) { + if (texture_) { + SDL_DestroyTexture(texture_); + } + texture_ = texture; + } + + // Check if texture is valid + explicit operator bool() const noexcept { return texture_ != nullptr; } + + // Get texture dimensions safely + bool getSize(int& width, int& height) const { + if (!texture_) { + width = height = 0; + return false; + } + return SDL_QueryTexture(texture_, nullptr, nullptr, &width, &height) == 0; + } + +private: + SDL_Texture* texture_; +}; + +// Type alias for shared ownership scenarios +using SharedSDLTexture = std::shared_ptr; + +// Helper function to create shared SDL texture +inline SharedSDLTexture makeSharedSDLTexture(SDL_Texture* texture) { + return std::make_shared(texture); +} \ No newline at end of file diff --git a/old_game_loop.md b/old_game_loop.md new file mode 100644 index 0000000..73f3f7e --- /dev/null +++ b/old_game_loop.md @@ -0,0 +1,287 @@ +# Old Game Loop Code + +This file will contain the entirity of a working gameLoop function from the main until finished refactoring to a fully functioning alternative that I am comfortable with. I could just use git commands but I'd like to have this here. + +```cpp +void gameLoop(RenderWindow& window, ResourceManager& resourceManager, bool& gameRunning, SDL_Event& event, GameStats& stats, InputHandler& inputHandler) +{ + auto& config = GameConfig::getInstance(); + config.initializeBeatTimings(); // Initialize beat timings + + const auto& assetPaths = config.getAssetPaths(); + const auto& fontSizes = config.getFontSizes(); + const auto& visualConfig = config.getVisualConfig(); + const auto& audioConfig = config.getAudioConfig(); + const auto& gameplayConfig = config.getGameplayConfig(); + + int fishStartX = 1920; + + // Textures loaded via ResourceManager + SDL_Texture* fishTextures[3]; + fishTextures[0] = resourceManager.loadTexture(assetPaths.blueFishTexture); + fishTextures[1] = resourceManager.loadTexture(assetPaths.greenFishTexture); + fishTextures[2] = resourceManager.loadTexture(assetPaths.goldFishTexture); + SDL_Texture* oceanTexture = resourceManager.loadTexture(assetPaths.oceanTexture); + SDL_Texture* boatTexture = resourceManager.loadTexture(assetPaths.boatTexture); + SDL_Texture* fisherTexture = resourceManager.loadTexture(assetPaths.fisherTexture); + SDL_Texture* hookTexture = resourceManager.loadTexture(assetPaths.hookTexture); + SDL_Texture* scoreTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameScore, "SCORE", visualConfig.BLACK); + SDL_Texture* numberTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameNumbers, "000000", visualConfig.BLACK); + SDL_Texture* perfectHitTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.hitFeedback, "1000", visualConfig.RED); + SDL_Texture* goodHitTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.hitFeedback, "500", visualConfig.RED); + + // Sprites & Background + Entity ocean(0, 0, oceanTexture); + Entity score(1720, 100, scoreTexture); + Entity number(1720, 150, numberTexture); + Sprite fisher(300, 200, fisherTexture, 1, 2); + Sprite boat(150, 350, boatTexture, 1, 1); + Sprite hook(430, 215, hookTexture, 1, 1); + std::vector fish; + fish.reserve(gameplayConfig.numBeats); + std::unordered_set fishHits; // index of fish hit + std::unordered_map fishHitTimes; // index -> time of hit + std::unordered_map fishHitTypes; // index -> false (Good), true (Perfect) + + for (int i = 0; i < gameplayConfig.numBeats; ++i) + { + fish.emplace_back(Sprite(gameplayConfig.fishStartXLocations[i], 720, fishTextures[rand() % gameplayConfig.numFishTextures], 1, 6)); + } + + float timeCounter = 0.0f; + + int songStartTime = SDL_GetTicks(); //Gets current ticks for better + int throwDuration = gameplayConfig.throwDuration; // for hook sprite + int hookTargetX = gameplayConfig.hookTargetX; // Fish location + int hookTargetY = gameplayConfig.hookTargetY; + int fishTargetX = gameplayConfig.fishTargetX; + int thrownTimer = 2; // for fisher sprite + int hookStartX; + int hookStartY; + int sway = 0; + int bob = 0; + int handX; // Fisher's hand position for hook throwing + int handY; + + bool isReturning = false; // for hook sprite + bool isThrowing = false; // for hook sprite + bool keydown = false; // Bool for the key + bool thrown = false; // for fisher sprite + + Uint32 throwStartTime = 0; + + Audio player; + AudioLogic gamePlay; + player.playBackgroundMusic(audioConfig.backgroundMusicPath); + + std::vector noteHitFlags(gameplayConfig.numBeats * 2, false); //This bool checks for the continueity (if the note has passed) regardless of getting hit. Overall helping with syncing + + const std::vector& noteBeats = gameplayConfig.noteBeats; + + while (gameRunning) + { + if (inputHandler.isKeyPressed(SDL_SCANCODE_SPACE)) //Checks the current state of the key and if true it makes the bool to be true (making it not work) unless not press down + keydown = true; + + double currentTime = SDL_GetTicks() - songStartTime; //calculates the delay by comparing the current ticks and when the song starts + + // Only update score texture when score actually changes + static int lastScore = -1; + int currentScore = stats.getScore(); + if (currentScore != lastScore) { + std::string strNum = formatScore(currentScore); + numberTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameNumbers, strNum, visualConfig.BLACK); + number.setTexture(numberTexture); + lastScore = currentScore; + } + handX = fisher.getX() + 135; + handY = fisher.getY() + 50; + + while (SDL_PollEvent(&event)) + { + InputAction action = inputHandler.processInput(event, GameState::Playing); + + if (action == InputAction::Quit) { + gameRunning = false; + break; + } + else if (action == InputAction::Select && !keydown) { // Space key for rhythm timing + if (!isThrowing) + { + thrown = true; + thrownTimer = 2; + isThrowing = true; + isReturning = false; + throwStartTime = SDL_GetTicks(); + hookStartX = handX; + hookStartY = handY; + hookTargetX = handX + 300; + hookTargetY = handY + 475; + } + // int j = 0; + for (int i = 0; i < noteBeats.size(); ++i) { + // if (noteHitFlags[i]) + // { + // j = 0; + // continue; + // } + + double expected = noteBeats[i]; + double delta = fabs(currentTime - expected); //Calculates the gurrent gap for the hit + // if (j == 0) + // std::cout << "Delta: " << delta << std::endl; + // j++; + if (delta <= gamePlay.getGOOD()) { + short int scoreType = gamePlay.checkHit(expected, currentTime); //This compares the time the SPACE or DOWN was pressed to the time it is requires for the PERFECT or GOOD or Miss + noteHitFlags[i] = true; + fishHits.insert(i); + fishHitTimes[i] = SDL_GetTicks(); // Record when hit occurred + if (scoreType == 2) + { + stats++; + stats.increaseScore(1000); + fishHitTypes[i] = true; + } + else if (scoreType == 1) + { + stats++; + stats.increaseScore(500); + fishHitTypes[i] = false; + } + break; + } + } + } + } + for (int i = 0; i < noteBeats.size(); ++i) { + if (noteHitFlags[i]) continue; + + double noteTime = noteBeats[i]; + if (currentTime > noteTime + gamePlay.getGOOD()) { + // std::cout << std::endl << "miss" << std::endl << std::endl; + stats--; + noteHitFlags[i] = true; + } + } + window.clear(); + window.render(ocean); + timeCounter += 0.05; + + // sways around sprites + for (int i = 0; i < gameplayConfig.numBeats; i++) + { + sway = static_cast(sin(timeCounter + i) * 1.1); + bob = static_cast(cos(timeCounter + i) * 1.1); + + fish[i].setLoc(fish[i].getX() + sway, fish[i].getY() + bob); + } + + hook.setLoc(hook.getX() + sway, hook.getY() + bob); + boat.setLoc(boat.getX() + sway, boat.getY() + bob); + fisher.setLoc(fisher.getX() + sway, fisher.getY() + bob); + + // Hand throwing sprite animation + if (thrown) + { + fisher.setFrame(1, 2); + thrownTimer--; + + if (thrownTimer <= 0) + { + thrown = false; + fisher.setFrame(1, 1); + } + } + else + { + fisher.setFrame(1, 1); + } + + // Hook throwing animation + if (isThrowing) + { + Uint32 now = SDL_GetTicks(); + Uint32 elapsed = now - throwStartTime; + + float progress = static_cast(elapsed) / throwDuration; + if (progress >= 1.0f) + { + progress = 1.0f; + + if (!isReturning) + { + isReturning = true; + // makes start the new target + std::swap(hookStartX, hookTargetX); + std::swap(hookStartY, hookTargetY); + } + else + { + isThrowing = false; + isReturning = false; + hook.setLoc(hookStartX, hookStartY); // back to original location + } + } + + int newX = static_cast(hookStartX + (hookTargetX - hookStartX) * progress); + int newY = static_cast(hookStartY + (hookTargetY - hookStartY) * progress); + hook.setLoc(newX, newY); + } + else + { + // Sway + bob when not throwing + hook.setLoc(hook.getX() + sway, hook.getY() + bob); + } + + Uint32 currentTicks = SDL_GetTicks(); + + // render fish + for (int i = 0; i < gameplayConfig.numBeats; i++) + { + if (fishHits.count(i)) + { + // Fish was hit calculate time since hit + Uint32 timeSinceHit = currentTicks - fishHitTimes[i]; + + if (timeSinceHit < 1000) + { + // Show text instead of fish for 1 second + SDL_Texture* scoreTex = (fishHitTypes[i]) ? perfectHitTexture : goodHitTexture; + + SDL_Rect textRect; + textRect.x = fish[i].getX(); // Same location as fish + textRect.y = fish[i].getY() - 30; // Slightly above fish + SDL_QueryTexture(scoreTex, NULL, NULL, &textRect.w, &textRect.h); + + SDL_RenderCopy(window.getRenderer(), scoreTex, NULL, &textRect); + } + + continue; // Skip rendering the fish itself + } + + // Move and render normal fish + fish[i].moveLeft(15); + window.render(fish[i]); + fish[i]++; + if (fish[i].getCol() == 4) + fish[i].resetFrame(); // dead fish frames were 4 and on + } + + window.render(boat); + window.render(hook); + window.render(fisher); + window.render(score); + window.render(number); + + window.display(); + keydown = false; // prevents holding space + + // Break the loop if music stopped playing + if (Mix_PlayingMusic() == 0) + gameRunning = false; + + SDL_Delay(visualConfig.frameDelay); + } + player.stopBackgroundMusic(); +} +``` \ No newline at end of file diff --git a/src/AnimationSystem.cpp b/src/AnimationSystem.cpp new file mode 100644 index 0000000..74bd04c --- /dev/null +++ b/src/AnimationSystem.cpp @@ -0,0 +1,127 @@ +#include "AnimationSystem.hpp" +#include "GameConfig.hpp" + +#include +#include + +AnimationSystem::AnimationSystem() : m_timeCounter(0.0f) { +} + +void AnimationSystem::initialize() { + m_timeCounter = 0.0f; +} + +void AnimationSystem::updateTiming() { + m_timeCounter += 0.05f; +} + +void AnimationSystem::startHookThrow(Sprite& hook, int handX, int handY, int throwDuration) { + // This method will be called to initialize hook throw parameters + // The actual state management happens in the calling code +} + +void AnimationSystem::updateHookAnimation(Sprite& hook, HookAnimationState& state) { + if (state.isThrowing) { + Uint32 now = SDL_GetTicks(); + Uint32 elapsed = now - state.throwStartTime; + + float progress = static_cast(elapsed) / state.throwDuration; + if (progress >= 1.0f) { + progress = 1.0f; + + if (!state.isReturning) { + state.isReturning = true; + // Reset timer for return journey to avoid teleporting + state.throwStartTime = SDL_GetTicks(); + // Swap start and target for return journey + std::swap(state.hookStartX, state.hookTargetX); + std::swap(state.hookStartY, state.hookTargetY); + // Recalculate progress for smooth transition + progress = 0.0f; + } + else { + state.isThrowing = false; + state.isReturning = false; + hook.setLoc(state.hookStartX, state.hookStartY); // back to original location + } + } + + // Always update position during throwing + if (state.isThrowing) { + int newX = static_cast(state.hookStartX + (state.hookTargetX - state.hookStartX) * progress); + int newY = static_cast(state.hookStartY + (state.hookTargetY - state.hookStartY) * progress); + hook.setLoc(newX, newY); + } + } +} + +bool AnimationSystem::isHookThrowing(const HookAnimationState& state) const { + return state.isThrowing; +} + +void AnimationSystem::startFisherThrow(FisherAnimationState& state) { + state.thrown = true; + state.thrownTimer = 2; +} + +void AnimationSystem::updateFisherAnimation(Sprite& fisher, FisherAnimationState& state) { + // Hand throwing sprite animation + if (state.thrown) { + fisher.setFrame(1, 2); + state.thrownTimer--; + + if (state.thrownTimer <= 0) { + state.thrown = false; + fisher.setFrame(1, 1); + } + } + else { + fisher.setFrame(1, 1); + } +} + +void AnimationSystem::updateSwayEffects(std::vector& fish, + const std::vector>& fishBasePositions) { + // Apply sway effects to fish with individual offsets (like the original) + for (size_t i = 0; i < fish.size() && i < fishBasePositions.size(); ++i) { + int fishSway = calculateSway(static_cast(i)); + int fishBob = calculateBob(static_cast(i)); + + // Apply sway relative to base position (not accumulating) + fish[i].setLoc(fishBasePositions[i].first + fishSway, + fishBasePositions[i].second + fishBob); + } +} + +void AnimationSystem::updateSwayEffects(Sprite& sprite, const std::pair& basePosition) { + int sway = calculateSway(); + int bob = calculateBob(); + + sprite.setLoc(basePosition.first + sway, basePosition.second + bob); +} + +void AnimationSystem::updateHookSway(Sprite& hook, const std::pair& basePosition, + const HookAnimationState& hookState) { + // Apply sway only when not performing hook throwing animation + if (!hookState.isThrowing) { + updateSwayEffects(hook, basePosition); + } +} + +void AnimationSystem::updateFishFrames(std::vector& fish, int numBeats) { + // Animate fish frames (same as original logic) + for (int i = 0; i < numBeats && i < static_cast(fish.size()); i++) { + fish[i]++; + if (fish[i].getCol() == 4) { + fish[i].resetFrame(); + } + } +} + +int AnimationSystem::calculateSway(float timeOffset) const { + return static_cast(sin(m_timeCounter + timeOffset) * 1.1); +} + +int AnimationSystem::calculateBob(float timeOffset) const { + return static_cast(cos(m_timeCounter + timeOffset) * 1.1); +} \ No newline at end of file diff --git a/src/Audio.cpp b/src/Audio.cpp index d2fd47e..5a3f4fe 100644 --- a/src/Audio.cpp +++ b/src/Audio.cpp @@ -1,34 +1,74 @@ #include "Audio.hpp" +#include "Logger.hpp" #include #include -Audio::Audio() { +Audio::Audio() : bgMusic(nullptr), m_valid(false) { if (SDL_Init(SDL_INIT_AUDIO) < 0) { - std::cerr << "Failed to initialize SDL audio: " << SDL_GetError() << std::endl; + Logger::logSDLError(LogLevel::ERROR, "Failed to initialize SDL audio"); return; } if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) { - std::cerr << "Failed to initialize SDL_mixer: " << Mix_GetError() << std::endl; + Logger::logSDLMixerError(LogLevel::ERROR, "Failed to initialize SDL_mixer"); + SDL_QuitSubSystem(SDL_INIT_AUDIO); return; } + + m_valid = true; + Logger::info("Audio system initialized successfully"); } Audio::~Audio() { + if (bgMusic) { + Mix_FreeMusic(bgMusic); + bgMusic = nullptr; + } Mix_CloseAudio(); } void Audio::playBackgroundMusic(const std::string& filePath) { - Mix_Music* bgMusic = Mix_LoadMUS(filePath.c_str()); + if (!m_valid) { + Logger::error("Audio::playBackgroundMusic called on invalid Audio system"); + return; + } + + if (filePath.empty()) { + Logger::error("Audio::playBackgroundMusic called with empty file path"); + return; + } + + // Free any existing music first + if (bgMusic) { + Mix_HaltMusic(); + Mix_FreeMusic(bgMusic); + bgMusic = nullptr; + } + + bgMusic = Mix_LoadMUS(filePath.c_str()); if (!bgMusic) { - std::cerr << "Failed to load music file: " << Mix_GetError() << std::endl; + Logger::logSDLMixerError(LogLevel::ERROR, "Failed to load music file: " + filePath); return; } + if (Mix_PlayMusic(bgMusic, 0) < 0) { - std::cerr << "Failed to play music: " << Mix_GetError() << std::endl; + Logger::logSDLMixerError(LogLevel::ERROR, "Failed to play music: " + filePath); Mix_FreeMusic(bgMusic); + bgMusic = nullptr; return; } + + Logger::info("Started playing background music: " + filePath); } void Audio::stopBackgroundMusic() { + if (!m_valid) { + Logger::warning("Audio::stopBackgroundMusic called on invalid Audio system"); + return; + } + Mix_HaltMusic(); + if (bgMusic) { + Mix_FreeMusic(bgMusic); + bgMusic = nullptr; + Logger::info("Stopped background music"); + } } diff --git a/src/AudioLogic.cpp b/src/AudioLogic.cpp index e246cf8..48c109e 100644 --- a/src/AudioLogic.cpp +++ b/src/AudioLogic.cpp @@ -24,11 +24,11 @@ short int AudioLogic::checkHit(double expectedMs, double actualMs) { short int scoreType = 0; if (delta <= PERFECT_WINDOW_MS) { - std::cout << "perfect, Delta: " << delta << std::endl; + // std::cout << "perfect, Delta: " << delta << std::endl; scoreType = 2; } else if (delta <= GOOD_WINDOW_MS) { - std::cout << "good, Delta: " << delta << std::endl; + // std::cout << "good, Delta: " << delta << std::endl; scoreType = 1; } diff --git a/src/Entity.cpp b/src/Entity.cpp index ba8ada2..e863163 100644 --- a/src/Entity.cpp +++ b/src/Entity.cpp @@ -1,14 +1,23 @@ #include "Entity.hpp" -Entity::Entity(float x, float y, SDL_Texture* texture) : x(x), y(y), texture(texture) +// Constructor overload for raw SDL_Texture* (backward compatibility) +Entity::Entity(float x, float y, SDL_Texture* texture) : Entity(x, y, texture ? makeSharedSDLTexture(texture) : nullptr) { +} + +Entity::Entity(float x, float y, SharedSDLTexture texture) : x(x), y(y), texture_(texture) { currentFrame.x = 0; currentFrame.y = 0; - // Automatically detect texture size + // Automatically detect texture size with error checking int textureW, textureH; - SDL_QueryTexture(texture, NULL, NULL, &textureW, &textureH); - currentFrame.w = textureW; - currentFrame.h = textureH; + if (texture_ && texture_->getSize(textureW, textureH)) { + currentFrame.w = textureW; + currentFrame.h = textureH; + } else { + // Default size for null or invalid textures + currentFrame.w = 0; + currentFrame.h = 0; + } } void Entity::setCurrentFrameW(int w) { diff --git a/src/Font.cpp b/src/Font.cpp index b4fb603..458271c 100644 --- a/src/Font.cpp +++ b/src/Font.cpp @@ -1,21 +1,19 @@ #include "Font.hpp" #include -using namespace std; - -Font::Font() : font(nullptr) {} // Init ptr 2 nullptr +Font::Font() : font(nullptr) {} Font::~Font() { - unload(); // Makin sure memory is nice n tidy + unload(); } -bool Font::load(const string& fontPath, int fontSz) +bool Font::load(const std::string& fontPath, int fontSz) { font = TTF_OpenFont(fontPath.c_str(), fontSz); // Opens up a font file, if works: true if !works: false if (!font) { - cerr << "Failed to load font: " << TTF_GetError() << endl; + std::cerr << "Failed to load font: " << TTF_GetError() << std::endl; return false; } return true; @@ -30,7 +28,7 @@ void Font::unload() } } -SDL_Texture* Font::renderText(SDL_Renderer* renderer, const string& txt, SDL_Color color) +SDL_Texture* Font::renderText(SDL_Renderer* renderer, const std::string& txt, SDL_Color color) { if (!font) { @@ -40,7 +38,7 @@ SDL_Texture* Font::renderText(SDL_Renderer* renderer, const string& txt, SDL_Col SDL_Surface* textSurface = TTF_RenderText_Blended(font, txt.c_str(), color); if (!textSurface) { - cerr << "Failed to create text surface: " << TTF_GetError() << endl; + std::cerr << "Failed to create text surface: " << TTF_GetError() << std::endl; return nullptr; } diff --git a/src/GameConfig.cpp b/src/GameConfig.cpp new file mode 100644 index 0000000..d45607c --- /dev/null +++ b/src/GameConfig.cpp @@ -0,0 +1,27 @@ +#include "GameConfig.hpp" +#include "AudioLogic.hpp" + +GameConfig& GameConfig::getInstance() +{ + static GameConfig instance; + return instance; +} + +void GameConfig::initializeBeatTimings() +{ + if (!gameplayConfig.noteBeats.empty()) + return; // Already initialized + + AudioLogic audioLogic; + gameplayConfig.noteBeats = { + audioLogic.msFromMscs(0,3,46), audioLogic.msFromMscs(0,7,75), audioLogic.msFromMscs(0,9,38), + audioLogic.msFromMscs(0,10,61), audioLogic.msFromMscs(0,12,24), audioLogic.msFromMscs(0,13,06), + audioLogic.msFromMscs(0,13,87), audioLogic.msFromMscs(0,15,30), audioLogic.msFromMscs(0,17,95), + audioLogic.msFromMscs(0,20,0), audioLogic.msFromMscs(0,21,22), audioLogic.msFromMscs(0,23,26), + audioLogic.msFromMscs(0,27,14), audioLogic.msFromMscs(0,28,57), audioLogic.msFromMscs(0,30,40), + audioLogic.msFromMscs(0,31,93), audioLogic.msFromMscs(0,32,65), audioLogic.msFromMscs(0,34,69), + audioLogic.msFromMscs(0,35,91), audioLogic.msFromMscs(0,37,95), audioLogic.msFromMscs(0,41,83), + audioLogic.msFromMscs(0,43,26), audioLogic.msFromMscs(0,45,10), audioLogic.msFromMscs(0,46,52), + audioLogic.msFromMscs(0,48,57) + }; +} \ No newline at end of file diff --git a/src/GameStateManager.cpp b/src/GameStateManager.cpp new file mode 100644 index 0000000..8e5e750 --- /dev/null +++ b/src/GameStateManager.cpp @@ -0,0 +1,145 @@ +#include "GameStateManager.hpp" +#include "RenderWindow.hpp" +#include "ResourceManager.hpp" +#include "GameConfig.hpp" +#include "Logger.hpp" + +#include + +GameStateManager::GameStateManager(RenderWindow& window, ResourceManager& resourceManager, InputHandler& inputHandler) + : currentState(GameState::MainMenu) + , nextState(GameState::MainMenu) + , window(window) + , resourceManager(resourceManager) + , inputHandler(inputHandler) +{ +} + +void GameStateManager::run() +{ + while (isRunning()) { + updateState(); + } +} + +void GameStateManager::transitionTo(GameState newState) +{ + nextState = newState; +} + +void GameStateManager::updateState() +{ + // Handle state transitions + if (currentState != nextState) { + currentState = nextState; + } + + // Execute current state + switch (currentState) { + case GameState::MainMenu: + Logger::info("Entering Main Menu"); + runMainMenu(); + break; + + case GameState::Playing: + Logger::info("Entering Gameplay"); + runGameplay(); + break; + + case GameState::EndScreen: + Logger::info("Entering End Screen"); + runEndScreen(); + break; + + case GameState::Quit: + // Will exit main loop + break; + } +} + +void GameStateManager::runMainMenu() +{ + MenuResult result = menuSystem.runMainMenu(window, resourceManager, inputHandler); + + switch (result) { + case MenuResult::StartGame: + resetGameStats(); + transitionTo(GameState::Playing); + break; + case MenuResult::QuitGame: + transitionTo(GameState::Quit); + break; + default: + break; + } +} + +void GameStateManager::runGameplay() +{ + // Initialize the rhythm game + rhythmGame.initialize(window, resourceManager, gameStats); + bool exitEarly = false; + // Main gameplay loop + while (currentState == GameState::Playing && isRunning()) { + exitEarly = false; + + // Process all SDL events this frame + while (SDL_PollEvent(&event)) { + InputAction action = inputHandler.processInput(event, GameState::Playing); + + // Process each action immediately instead of only keeping the last one + if (action != InputAction::None) { + if (!rhythmGame.update(action, inputHandler)) { + // Game ended (music finished or quit) + exitEarly = true; + break; + } + } + } + + // Update game logic even when no input events occurred + if (!exitEarly && !rhythmGame.update(InputAction::None, inputHandler)) { + exitEarly = true; + } + + // Render the game + rhythmGame.render(window); + + // Check if we should exit the gameplay state + if (rhythmGame.isGameOver(exitEarly)) { + break; + } + } + + // Clean up rhythm game resources (stop music, etc.) + rhythmGame.cleanup(); + + // Logger::logObject(LogLevel::INFO, gameStats); I need to update the formatting of cout gamestats + + transitionTo(GameState::EndScreen); +} + +void GameStateManager::runEndScreen() +{ + MenuResult result = menuSystem.runEndScreen(window, resourceManager, gameStats, inputHandler); + + switch (result) { + case MenuResult::RetryGame: + resetGameStats(); + transitionTo(GameState::Playing); + break; + case MenuResult::GoToMainMenu: + transitionTo(GameState::MainMenu); + break; + case MenuResult::QuitGame: + transitionTo(GameState::Quit); + break; + default: + break; + } +} + +void GameStateManager::resetGameStats() +{ + gameStats.resetStats(); +} \ No newline at end of file diff --git a/src/GameStats.cpp b/src/GameStats.cpp index ad2f4d2..499fdc2 100644 --- a/src/GameStats.cpp +++ b/src/GameStats.cpp @@ -2,12 +2,11 @@ //Desc: .cpp for displaying game statistics #include "GameStats.hpp" #include -using namespace std; GameStats::GameStats() { score = 0; - combo = 0; //willing to drop combo + combo = 0; // Combo system currently unused hits = 0; misses = 0; accuracy = 0.0; @@ -15,14 +14,16 @@ GameStats::GameStats() GameStats::GameStats(int score, int combo, int hits, int misses) //takes in hits and misses to calculate accuracy { setScore(score); - setCombo(combo); //cut + setCombo(combo); + setHits(hits); + setMisses(misses); accuracy = (hits > 0) ? (static_cast(hits) / (hits + misses)) * 100.0 : 0.0; // (1 - %ofMisses) * 100 = accuracy } void GameStats::setScore(int score) { this->score = score; } -void GameStats::setCombo(int combo) //old yeller this +void GameStats::setCombo(int combo) { this->combo = combo; } @@ -42,7 +43,7 @@ int GameStats::getScore()const { return score; } -int GameStats::getCombo()const //this too +int GameStats::getCombo()const { return combo; } @@ -70,19 +71,19 @@ void GameStats::resetStats() setMisses(0); setAccuracy(0); } -ostream& operator << (ostream& out, const GameStats& s) //displays stats at end of game +std::ostream& operator << (std::ostream& out, const GameStats& s) //displays stats at end of game { - out << "-*Final Stats!*-" << endl; + out << "-*Final Stats!*-" << std::endl; out << "Score: " << s.getScore() << "!!!\n"; - out << "Combo: " << s.getCombo() << endl; - out << "Accuracy: " << s.getAccuracy() << endl; + out << "Combo: " << s.getCombo() << std::endl; + out << "Accuracy: " << s.getAccuracy() << std::endl; if (s.getAccuracy() < 50.0) { - out << "Oh.. thats kinda bad.." << endl; + out << "Oh.. thats kinda bad.." << std::endl; } else { - out << "Well done!" << endl; + out << "Well done!" << std::endl; } return out; } diff --git a/src/InputHandler.cpp b/src/InputHandler.cpp new file mode 100644 index 0000000..38baedb --- /dev/null +++ b/src/InputHandler.cpp @@ -0,0 +1,100 @@ +#include "InputHandler.hpp" + +InputHandler::InputHandler() + : spaceKeyDown(false) +{ + keyboardState = SDL_GetKeyboardState(NULL); +} + +InputAction InputHandler::processInput(SDL_Event& event, GameState currentState) +{ + // Handle SDL_QUIT universally + if (event.type == SDL_QUIT) { + return InputAction::Quit; + } + + // Delegate to appropriate state handler + switch (currentState) { + case GameState::MainMenu: + return processMenuInput(event); + case GameState::Playing: + return processGameInput(event); + case GameState::EndScreen: + return processEndScreenInput(event); + default: + return InputAction::None; + } +} + +bool InputHandler::isKeyPressed(SDL_Scancode key) const +{ + return keyboardState[key] != 0; +} + +bool InputHandler::isSpaceHeld() const +{ + return spaceKeyDown; +} + +InputAction InputHandler::processMenuInput(const SDL_Event& event) +{ + if (event.type == SDL_KEYDOWN) { + switch (event.key.keysym.sym) { + case SDLK_ESCAPE: + return InputAction::Quit; + case SDLK_SPACE: + return InputAction::Select; + case SDLK_UP: + return InputAction::MenuUp; + case SDLK_DOWN: + return InputAction::MenuDown; + default: + break; + } + } + return InputAction::None; +} + +InputAction InputHandler::processGameInput(const SDL_Event& event) +{ + if (event.type == SDL_KEYDOWN) { + switch (event.key.keysym.sym) { + case SDLK_ESCAPE: + return InputAction::Quit; + case SDLK_SPACE: + // Only process spacebar on initial press (not when held) + if (!spaceKeyDown) { + spaceKeyDown = true; + return InputAction::Select; + } + break; // Ignore repeated key events from holding + default: + break; + } + } + else if (event.type == SDL_KEYUP) { + if (event.key.keysym.sym == SDLK_SPACE) { + spaceKeyDown = false; // Reset when space is released + } + } + return InputAction::None; +} + +InputAction InputHandler::processEndScreenInput(const SDL_Event& event) +{ + if (event.type == SDL_KEYDOWN) { + switch (event.key.keysym.sym) { + case SDLK_ESCAPE: + return InputAction::Escape; + case SDLK_SPACE: + return InputAction::Select; + case SDLK_UP: + return InputAction::MenuUp; + case SDLK_DOWN: + return InputAction::MenuDown; + default: + break; + } + } + return InputAction::None; +} \ No newline at end of file diff --git a/src/Logger.cpp b/src/Logger.cpp new file mode 100644 index 0000000..c982cd9 --- /dev/null +++ b/src/Logger.cpp @@ -0,0 +1,40 @@ +#include "Logger.hpp" +#include +#include +#include +#include + +void Logger::log(LogLevel level, const std::string& message) { + std::ostream& stream = getOutputStream(level); + stream << "[" << levelToString(level) << "] " << message << std::endl; +} + +void Logger::logSDLError(LogLevel level, const std::string& context) { + log(level, context + ": " + std::string(SDL_GetError())); +} + +void Logger::logSDLImageError(LogLevel level, const std::string& context) { + log(level, context + ": " + std::string(IMG_GetError())); +} + +void Logger::logSDLTTFError(LogLevel level, const std::string& context) { + log(level, context + ": " + std::string(TTF_GetError())); +} + +void Logger::logSDLMixerError(LogLevel level, const std::string& context) { + log(level, context + ": " + std::string(Mix_GetError())); +} + +std::string Logger::levelToString(LogLevel level) { + switch (level) { + case LogLevel::ERROR: return "ERROR"; + case LogLevel::WARNING: return "WARN"; + case LogLevel::INFO: return "INFO"; + case LogLevel::DEBUG: return "DEBUG"; + default: return "UNKNOWN"; + } +} + +std::ostream& Logger::getOutputStream(LogLevel level) { + return (level == LogLevel::ERROR || level == LogLevel::WARNING) ? std::cerr : std::cout; +} \ No newline at end of file diff --git a/src/MenuSystem.cpp b/src/MenuSystem.cpp new file mode 100644 index 0000000..c7392c6 --- /dev/null +++ b/src/MenuSystem.cpp @@ -0,0 +1,236 @@ +#include "MenuSystem.hpp" +#include "GameConfig.hpp" + +#include +#include +#include + +MenuSystem::MenuSystem() + : currentMenuType(MenuType::MainMenu) + , currentOption(0) + , menuActive(false) +{ +} + +MenuResult MenuSystem::runMainMenu(RenderWindow& window, ResourceManager& resourceManager, InputHandler& inputHandler) { + resetMenuState(MenuType::MainMenu); + + const auto& config = GameConfig::getInstance(); + const auto& assetPaths = config.getAssetPaths(); + const auto& fontSizes = config.getFontSizes(); + const auto& visualConfig = config.getVisualConfig(); + + // Create menu textures + SDL_Texture* quitTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.quitButton, "QUIT", visualConfig.YELLOW); + SDL_Texture* startTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.menuButtons, "START", visualConfig.YELLOW); + SDL_Texture* logoTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.menuLogo, "MEOWSTRO", visualConfig.YELLOW); + SDL_Texture* logoCatTexture = resourceManager.loadTexture(assetPaths.menuCatTexture); + SDL_Texture* selectedTexture = resourceManager.loadTexture(assetPaths.selectCatTexture); + + // Create menu entities + Entity quit(850, 800, quitTexture); + Entity logo(715, 350, logoTexture); + Entity start(850, 625, startTexture); + Entity logoCat(660, 200, logoCatTexture); + Sprite selectCat(760, 500, selectedTexture, 1, 1); + + SDL_Event event; + + while (menuActive) { + while (SDL_PollEvent(&event)) { + InputAction action = inputHandler.processInput(event, GameState::MainMenu); + + switch (action) { + case InputAction::Quit: + return MenuResult::QuitGame; + + case InputAction::Select: + // currentOption: 0 = start, 1 = quit + if (currentOption == 0) { + return MenuResult::StartGame; + } else { + return MenuResult::QuitGame; + } + + case InputAction::MenuUp: + case InputAction::MenuDown: + handleMenuNavigation(action, 2); // 2 options: start/quit + break; + + case InputAction::None: + default: + break; + } + } + + // Update selector position + updateSelectorPosition(selectCat, MenuType::MainMenu); + + // Render menu + window.clear(); + window.render(selectCat); + window.render(logoCat); + window.render(logo); + window.render(start); + window.render(quit); + window.display(); + } + + return MenuResult::None; +} + +MenuResult MenuSystem::runEndScreen(RenderWindow& window, ResourceManager& resourceManager, GameStats& stats, InputHandler& inputHandler) { + resetMenuState(MenuType::EndScreen); + + const auto& config = GameConfig::getInstance(); + const auto& assetPaths = config.getAssetPaths(); + const auto& fontSizes = config.getFontSizes(); + const auto& visualConfig = config.getVisualConfig(); + + // Create stats textures + SDL_Texture* statsTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameStats, "GAME STATS", visualConfig.YELLOW); + SDL_Texture* scoreTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameScore, "SCORE", visualConfig.YELLOW); + SDL_Texture* numberTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameScore, formatScore(stats.getScore()), visualConfig.YELLOW); + SDL_Texture* hitsTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameScore, "HITS", visualConfig.YELLOW); + SDL_Texture* numHitsTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameScore, std::to_string(stats.getHits()), visualConfig.YELLOW); + SDL_Texture* accuracyTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameScore, "ACCURACY", visualConfig.YELLOW); + SDL_Texture* accPercentTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameScore, (std::to_string(stats.getAccuracy()) + "%"), visualConfig.YELLOW); + SDL_Texture* missTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameScore, "MISSES", visualConfig.YELLOW); + SDL_Texture* numMissTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.gameScore, std::to_string(stats.getMisses()), visualConfig.YELLOW); + + // Create menu textures + SDL_Texture* quitTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.quitButton, "QUIT", visualConfig.YELLOW); + SDL_Texture* retryTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.quitButton, "RETRY", visualConfig.YELLOW); + SDL_Texture* logoTexture = resourceManager.createTextTexture(assetPaths.fontPath, fontSizes.menuLogo, "MEOWSTRO", visualConfig.YELLOW); + SDL_Texture* selectedTexture = resourceManager.loadTexture(assetPaths.selectCatTexture); + SDL_Texture* logoCatTexture = resourceManager.loadTexture(assetPaths.menuCatTexture); + + // Create entities + Entity titleStats(785, 325, statsTexture); + Entity score(650, 400, scoreTexture); + Entity number(1150, 400, numberTexture); + Entity hits(650, 500, hitsTexture); + Entity numHits(1150, 500, numHitsTexture); + Entity accuracy(650, 600, accuracyTexture); + Entity accPercent(1150, 600, accPercentTexture); + Entity misses(650, 700, missTexture); + Entity numMisses(1150, 700, numMissTexture); + + Entity quit(875, 900, quitTexture); + Entity logo(735, 150, logoTexture); + Entity retry(860, 800, retryTexture); + Entity logoCat(675, 0, logoCatTexture); + Sprite selectCat(775, 700, selectedTexture, 1, 1); + + SDL_Event event; + + while (menuActive) { + while (SDL_PollEvent(&event)) { + InputAction action = inputHandler.processInput(event, GameState::EndScreen); + + switch (action) { + case InputAction::Escape: + return MenuResult::GoToMainMenu; + + case InputAction::Select: + // currentOption: 0 = retry, 1 = quit + if (currentOption == 0) { + return MenuResult::RetryGame; + } else { + return MenuResult::QuitGame; + } + + case InputAction::MenuUp: + case InputAction::MenuDown: + handleMenuNavigation(action, 2); // 2 options: retry/quit + break; + + case InputAction::None: + default: + break; + } + } + + // Update selector position + updateSelectorPosition(selectCat, MenuType::EndScreen); + + // Render menu + window.clear(); + window.render(selectCat); + window.render(logoCat); + window.render(logo); + window.render(retry); + window.render(quit); + window.render(titleStats); + window.render(score); + window.render(number); + window.render(hits); + window.render(numHits); + window.render(accuracy); + window.render(accPercent); + window.render(misses); + window.render(numMisses); + window.display(); + } + + return MenuResult::None; +} + +// Future menu implementations +MenuResult MenuSystem::runPauseMenu(RenderWindow& window, ResourceManager& resourceManager, InputHandler& inputHandler) { + // TODO: Implement pause menu for future expansion + return MenuResult::None; +} + +MenuResult MenuSystem::runSettingsMenu(RenderWindow& window, ResourceManager& resourceManager, InputHandler& inputHandler) { + // TODO: Implement settings menu for future expansion + return MenuResult::None; +} + +void MenuSystem::resetMenuState(MenuType type) { + currentMenuType = type; + currentOption = 0; + menuActive = true; +} + +void MenuSystem::handleMenuNavigation(InputAction action, int maxOptions) { + switch (action) { + case InputAction::MenuUp: + currentOption = (currentOption > 0) ? currentOption - 1 : maxOptions - 1; + break; + case InputAction::MenuDown: + currentOption = (currentOption < maxOptions - 1) ? currentOption + 1 : 0; + break; + default: + break; + } +} + +void MenuSystem::updateSelectorPosition(Sprite& selector, MenuType menuType) { + switch (menuType) { + case MenuType::MainMenu: + if (currentOption == 0) { + selector.setLoc(760, 600); // Start position + } else { + selector.setLoc(760, 775); // Quit position + } + break; + + case MenuType::EndScreen: + if (currentOption == 0) { + selector.setLoc(775, 775); // Retry position + } else { + selector.setLoc(775, 900); // Quit position + } + break; + + default: + break; + } +} + +std::string MenuSystem::formatScore(int score) { + std::ostringstream ss; + ss << std::setw(6) << std::setfill('0') << score; + return ss.str(); +} \ No newline at end of file diff --git a/src/RenderWindow.cpp b/src/RenderWindow.cpp index 0eb4005..686d9cd 100644 --- a/src/RenderWindow.cpp +++ b/src/RenderWindow.cpp @@ -1,35 +1,48 @@ #include #include "RenderWindow.hpp" +#include "Logger.hpp" -RenderWindow::RenderWindow(const char *title, int w, int h, Uint32 windowFlags) : window(NULL), renderer(NULL) +RenderWindow::RenderWindow(const char *title, int w, int h, Uint32 windowFlags) + : window(nullptr), renderer(nullptr), m_valid(false) { window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, w, h, windowFlags); - if (window == NULL) + if (window == nullptr) { - std::cout << "Window failed to init. ERROR: " << SDL_GetError() << std::endl; + Logger::logSDLError(LogLevel::ERROR, "Failed to create window"); return; } + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + if (renderer == nullptr) + { + Logger::logSDLError(LogLevel::ERROR, "Failed to create renderer"); + SDL_DestroyWindow(window); + window = nullptr; + return; + } + SDL_SetRenderDrawColor(renderer, 100, 115, 180, 185); -} - -SDL_Texture *RenderWindow::loadTexture(const char *filePath) -{ - SDL_Texture *texture = NULL; - texture = IMG_LoadTexture(renderer, filePath); - - if (texture == NULL) - std::cout << "IMG_LoadTexture failed. ERROR: " << SDL_GetError() << std::endl; - - return texture; + m_valid = true; } void RenderWindow::clear() { - SDL_RenderClear(renderer); + if (m_valid && renderer) { + SDL_RenderClear(renderer); + } } void RenderWindow::render(Entity& entity) { + if (!m_valid || !renderer) { + Logger::error("RenderWindow::render called on invalid window"); + return; + } + + if (entity.getTexture() == nullptr) { + Logger::warning("RenderWindow::render called with null texture"); + return; + } + SDL_Rect src = entity.getCurrentFrame(); SDL_Rect destination; @@ -42,11 +55,19 @@ void RenderWindow::render(Entity& entity) } void RenderWindow::display() { - SDL_RenderPresent(renderer); + if (m_valid && renderer) { + SDL_RenderPresent(renderer); + } } RenderWindow::~RenderWindow() { - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); + if (renderer) { + SDL_DestroyRenderer(renderer); + renderer = nullptr; + } + if (window) { + SDL_DestroyWindow(window); + window = nullptr; + } } \ No newline at end of file diff --git a/src/ResourceManager.cpp b/src/ResourceManager.cpp new file mode 100644 index 0000000..ea4293f --- /dev/null +++ b/src/ResourceManager.cpp @@ -0,0 +1,133 @@ +#include "ResourceManager.hpp" +#include "Logger.hpp" + +#include + +ResourceManager::ResourceManager(SDL_Renderer* renderer) : renderer(renderer), m_valid(false) { + if (renderer == nullptr) { + Logger::error("ResourceManager: null renderer provided"); + return; + } + m_valid = true; +} + +ResourceManager::~ResourceManager() { + cleanup(); +} + +SDL_Texture* ResourceManager::loadTexture(const std::string& filePath) { + if (!m_valid) { + Logger::error("ResourceManager::loadTexture called on invalid ResourceManager"); + return nullptr; + } + + if (filePath.empty()) { + Logger::error("ResourceManager::loadTexture called with empty file path"); + return nullptr; + } + + // Check if texture already loaded + auto it = textures.find(filePath); + if (it != textures.end()) { + return it->second; + } + + // Load new texture + SDL_Texture* texture = IMG_LoadTexture(renderer, filePath.c_str()); + if (!texture) { + Logger::logSDLImageError(LogLevel::ERROR, "Failed to load texture: " + filePath); + return nullptr; + } + + // Cache the texture + textures[filePath] = texture; + Logger::debug("Loaded texture: " + filePath); + return texture; +} + +SDL_Texture* ResourceManager::createTextTexture(const std::string& fontPath, int fontSize, const std::string& text, SDL_Color color) { + if (!m_valid) { + Logger::error("ResourceManager::createTextTexture called on invalid ResourceManager"); + return nullptr; + } + + if (fontPath.empty()) { + Logger::error("ResourceManager::createTextTexture called with empty font path"); + return nullptr; + } + + if (text.empty()) { + Logger::warning("ResourceManager::createTextTexture called with empty text"); + return nullptr; + } + + std::string textKey = generateTextKey(fontPath, fontSize, text, color); + + // Check if text texture already exists + auto it = textures.find(textKey); + if (it != textures.end()) { + return it->second; + } + + // Get or load font + Font* font = getFont(fontPath, fontSize); + if (!font) { + Logger::error("Failed to get font for text texture: " + fontPath); + return nullptr; + } + + // Create text texture + SDL_Texture* textTexture = font->renderText(renderer, text, color); + if (textTexture) { + textures[textKey] = textTexture; + Logger::debug("Created text texture: " + text); + } else { + Logger::error("Failed to create text texture for: " + text); + } + + return textTexture; +} + +Font* ResourceManager::getFont(const std::string& fontPath, int fontSize) { + std::string fontKey = generateFontKey(fontPath, fontSize); + + // Check if font already loaded + auto it = fonts.find(fontKey); + if (it != fonts.end()) { + return it->second.get(); + } + + // Load new font + auto font = std::make_unique(); + if (!font->load(fontPath, fontSize)) { + return nullptr; + } + + // Cache the font and return raw pointer for API compatibility + Font* fontPtr = font.get(); + fonts[fontKey] = std::move(font); + return fontPtr; +} + +void ResourceManager::cleanup() { + // Clean up all textures + for (auto& pair : textures) { + if (pair.second) { + SDL_DestroyTexture(pair.second); + } + } + textures.clear(); + + // Clean up all fonts - unique_ptr handles deletion automatically + fonts.clear(); +} + +std::string ResourceManager::generateFontKey(const std::string& fontPath, int fontSize) const { + return fontPath + "_" + std::to_string(fontSize); +} + +std::string ResourceManager::generateTextKey(const std::string& fontPath, int fontSize, const std::string& text, SDL_Color color) const { + return fontPath + "_" + std::to_string(fontSize) + "_" + text + "_" + + std::to_string(color.r) + "_" + std::to_string(color.g) + "_" + + std::to_string(color.b) + "_" + std::to_string(color.a); +} \ No newline at end of file diff --git a/src/RhythmGame.cpp b/src/RhythmGame.cpp new file mode 100644 index 0000000..ee45b03 --- /dev/null +++ b/src/RhythmGame.cpp @@ -0,0 +1,372 @@ +#include "RhythmGame.hpp" +#include "GameConfig.hpp" + +#include +#include +#include +#include +#include +#include + +RhythmGame::RhythmGame() + : m_resourceManager(nullptr) + , m_gameStats(nullptr) + , m_songStartTime(0) + , m_ocean(0, 0, nullptr) + , m_scoreLabel(0, 0, nullptr) + , m_scoreNumber(0, 0, nullptr) + , m_fisher(0, 0, nullptr, 1, 2) + , m_boat(0, 0, nullptr, 1, 1) + , m_hook(0, 0, nullptr, 1, 1) + , m_throwDuration(0) + , m_hookTargetX(0) + , m_hookTargetY(0) + , m_perfectHitTexture(nullptr) + , m_goodHitTexture(nullptr) + , m_lastScore(-1) +{ + m_fishTextures[0] = nullptr; + m_fishTextures[1] = nullptr; + m_fishTextures[2] = nullptr; +} + +RhythmGame::~RhythmGame() { + m_audioPlayer.stopBackgroundMusic(); +} + +void RhythmGame::initialize(RenderWindow& window, ResourceManager& resourceManager, GameStats& stats) { + m_resourceManager = &resourceManager; + m_gameStats = &stats; + + auto& config = GameConfig::getInstance(); + config.initializeBeatTimings(); + + const auto& gameplayConfig = config.getGameplayConfig(); + + // Initialize timing + m_songStartTime = SDL_GetTicks(); + m_noteHitFlags.assign(gameplayConfig.numBeats * 2, false); + + // Initialize animation system + m_animationSystem.initialize(); + + // Clear fish hit tracking data from previous game + m_fishHits.clear(); + m_fishHitTimes.clear(); + m_fishHitTypes.clear(); + + // Initialize animation parameters + m_throwDuration = gameplayConfig.throwDuration; + m_hookTargetX = gameplayConfig.hookTargetX; + m_hookTargetY = gameplayConfig.hookTargetY; + m_hookAnimationState.throwDuration = m_throwDuration; + + // Initialize textures and entities + initializeTextures(); + initializeEntities(); + initializeFish(); + + // Start music + const auto& audioConfig = config.getAudioConfig(); + m_audioPlayer.playBackgroundMusic(audioConfig.backgroundMusicPath); +} + +void RhythmGame::initializeTextures() { + const auto& config = GameConfig::getInstance(); + const auto& assetPaths = config.getAssetPaths(); + const auto& fontSizes = config.getFontSizes(); + const auto& visualConfig = config.getVisualConfig(); + + // Load fish textures + m_fishTextures[0] = m_resourceManager->loadTexture(assetPaths.blueFishTexture); + m_fishTextures[1] = m_resourceManager->loadTexture(assetPaths.greenFishTexture); + m_fishTextures[2] = m_resourceManager->loadTexture(assetPaths.goldFishTexture); + + // Load hit feedback textures + m_perfectHitTexture = m_resourceManager->createTextTexture(assetPaths.fontPath, fontSizes.hitFeedback, "1000", visualConfig.RED); + m_goodHitTexture = m_resourceManager->createTextTexture(assetPaths.fontPath, fontSizes.hitFeedback, "500", visualConfig.RED); +} + +void RhythmGame::initializeEntities() { + const auto& config = GameConfig::getInstance(); + const auto& assetPaths = config.getAssetPaths(); + const auto& fontSizes = config.getFontSizes(); + const auto& visualConfig = config.getVisualConfig(); + + // Load textures + SDL_Texture* oceanTexture = m_resourceManager->loadTexture(assetPaths.oceanTexture); + SDL_Texture* boatTexture = m_resourceManager->loadTexture(assetPaths.boatTexture); + SDL_Texture* fisherTexture = m_resourceManager->loadTexture(assetPaths.fisherTexture); + SDL_Texture* hookTexture = m_resourceManager->loadTexture(assetPaths.hookTexture); + SDL_Texture* scoreTexture = m_resourceManager->createTextTexture(assetPaths.fontPath, fontSizes.gameScore, "SCORE", visualConfig.BLACK); + SDL_Texture* numberTexture = m_resourceManager->createTextTexture(assetPaths.fontPath, fontSizes.gameNumbers, "000000", visualConfig.BLACK); + + // Initialize entities + m_ocean = Entity(0, 0, oceanTexture); + m_scoreLabel = Entity(1720, 100, scoreTexture); + m_scoreNumber = Entity(1720, 150, numberTexture); + m_fisher = Sprite(300, 200, fisherTexture, 1, 2); + m_boat = Sprite(150, 350, boatTexture, 1, 1); + m_hook = Sprite(430, 215, hookTexture, 1, 1); + + // Store base positions for sway effect + m_fisherBasePosition = {300, 200}; + m_boatBasePosition = {150, 350}; + m_hookBasePosition = {430, 215}; +} + +void RhythmGame::initializeFish() { + const auto& config = GameConfig::getInstance(); + const auto& gameplayConfig = config.getGameplayConfig(); + + m_fish.clear(); + m_fish.reserve(gameplayConfig.numBeats); + m_fishBasePositions.clear(); + m_fishBasePositions.reserve(gameplayConfig.numBeats); + + for (int i = 0; i < gameplayConfig.numBeats; ++i) { + int baseX = gameplayConfig.fishStartXLocations[i]; + int baseY = 720; + + m_fish.emplace_back(Sprite(baseX, baseY, m_fishTextures[rand() % gameplayConfig.numFishTextures], 1, 6)); + m_fishBasePositions.emplace_back(baseX, baseY); + } +} + +bool RhythmGame::update(InputAction action, InputHandler& inputHandler) { + const auto& config = GameConfig::getInstance(); + const auto& visualConfig = config.getVisualConfig(); + + // Handle input state + if (action == InputAction::Quit || action == InputAction::Escape) { + return false; // Game should end (ESC or window close) + } + + // Update animation timing + m_animationSystem.updateTiming(); + + double currentTime = SDL_GetTicks() - m_songStartTime; + + // Handle rhythm input - simplified logic + if (action == InputAction::Select) { + handleRhythmInput(currentTime); + } + + // Only do these updates when no specific action is being processed + // (to avoid duplicate work when processing multiple events per frame) + if (action == InputAction::None) { + // Check for missed notes + checkMissedNotes(currentTime); + + // Update score display + updateScore(); + + // Update animations and fish movement + updateAnimations(); + updateFishMovement(); + + // Check if game should end (music stopped) + if (Mix_PlayingMusic() == 0) { + return false; + } + + // Frame delay (only when no input events) + SDL_Delay(visualConfig.frameDelay); + } + + return true; // Continue game +} + + +void RhythmGame::handleRhythmInput(double currentTime) { + const auto& config = GameConfig::getInstance(); + const auto& gameplayConfig = config.getGameplayConfig(); + const std::vector& noteBeats = gameplayConfig.noteBeats; + + // Handle hook throwing + if (!m_hookAnimationState.isThrowing) { + // Start fisher animation + m_animationSystem.startFisherThrow(m_fisherAnimationState); + + // Start hook throwing + m_hookAnimationState.isThrowing = true; + m_hookAnimationState.isReturning = false; + m_hookAnimationState.throwStartTime = SDL_GetTicks(); + + int handX = m_fisher.getX() + 135; + int handY = m_fisher.getY() + 50; + m_hookAnimationState.hookStartX = handX; + m_hookAnimationState.hookStartY = handY; + m_hookAnimationState.hookTargetX = handX + 300; + m_hookAnimationState.hookTargetY = handY + 475; + } + + // Check rhythm timing + for (int i = 0; i < noteBeats.size(); ++i) { + // Skip notes that have already been hit + if (m_noteHitFlags[i]) continue; + + double expected = noteBeats[i]; + double delta = fabs(currentTime - expected); + + if (delta <= m_rhythmLogic.getGOOD()) { + short int scoreType = m_rhythmLogic.checkHit(expected, currentTime); + m_noteHitFlags[i] = true; + m_fishHits.insert(i); + m_fishHitTimes[i] = SDL_GetTicks(); + + if (scoreType == 2) { // Perfect + (*m_gameStats)++; + m_gameStats->increaseScore(1000); + m_fishHitTypes[i] = true; + } + else if (scoreType == 1) { // Good + (*m_gameStats)++; + m_gameStats->increaseScore(500); + m_fishHitTypes[i] = false; + } + break; + } + } +} + +void RhythmGame::checkMissedNotes(double currentTime) { + const auto& config = GameConfig::getInstance(); + const auto& gameplayConfig = config.getGameplayConfig(); + const std::vector& noteBeats = gameplayConfig.noteBeats; + + for (int i = 0; i < noteBeats.size(); ++i) { + if (m_noteHitFlags[i]) continue; + + double noteTime = noteBeats[i]; + if (currentTime > noteTime + m_rhythmLogic.getGOOD()) { + (*m_gameStats)--; + m_noteHitFlags[i] = true; + } + } +} + +void RhythmGame::updateScore() { + int currentScore = m_gameStats->getScore(); + if (currentScore != m_lastScore) { + const auto& config = GameConfig::getInstance(); + const auto& assetPaths = config.getAssetPaths(); + const auto& fontSizes = config.getFontSizes(); + const auto& visualConfig = config.getVisualConfig(); + + std::string strNum = formatScore(currentScore); + SDL_Texture* numberTexture = m_resourceManager->createTextTexture(assetPaths.fontPath, fontSizes.gameNumbers, strNum, visualConfig.BLACK); + m_scoreNumber.setTexture(numberTexture); + m_lastScore = currentScore; + } +} + +void RhythmGame::updateAnimations() { + // Update fisher animation + m_animationSystem.updateFisherAnimation(m_fisher, m_fisherAnimationState); + + // Update hook animation + m_animationSystem.updateHookAnimation(m_hook, m_hookAnimationState); + + // Update sway effects + m_animationSystem.updateHookSway(m_hook, m_hookBasePosition, m_hookAnimationState); + m_animationSystem.updateSwayEffects(m_boat, m_boatBasePosition); + m_animationSystem.updateSwayEffects(m_fisher, m_fisherBasePosition); + m_animationSystem.updateSwayEffects(m_fish, m_fishBasePositions); +} + +void RhythmGame::updateFishMovement() { + const auto& config = GameConfig::getInstance(); + const auto& gameplayConfig = config.getGameplayConfig(); + + // Move fish (only if not hit) + for (int i = 0; i < gameplayConfig.numBeats; i++) { + if (m_fishHits.count(i)) { + continue; // Skip hit fish + } + + // Move fish left (same as original) + m_fish[i].moveLeft(15); + + // Update base position after movement (for sway effects) + m_fishBasePositions[i].first = m_fish[i].getX(); + m_fishBasePositions[i].second = m_fish[i].getY(); + } + + // Animate fish frames using animation system (for all fish including hit ones) + // The animation system will handle the frame updates + for (int i = 0; i < gameplayConfig.numBeats; i++) { + if (!m_fishHits.count(i)) { + m_fish[i]++; + if (m_fish[i].getCol() == 4) { + m_fish[i].resetFrame(); + } + } + } +} + + + + +void RhythmGame::render(RenderWindow& window) { + window.clear(); + + // Render background + window.render(m_ocean); + + // Render fish with hit feedback + Uint32 currentTicks = SDL_GetTicks(); + renderFish(window, currentTicks); + + // Render game objects + window.render(m_boat); + window.render(m_hook); + window.render(m_fisher); + window.render(m_scoreLabel); + window.render(m_scoreNumber); + + window.display(); +} + +void RhythmGame::renderFish(RenderWindow& window, Uint32 currentTicks) { + const auto& config = GameConfig::getInstance(); + const auto& gameplayConfig = config.getGameplayConfig(); + + for (int i = 0; i < gameplayConfig.numBeats; i++) { + if (m_fishHits.count(i)) { + // Fish was hit - calculate time since hit + Uint32 timeSinceHit = currentTicks - m_fishHitTimes[i]; + + if (timeSinceHit < 1000) { + // Show score text instead of fish for 1 second + SDL_Texture* scoreTex = m_fishHitTypes[i] ? m_perfectHitTexture : m_goodHitTexture; + + SDL_Rect textRect; + textRect.x = m_fish[i].getX(); + textRect.y = m_fish[i].getY() - 30; + SDL_QueryTexture(scoreTex, NULL, NULL, &textRect.w, &textRect.h); + + SDL_RenderCopy(window.getRenderer(), scoreTex, NULL, &textRect); + } + continue; // Skip rendering the fish itself + } + + // Render normal fish (movement happens in updateFishMovement) + window.render(m_fish[i]); + } +} + +bool RhythmGame::isGameOver(bool exitEarly) const { + return Mix_PlayingMusic() == 0 || exitEarly; +} + +void RhythmGame::cleanup() { + // Stop background music (like the original gameLoop does) + m_audioPlayer.stopBackgroundMusic(); +} + +std::string RhythmGame::formatScore(int score) { + std::ostringstream ss; + ss << std::setw(6) << std::setfill('0') << score; + return ss.str(); +} \ No newline at end of file diff --git a/src/meowstro.cpp b/src/meowstro.cpp index c536466..b959fd1 100644 --- a/src/meowstro.cpp +++ b/src/meowstro.cpp @@ -1,554 +1,90 @@ // Names: Hugo, Jaime, Jay, Leo -// Last Modified: 05/17/25 +// Last Modified: 08/30/25 // Purpose: MEOWSTRO // // -#include -#include -#include -#include -#include -#include -#include - +#include "GameStateManager.hpp" +#include "ResourceManager.hpp" +#include "InputHandler.hpp" #include "RenderWindow.hpp" +#include "GameConfig.hpp" #include "AudioLogic.hpp" #include "GameStats.hpp" #include "Entity.hpp" #include "Sprite.hpp" #include "Audio.hpp" #include "Font.hpp" +#include "Logger.hpp" +#include "Exceptions.hpp" -const char* comicSans = "../assets/fonts/Comic Sans MS.ttf"; -const int NUM_OF_BEATS = 25; -const int NUM_FISH_TEXTURES = 3; -const SDL_Color YELLOW = { 255, 255, 100, 255 }; -const int FISH_START_X_LOCS[NUM_OF_BEATS] = { 1352, 2350, 2465, 2800, 3145, - 3330, 3480, 3663, 4175, 4560, - 4816, 5245, 6059, 6260, 6644, - 6885, 7100, 7545, 7801, 8230, - 8775, 9145, 9531, 9829, 10160 }; - -void mainMenu(RenderWindow& window, bool &gameRunning, SDL_Event &event); -void gameLoop(RenderWindow& window, bool& gameRunning, SDL_Event& event, GameStats& stats); -void endScreen(RenderWindow& window, bool& gameRunning, SDL_Event& event, GameStats& stats); - -std::string formatScore(int score); - -int main(int argc, char* args[]) -{ - - if (SDL_Init(SDL_INIT_VIDEO) > 0) - std::cout << "SDL_Init has failed, SDL ERROR: " << SDL_GetError(); - if (!(IMG_Init(IMG_INIT_PNG))) - std::cout << "IMG_Init has failed, SDL ERROR: " << SDL_GetError(); - if (TTF_Init() == -1) - std::cerr << "TTF_Init failed: " << TTF_GetError() << std::endl; - - RenderWindow window("Meowstro", 1920, 1080, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); - - srand(static_cast(time(NULL))); - bool gameRunning = true; - GameStats stats; - SDL_Event event; - - while (gameRunning) - { - mainMenu(window, gameRunning, event); - if (gameRunning) - { - gameLoop(window, gameRunning, event, stats); - std::cout << stats; - endScreen(window, gameRunning, event, stats); - stats.resetStats(); - } - } - window.~RenderWindow(); - - TTF_Quit(); - SDL_Quit(); - - return 0; -} +#include +#include +#include +#include +#include +#include +#include +#include -void mainMenu(RenderWindow &window, bool &gameRunning, SDL_Event &event) +int main(int argc, char** argv) { - Font logoFont, startFont, quitFont; - bool onMenu = true; - bool option = false; - - logoFont.load(comicSans, 75); - startFont.load(comicSans, 55); - quitFont.load(comicSans, 65); - - SDL_Texture* quitTexture = quitFont.renderText(window.getRenderer(), "QUIT", YELLOW); - SDL_Texture* startTexture = startFont.renderText(window.getRenderer(), "START", YELLOW); - SDL_Texture* logoTexture = logoFont.renderText(window.getRenderer(), "MEOWSTRO", YELLOW); - SDL_Texture* logoCatTexture = window.loadTexture("../assets/images/menu_cat.png"); - SDL_Texture* selectedTexture = window.loadTexture("../assets/images/select_cat.png"); - - Entity quit(850, 800, quitTexture); - Entity logo(715, 350, logoTexture); - Entity start(850, 625, startTexture); - Entity logoCat(660, 200, logoCatTexture); - Sprite selectCat(760, 500, selectedTexture, 1, 1); - - while (onMenu) - { - #ifdef CI_BUILD - onMenu = false; - #endif - while (SDL_PollEvent(&event)) - { - switch (event.type) - { - case SDL_KEYDOWN: - { - switch (event.key.keysym.sym) // input - { - case SDLK_ESCAPE: - { - onMenu = false; - gameRunning = false; - break; - } - case SDLK_SPACE: - { - onMenu = false; - gameRunning = (option) ? false : true; // op0 = start op1 = exit - break; - } - case SDLK_UP: - case SDLK_DOWN: - { - option = (option) ? false : true; - break; - } - } - } - } + try { + // Initialize SDL subsystems - fail fast on critical errors + if (SDL_Init(SDL_INIT_VIDEO) != EXIT_SUCCESS) { + throw InitializationException("SDL_Init(VIDEO) failed: " + std::string(SDL_GetError())); } - window.clear(); - // if represents selected option render location - if (option) - selectCat.setLoc(760, 775); - else - selectCat.setLoc(760, 600); - - window.render(selectCat); - window.render(logoCat); - window.render(logo); - window.render(start); - window.render(quit); - window.display(); - } - logoFont.unload(); - startFont.unload(); - quitFont.unload(); -} - -void gameLoop(RenderWindow& window, bool& gameRunning, SDL_Event& event, GameStats& stats) -{ - static int bpm = 147; - int fishStartX = 1920; - - Font scoreFont, numberFont, perfectHitFont, goodHitFont; - scoreFont.load(comicSans, 40); - numberFont.load(comicSans, 35); - perfectHitFont.load(comicSans, 30); - goodHitFont.load(comicSans, 30); - - // Textures - SDL_Texture* fishTextures[NUM_FISH_TEXTURES]; - fishTextures[0] = window.loadTexture("../assets/images/blue_fish.png"); - fishTextures[1] = window.loadTexture("../assets/images/green_fish.png"); - fishTextures[2] = window.loadTexture("../assets/images/gold_fish.png"); - SDL_Texture* oceanTexture = window.loadTexture("../assets/images/Ocean.png"); - SDL_Texture* boatTexture = window.loadTexture("../assets/images/boat.png"); - SDL_Texture* fisherTexture = window.loadTexture("../assets/images/fisher.png"); - SDL_Texture* hookTexture = window.loadTexture("../assets/images/hook.png"); - SDL_Texture* scoreTexture = scoreFont.renderText(window.getRenderer(), "SCORE", { 0, 0, 0, 255 }); - SDL_Texture* numberTexture = numberFont.renderText(window.getRenderer(), "000000", { 0, 0, 0, 255 }); - SDL_Texture* perfectHitTexture = perfectHitFont.renderText(window.getRenderer(), "1000", { 255, 0, 0, 255 }); - SDL_Texture* goodHitTexture = goodHitFont.renderText(window.getRenderer(), "500", { 255, 0, 0, 255 }); - - // Sprites & Background - Entity ocean(0, 0, oceanTexture); - Entity score(1720, 100, scoreTexture); - Entity number(1720, 150, numberTexture); - Sprite fisher(300, 200, fisherTexture, 1, 2); - Sprite boat(150, 350, boatTexture, 1, 1); - Sprite hook(430, 215, hookTexture, 1, 1); - std::vector fish; - std::unordered_set fishHits; // index of fish hit - std::unordered_map fishHitTimes; // index -> time of hit - std::unordered_map fishHitTypes; // index -> false (Good), true (Perfect) - - for (int i = 0; i < 25; i++) - { - fish.push_back(Sprite(FISH_START_X_LOCS[i], 720, fishTextures[rand() % 3], 1, 6)); - } - - const double travelDuration = 2000.0; // ms before beat to start moving - float timeCounter = 0.0f; - - int songStartTime = SDL_GetTicks(); //Gets current ticks for better - int throwDuration = 200; // for hook sprite - int hookTargetX = 650; // Fish location - int hookTargetY = 625; - int fishTargetX = 660; - int thrownTimer = 2; // for fisher sprite - int hookStartX; - int hookStartY; - int sway = 0; - int bob = 0; - int handX; // This cat is thor but with a spear hook thing - int handY; - - bool isReturning = false; // for hook sprite - bool isThrowing = false; // for hook sprite - bool keydown = false; //Bool for the key - bool thrown = false; // for fisher sprite - - Uint32 throwStartTime = 0; - - Audio player; - AudioLogic gamePlay; - player.playBackgroundMusic("../assets/audio/meowstro_short_ver.mp3"); - - std::vector noteHitFlags(49, false); //This bool checks for the continueity (if the note has passed) regardless of getting hit. Overall helping with syncing - - std::vector noteBeats = { //The vector contains the clicks in miliseconds - gamePlay.msFromMscs(0,3,46), gamePlay.msFromMscs(0,7,75), gamePlay.msFromMscs(0,9,38), gamePlay.msFromMscs(0,10,61), gamePlay.msFromMscs(0,12,24), gamePlay.msFromMscs(0,13,06), - gamePlay.msFromMscs(0,13,87), gamePlay.msFromMscs(0,15,30), gamePlay.msFromMscs(0,17,95), gamePlay.msFromMscs(0,20,0), gamePlay.msFromMscs(0,21,22), gamePlay.msFromMscs(0,23,26), - gamePlay.msFromMscs(0,27,14), gamePlay.msFromMscs(0,28,57), gamePlay.msFromMscs(0,30,40), gamePlay.msFromMscs(0,31,93), gamePlay.msFromMscs(0,32,65), gamePlay.msFromMscs(0,34,69), - gamePlay.msFromMscs(0,35,91), gamePlay.msFromMscs(0,37,95), gamePlay.msFromMscs(0,41,83), gamePlay.msFromMscs(0,43,26), gamePlay.msFromMscs(0,45,10), gamePlay.msFromMscs(0,46,52), - gamePlay.msFromMscs(0,48,57) - }; - - const Uint8* state = SDL_GetKeyboardState(NULL); - - while (gameRunning) - { - #ifdef CI_BUILD - gameRunning = false; - #endif - if (state[SDL_SCANCODE_SPACE]) //Checks the current state of the key and if true it makes the bool to be true (making it not work) unless not press down - keydown = true; - - double currentTime = SDL_GetTicks() - songStartTime; //calculates the delay by comparing the current ticks and when the song starts - - std::string strNum = formatScore(stats.getScore()); - numberTexture = numberFont.renderText(window.getRenderer(), strNum, { 0, 0, 0, 255 }); - number.setTexture(numberTexture); - handX = fisher.getX() + 135; - handY = fisher.getY() + 50; - while (SDL_PollEvent(&event)) - { - if (event.type == SDL_QUIT || event.key.keysym.sym == SDLK_ESCAPE) { //Exit key, stop the geames - gameRunning = false; - break; - } - else if (!keydown && event.key.keysym.sym == SDLK_SPACE) { //Space and down arrow are use to hit or make the clicks - if (!isThrowing) - { - thrown = true; - thrownTimer = 2; - isThrowing = true; - isReturning = false; - throwStartTime = SDL_GetTicks(); - hookStartX = handX; - hookStartY = handY; - hookTargetX = handX + 300; - hookTargetY = handY + 475; - } - int j = 0; - for (int i = 0; i < noteBeats.size(); ++i) { - if (noteHitFlags[i]) - { - j = 0; - continue; - } - - double expected = noteBeats[i]; - double delta = fabs(currentTime - expected); //Calculates the gurrent gap for the hit - if (j == 0) - std::cout << "Delta: " << delta << std::endl; - j++; - if (delta <= gamePlay.getGOOD()) { - short int scoreType = gamePlay.checkHit(expected, currentTime); //This compares the time the SPACE or DOWN was pressed to the time it is requires for the PERFECT or GOOD or Miss - noteHitFlags[i] = true; - fishHits.insert(i); - fishHitTimes[i] = SDL_GetTicks(); // Record when hit occurred - if (scoreType == 2) - { - stats++; - stats.increaseScore(1000); - fishHitTypes[i] = true; - } - else if (scoreType == 1) - { - stats++; - stats.increaseScore(500); - fishHitTypes[i] = false; - } - break; - } - } - } - } - for (int i = 0; i < noteBeats.size(); ++i) { - if (noteHitFlags[i]) continue; - - double noteTime = noteBeats[i]; - if (currentTime > noteTime + gamePlay.getGOOD()) { - std::cout << std::endl << "miss" << std::endl << std::endl; - stats--; - noteHitFlags[i] = true; - } - } - window.clear(); - window.render(ocean); - timeCounter += 0.05; - - // sways around sprites - for (int i = 0; i < NUM_OF_BEATS; i++) - { - sway = static_cast(sin(timeCounter + i) * 1.1); - bob = static_cast(cos(timeCounter + i) * 1.1); - - fish[i].setLoc(fish[i].getX() + sway, fish[i].getY() + bob); + if (!(IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG)) { + throw InitializationException("IMG_Init(PNG) failed: " + std::string(IMG_GetError())); } - - hook.setLoc(hook.getX() + sway, hook.getY() + bob); - boat.setLoc(boat.getX() + sway, boat.getY() + bob); - fisher.setLoc(fisher.getX() + sway, fisher.getY() + bob); - // Hand throwing sprite animation - if (thrown) - { - fisher.setFrame(1, 2); - thrownTimer--; - - if (thrownTimer <= 0) - { - thrown = false; - fisher.setFrame(1, 1); - } + if (TTF_Init() != EXIT_SUCCESS) { + throw InitializationException("TTF_Init failed: " + std::string(TTF_GetError())); } - else - { - fisher.setFrame(1, 1); - } - - // Hook throwing animation - if (isThrowing) - { - Uint32 now = SDL_GetTicks(); - Uint32 elapsed = now - throwStartTime; - - float progress = static_cast(elapsed) / throwDuration; - if (progress >= 1.0f) - { - progress = 1.0f; - - if (!isReturning) - { - isReturning = true; - // makes start the new target - std::swap(hookStartX, hookTargetX); - std::swap(hookStartY, hookTargetY); - } - else - { - isThrowing = false; - isReturning = false; - hook.setLoc(hookStartX, hookStartY); // back to original location - } - } - - int newX = static_cast(hookStartX + (hookTargetX - hookStartX) * progress); - int newY = static_cast(hookStartY + (hookTargetY - hookStartY) * progress); - hook.setLoc(newX, newY); - } - else - { - // Sway + bob when not throwing - hook.setLoc(hook.getX() + sway, hook.getY() + bob); - } - - Uint32 currentTicks = SDL_GetTicks(); - - // render fish - for (int i = 0; i < NUM_OF_BEATS; i++) - { - if (fishHits.count(i)) - { - // Fish was hit calculate time since hit - Uint32 timeSinceHit = currentTicks - fishHitTimes[i]; - - if (timeSinceHit < 1000) - { - // Show text instead of fish for 1 second - SDL_Texture* scoreTex = (fishHitTypes[i]) ? perfectHitTexture : goodHitTexture; - - SDL_Rect textRect; - textRect.x = fish[i].getX(); // Same location as fish - textRect.y = fish[i].getY() - 30; // Slightly above fish - SDL_QueryTexture(scoreTex, NULL, NULL, &textRect.w, &textRect.h); - - SDL_RenderCopy(window.getRenderer(), scoreTex, NULL, &textRect); - } - - continue; // Skip rendering the fish itself - } + + Logger::info("SDL subsystems initialized successfully"); - // Move and render normal fish - fish[i].moveLeft(15); - window.render(fish[i]++); - if (fish[i].getCol() == 4) - fish[i].resetFrame(); // dead fish frames were 4 and on + const auto& config = GameConfig::getInstance(); + const auto& windowConfig = config.getWindowConfig(); + RenderWindow window(windowConfig.title, windowConfig.width, windowConfig.height, windowConfig.flags); + + if (!window.isValid()) { + throw InitializationException("Failed to create render window"); } - - window.render(boat); - window.render(hook); - window.render(fisher); - window.render(score); - window.render(number); - - window.display(); - keydown = false; // prevents holding space - // Break the loop if music stopped playing - if (Mix_PlayingMusic() == 0) - gameRunning = false; - - SDL_Delay(75); - } - player.stopBackgroundMusic(); - player.~Audio(); - scoreFont.unload(); - numberFont.unload(); -} - -void endScreen(RenderWindow& window, bool& gameRunning, SDL_Event& event, GameStats& stats) -{ - bool option = false; - Font logoFont, retryFont, quitFont, - statsFont, scoreFont, numberFont, - hitsFont, numHitsFont, accuracyFont, - accPercentFont, missFont, numMissFont; - - logoFont.load(comicSans, 75); - retryFont.load(comicSans, 65); - quitFont.load(comicSans, 65); - - statsFont.load(comicSans, 55); - scoreFont.load(comicSans, 40); - numberFont.load(comicSans, 40); - hitsFont.load(comicSans, 40); - numHitsFont.load(comicSans, 40); - accuracyFont.load(comicSans, 40); - accPercentFont.load(comicSans, 40); - missFont.load(comicSans, 40); - numMissFont.load(comicSans, 40); - - SDL_Texture* statsTexture = statsFont.renderText(window.getRenderer(), "GAME STATS", YELLOW); - SDL_Texture* scoreTexture = scoreFont.renderText(window.getRenderer(), "SCORE", YELLOW); - SDL_Texture* numberTexture = numberFont.renderText(window.getRenderer(), formatScore(stats.getScore()), YELLOW); - SDL_Texture* hitsTexture = hitsFont.renderText(window.getRenderer(), "HITS", YELLOW); - SDL_Texture* numHitsTexture = numHitsFont.renderText(window.getRenderer(), std::to_string(stats.getHits()), YELLOW); - SDL_Texture* accuracyTexture = accuracyFont.renderText(window.getRenderer(), "ACCURACY", YELLOW); - SDL_Texture* accPercentTexture = accPercentFont.renderText(window.getRenderer(), (std::to_string(stats.getAccuracy()) + "%"), YELLOW); - SDL_Texture* missTexture = missFont.renderText(window.getRenderer(), "MISSES", YELLOW); - SDL_Texture* numMissTexture = numMissFont.renderText(window.getRenderer(), std::to_string(stats.getMisses()), YELLOW); - - SDL_Texture* quitTexture = quitFont.renderText(window.getRenderer(), "QUIT", YELLOW); - SDL_Texture* retryTexture = retryFont.renderText(window.getRenderer(), "RETRY", YELLOW); - SDL_Texture* logoTexture = logoFont.renderText(window.getRenderer(), "MEOWSTRO", YELLOW); - SDL_Texture* selectedTexture = window.loadTexture("../assets/images/select_cat.png"); - SDL_Texture* logoCatTexture = window.loadTexture("../assets/images/menu_cat.png"); - - Entity titleStats(785, 325, statsTexture); - Entity score(650, 400, scoreTexture); - Entity number(1150, 400, numberTexture); - Entity hits(650, 500, hitsTexture); - Entity numHits(1150, 500, numHitsTexture); - Entity accuracy(650, 600, accuracyTexture); - Entity accPercent(1150, 600, accPercentTexture); - Entity misses(650, 700, missTexture); - Entity numMisses(1150, 700, numMissTexture); - - Entity quit(875, 900, quitTexture); - Entity logo(735, 150, logoTexture); - Entity retry(860, 800, retryTexture); - Entity logoCat(675, 0, logoCatTexture); - Sprite selectCat(775, 700, selectedTexture, 1, 1); - - while (!gameRunning) - { - while (SDL_PollEvent(&event)) - { - switch (event.type) - { - case SDL_KEYDOWN: - { - switch (event.key.keysym.sym) - { - case SDLK_ESCAPE: - { - return; - } - case SDLK_SPACE: - { - gameRunning = (option) ? false : true; // op0 = retry op1 = exit - return; - } - case SDLK_UP: - case SDLK_DOWN: - { - option = (option) ? false : true; - break; - } - } - } - } + ResourceManager resourceManager(window.getRenderer()); + if (!resourceManager.isValid()) { + throw InitializationException("Failed to create resource manager"); } - window.clear(); - // if represents selected option render location - if (option) - selectCat.setLoc(775, 900); - else - selectCat.setLoc(775, 775); - - window.render(selectCat); - window.render(logoCat); - window.render(logo); - window.render(retry); - window.render(quit); - window.render(titleStats); - window.render(score); - window.render(number); - window.render(hits); - window.render(numHits); - window.render(accuracy); - window.render(accPercent); - window.render(misses); - window.render(numMisses); - window.display(); - - #ifdef CI_BUILD - gameRunning = false; - return; - #endif + + InputHandler inputHandler; + srand(static_cast(time(NULL))); + + Logger::info("Game systems initialized, starting game loop"); + + // Create the game state manager and run the game + GameStateManager gameStateManager(window, resourceManager, inputHandler); + gameStateManager.run(); + + Logger::info("Game ended successfully"); + + } catch (const InitializationException& e) { + Logger::error(e.what()); + TTF_Quit(); + IMG_Quit(); + SDL_Quit(); + return EXIT_FAILURE; + } catch (const std::exception& e) { + Logger::error("Unexpected error: " + std::string(e.what())); + TTF_Quit(); + IMG_Quit(); + SDL_Quit(); + return EXIT_FAILURE; } -} -std::string formatScore(int score) -{ - std::ostringstream ss; - ss << std::setw(6) << std::setfill('0') << score; - return ss.str(); + TTF_Quit(); + IMG_Quit(); + SDL_Quit(); + return EXIT_SUCCESS; } \ No newline at end of file diff --git a/test.bat b/test.bat new file mode 100644 index 0000000..0cc7ba3 --- /dev/null +++ b/test.bat @@ -0,0 +1,64 @@ +@echo off +REM ===== test.bat ===== +REM Simple test runner for Meowstro + +echo Running Meowstro Tests... + +REM Build the project first (including tests) +echo Building project... +cmake --build build --config Debug + +if %errorlevel% neq 0 ( + echo Build failed! Cannot run tests. + exit /b 1 +) + +echo. +echo Running tests with detailed output... + +REM Find and run the test executable +set "TEST_EXECUTABLE=" +if exist ".\build\bin\Debug\meowstro_tests.exe" ( + set "TEST_EXECUTABLE=.\build\bin\Debug\meowstro_tests.exe" +) else if exist ".\build\bin\Debug\meowstro_tests" ( + set "TEST_EXECUTABLE=.\build\bin\Debug\meowstro_tests" +) else if exist ".\build\bin\meowstro_tests.exe" ( + set "TEST_EXECUTABLE=.\build\bin\meowstro_tests.exe" +) else if exist ".\build\bin\meowstro_tests" ( + set "TEST_EXECUTABLE=.\build\bin\meowstro_tests" +) else ( + echo ❌ Test executable not found! + echo Searched for: + echo - .\build\bin\Debug\meowstro_tests.exe + echo - .\build\bin\Debug\meowstro_tests + echo - .\build\bin\meowstro_tests.exe + echo - .\build\bin\meowstro_tests + exit /b 1 +) + +echo Running: %TEST_EXECUTABLE% +%TEST_EXECUTABLE% + +if %errorlevel% equ 0 ( + echo. + echo ✅ All tests passed! + + REM Also show CTest summary + echo. + echo CTest Summary: + pushd build + ctest --output-on-failure -C Debug + if %errorlevel% neq 0 ( + echo ❌ CTest failed! + popd + exit /b 1 + ) + popd +) else ( + echo. + echo ❌ Some tests failed! + exit /b 1 +) + +echo. +echo Testing complete! \ No newline at end of file diff --git a/test.ps1 b/test.ps1 new file mode 100644 index 0000000..716ea3d --- /dev/null +++ b/test.ps1 @@ -0,0 +1,69 @@ +# ===== test.ps1 ===== +# Simple test runner for Meowstro + +Write-Host "Running Meowstro Tests..." -ForegroundColor Cyan + +# Build the project first (including tests) +Write-Host "Building project..." -ForegroundColor Yellow +cmake --build build --config Debug + +if ($LASTEXITCODE -ne 0) { + Write-Host "Build failed! Cannot run tests." -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "Running tests with detailed output..." -ForegroundColor Green + +# Find and run the test executable +$testExecutablePaths = @( + ".\build\bin\Debug\meowstro_tests.exe", + ".\build\bin\Debug\meowstro_tests", + ".\build\bin\meowstro_tests.exe", + ".\build\bin\meowstro_tests" +) + +$testExecutable = $null +foreach ($path in $testExecutablePaths) { + if (Test-Path $path) { + $testExecutable = $path + break + } +} + +if (-not $testExecutable) { + Write-Host "Test executable not found!" -ForegroundColor Red + Write-Host "Searched for:" -ForegroundColor Red + foreach ($path in $testExecutablePaths) { + Write-Host " - $path" -ForegroundColor Red + } + exit 1 +} + +Write-Host "Running: $testExecutable" -ForegroundColor Green +& $testExecutable +$testResult = $LASTEXITCODE + +Write-Host "" +if ($testResult -eq 0) { + Write-Host "All tests passed!" -ForegroundColor Green + + # Also show CTest summary + Write-Host "" + Write-Host "CTest Summary:" -ForegroundColor Cyan + Push-Location build + ctest --output-on-failure -C Debug + $ctestResult = $LASTEXITCODE + Pop-Location + + if ($ctestResult -ne 0) { + Write-Host "CTest failed!" -ForegroundColor Red + exit 1 + } +} else { + Write-Host "Some tests failed!" -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "Testing complete!" -ForegroundColor Magenta \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..7b5c82f --- /dev/null +++ b/test.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# ===== test.sh ===== +# Simple test runner for Meowstro + +# Colors for better output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +NC='\033[0m' # No Color + +echo -e "${CYAN}Running Meowstro Tests...${NC}" + +# Build the project first (including tests) +echo -e "${YELLOW}Building project...${NC}" +# Use --config Debug to ensure consistent build type across platforms +cmake --build build --config Debug + +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Build failed! Cannot run tests.${NC}" + exit 1 +fi + +echo "" +echo -e "${GREEN}Running tests with detailed output...${NC}" + +# Find and run the test executable +if [ -f "./build/bin/Debug/meowstro_tests" ]; then + TEST_EXECUTABLE="./build/bin/Debug/meowstro_tests" +elif [ -f "./build/bin/Debug/meowstro_tests.exe" ]; then + TEST_EXECUTABLE="./build/bin/Debug/meowstro_tests.exe" +elif [ -f "./build/bin/meowstro_tests" ]; then + TEST_EXECUTABLE="./build/bin/meowstro_tests" +else + echo -e "${RED}❌ Test executable not found!${NC}" + echo "Searched for:" + echo " - ./build/bin/Debug/meowstro_tests" + echo " - ./build/bin/Debug/meowstro_tests.exe" + echo " - ./build/bin/meowstro_tests" + exit 1 +fi + +echo "Running: $TEST_EXECUTABLE" +$TEST_EXECUTABLE + +if [ $? -eq 0 ]; then + echo "" + echo -e "${GREEN}✅ All tests passed!${NC}" + + # Also show CTest summary + echo "" + echo -e "${CYAN}CTest Summary:${NC}" + cd build + # Try with -C Debug first (Windows), fall back to without config (Linux/macOS) + ctest --output-on-failure -C Debug 2>/dev/null || ctest --output-on-failure + CTEST_RESULT=$? + cd .. + + if [ $CTEST_RESULT -ne 0 ]; then + echo -e "${RED}❌ CTest failed!${NC}" + exit 1 + fi +else + echo "" + echo -e "${RED}❌ Some tests failed!${NC}" + exit 1 +fi + +echo "" +echo -e "${MAGENTA}Testing complete!${NC}" \ No newline at end of file diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 0000000..2dc3787 --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,6 @@ +#include + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/tests/unit/test_Entity.cpp b/tests/unit/test_Entity.cpp new file mode 100644 index 0000000..3ae4ff8 --- /dev/null +++ b/tests/unit/test_Entity.cpp @@ -0,0 +1,319 @@ +#include +#include +#include +#include "Entity.hpp" + +// Test fixture for Entity tests that handles SDL initialization +class EntityTest : public ::testing::Test { +protected: + void SetUp() override { + // Initialize SDL for texture operations + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + FAIL() << "SDL_Init failed: " << SDL_GetError(); + } + + if (!(IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG)) { + SDL_Quit(); + FAIL() << "IMG_Init failed: " << IMG_GetError(); + } + + // Create a minimal window and renderer for texture operations + window = SDL_CreateWindow("Test Window", + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + 100, 100, + SDL_WINDOW_HIDDEN); + if (!window) { + IMG_Quit(); + SDL_Quit(); + FAIL() << "SDL_CreateWindow failed: " << SDL_GetError(); + } + + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE); + if (!renderer) { + SDL_DestroyWindow(window); + IMG_Quit(); + SDL_Quit(); + FAIL() << "SDL_CreateRenderer failed: " << SDL_GetError(); + } + } + + void TearDown() override { + if (testTexture) { + SDL_DestroyTexture(testTexture); + } + if (renderer) { + SDL_DestroyRenderer(renderer); + } + if (window) { + SDL_DestroyWindow(window); + } + IMG_Quit(); + SDL_Quit(); + } + + // Helper function to create a test texture + SDL_Texture* createTestTexture(int width, int height) { + SDL_Surface* surface = SDL_CreateRGBSurface(0, width, height, 32, 0, 0, 0, 0); + if (!surface) return nullptr; + + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + SDL_FreeSurface(surface); + return texture; + } + + SDL_Window* window = nullptr; + SDL_Renderer* renderer = nullptr; + SDL_Texture* testTexture = nullptr; +}; + +// Test Entity construction with valid texture +TEST_F(EntityTest, ConstructorWithValidTexture) { + testTexture = createTestTexture(64, 48); + ASSERT_NE(testTexture, nullptr); + + Entity entity(10.5f, 20.3f, testTexture); + + EXPECT_FLOAT_EQ(entity.getX(), 10.5f); + EXPECT_FLOAT_EQ(entity.getY(), 20.3f); + EXPECT_EQ(entity.getTexture(), testTexture); + + SDL_Rect frame = entity.getCurrentFrame(); + EXPECT_EQ(frame.x, 0); + EXPECT_EQ(frame.y, 0); + EXPECT_EQ(frame.w, 64); // Should match texture width + EXPECT_EQ(frame.h, 48); // Should match texture height +} + +// Test Entity construction with null texture +TEST_F(EntityTest, ConstructorWithNullTexture) { + Entity entity(15.7f, 25.9f, nullptr); + + EXPECT_FLOAT_EQ(entity.getX(), 15.7f); + EXPECT_FLOAT_EQ(entity.getY(), 25.9f); + EXPECT_EQ(entity.getTexture(), nullptr); + + SDL_Rect frame = entity.getCurrentFrame(); + EXPECT_EQ(frame.x, 0); + EXPECT_EQ(frame.y, 0); + EXPECT_EQ(frame.w, 0); // Should be 0 for null texture + EXPECT_EQ(frame.h, 0); // Should be 0 for null texture +} + +// Test Entity construction with zero coordinates +TEST_F(EntityTest, ConstructorWithZeroCoordinates) { + testTexture = createTestTexture(32, 32); + ASSERT_NE(testTexture, nullptr); + + Entity entity(0.0f, 0.0f, testTexture); + + EXPECT_FLOAT_EQ(entity.getX(), 0.0f); + EXPECT_FLOAT_EQ(entity.getY(), 0.0f); + EXPECT_EQ(entity.getTexture(), testTexture); +} + +// Test Entity construction with negative coordinates +TEST_F(EntityTest, ConstructorWithNegativeCoordinates) { + testTexture = createTestTexture(16, 16); + ASSERT_NE(testTexture, nullptr); + + Entity entity(-10.5f, -20.3f, testTexture); + + EXPECT_FLOAT_EQ(entity.getX(), -10.5f); + EXPECT_FLOAT_EQ(entity.getY(), -20.3f); + EXPECT_EQ(entity.getTexture(), testTexture); +} + +// Test setTexture functionality +TEST_F(EntityTest, SetTexture) { + Entity entity(5.0f, 10.0f, nullptr); + EXPECT_EQ(entity.getTexture(), nullptr); + + testTexture = createTestTexture(128, 96); + ASSERT_NE(testTexture, nullptr); + + entity.setTexture(testTexture); + EXPECT_EQ(entity.getTexture(), testTexture); + + // Setting to nullptr should also work + entity.setTexture(nullptr); + EXPECT_EQ(entity.getTexture(), nullptr); +} + +// Test getCurrentFrame returns copy, not reference +TEST_F(EntityTest, GetCurrentFrameReturnsCopy) { + testTexture = createTestTexture(50, 60); + ASSERT_NE(testTexture, nullptr); + + Entity entity(0.0f, 0.0f, testTexture); + + SDL_Rect frame1 = entity.getCurrentFrame(); + SDL_Rect frame2 = entity.getCurrentFrame(); + + // Should be equal values + EXPECT_EQ(frame1.x, frame2.x); + EXPECT_EQ(frame1.y, frame2.y); + EXPECT_EQ(frame1.w, frame2.w); + EXPECT_EQ(frame1.h, frame2.h); + + // But different memory addresses (copies) + EXPECT_NE(&frame1, &frame2); + + // Modifying one shouldn't affect the other + frame1.w = 999; + frame2 = entity.getCurrentFrame(); + EXPECT_NE(frame1.w, frame2.w); + EXPECT_EQ(frame2.w, 50); +} + +// Test setCurrentFrameW +TEST_F(EntityTest, SetCurrentFrameW) { + testTexture = createTestTexture(32, 32); + ASSERT_NE(testTexture, nullptr); + + Entity entity(0.0f, 0.0f, testTexture); + + SDL_Rect originalFrame = entity.getCurrentFrame(); + EXPECT_EQ(originalFrame.w, 32); + + entity.setCurrentFrameW(64); + SDL_Rect newFrame = entity.getCurrentFrame(); + EXPECT_EQ(newFrame.w, 64); + EXPECT_EQ(newFrame.h, 32); // Height should remain unchanged + EXPECT_EQ(newFrame.x, 0); // X should remain unchanged + EXPECT_EQ(newFrame.y, 0); // Y should remain unchanged +} + +// Test setCurrentFrameH +TEST_F(EntityTest, SetCurrentFrameH) { + testTexture = createTestTexture(32, 32); + ASSERT_NE(testTexture, nullptr); + + Entity entity(0.0f, 0.0f, testTexture); + + SDL_Rect originalFrame = entity.getCurrentFrame(); + EXPECT_EQ(originalFrame.h, 32); + + entity.setCurrentFrameH(128); + SDL_Rect newFrame = entity.getCurrentFrame(); + EXPECT_EQ(newFrame.h, 128); + EXPECT_EQ(newFrame.w, 32); // Width should remain unchanged + EXPECT_EQ(newFrame.x, 0); // X should remain unchanged + EXPECT_EQ(newFrame.y, 0); // Y should remain unchanged +} + +// Test setting frame dimensions to zero +TEST_F(EntityTest, SetFrameDimensionsToZero) { + testTexture = createTestTexture(100, 100); + ASSERT_NE(testTexture, nullptr); + + Entity entity(0.0f, 0.0f, testTexture); + + entity.setCurrentFrameW(0); + entity.setCurrentFrameH(0); + + SDL_Rect frame = entity.getCurrentFrame(); + EXPECT_EQ(frame.w, 0); + EXPECT_EQ(frame.h, 0); +} + +// Test setting negative frame dimensions +TEST_F(EntityTest, SetNegativeFrameDimensions) { + testTexture = createTestTexture(50, 50); + ASSERT_NE(testTexture, nullptr); + + Entity entity(0.0f, 0.0f, testTexture); + + entity.setCurrentFrameW(-10); + entity.setCurrentFrameH(-20); + + SDL_Rect frame = entity.getCurrentFrame(); + EXPECT_EQ(frame.w, -10); // Should accept negative values + EXPECT_EQ(frame.h, -20); // Should accept negative values +} + +// Test multiple frame dimension changes +TEST_F(EntityTest, MultipleFrameDimensionChanges) { + testTexture = createTestTexture(25, 25); + ASSERT_NE(testTexture, nullptr); + + Entity entity(0.0f, 0.0f, testTexture); + + // Test multiple width changes + entity.setCurrentFrameW(50); + EXPECT_EQ(entity.getCurrentFrame().w, 50); + + entity.setCurrentFrameW(75); + EXPECT_EQ(entity.getCurrentFrame().w, 75); + + entity.setCurrentFrameW(100); + EXPECT_EQ(entity.getCurrentFrame().w, 100); + + // Test multiple height changes + entity.setCurrentFrameH(30); + EXPECT_EQ(entity.getCurrentFrame().h, 30); + + entity.setCurrentFrameH(60); + EXPECT_EQ(entity.getCurrentFrame().h, 60); + + entity.setCurrentFrameH(90); + EXPECT_EQ(entity.getCurrentFrame().h, 90); + + // Final state check + SDL_Rect finalFrame = entity.getCurrentFrame(); + EXPECT_EQ(finalFrame.w, 100); + EXPECT_EQ(finalFrame.x, 0); + EXPECT_EQ(finalFrame.y, 0); + EXPECT_EQ(finalFrame.h, 90); +} + +// Test with different texture sizes +TEST_F(EntityTest, DifferentTextureSizes) { + // Test very small texture + SDL_Texture* smallTexture = createTestTexture(1, 1); + ASSERT_NE(smallTexture, nullptr); + + Entity smallEntity(0.0f, 0.0f, smallTexture); + SDL_Rect smallFrame = smallEntity.getCurrentFrame(); + EXPECT_EQ(smallFrame.w, 1); + EXPECT_EQ(smallFrame.h, 1); + + SDL_DestroyTexture(smallTexture); + + // Test large texture + SDL_Texture* largeTexture = createTestTexture(512, 256); + ASSERT_NE(largeTexture, nullptr); + + Entity largeEntity(0.0f, 0.0f, largeTexture); + SDL_Rect largeFrame = largeEntity.getCurrentFrame(); + EXPECT_EQ(largeFrame.w, 512); + EXPECT_EQ(largeFrame.h, 256); + + SDL_DestroyTexture(largeTexture); + + // Test non-square texture + SDL_Texture* rectTexture = createTestTexture(200, 50); + ASSERT_NE(rectTexture, nullptr); + + Entity rectEntity(0.0f, 0.0f, rectTexture); + SDL_Rect rectFrame = rectEntity.getCurrentFrame(); + EXPECT_EQ(rectFrame.w, 200); + EXPECT_EQ(rectFrame.h, 50); + + SDL_DestroyTexture(rectTexture); +} + +// Test Entity with floating point precision +TEST_F(EntityTest, FloatingPointPrecision) { + testTexture = createTestTexture(32, 32); + ASSERT_NE(testTexture, nullptr); + + float preciseX = 10.123456789f; + float preciseY = 20.987654321f; + + Entity entity(preciseX, preciseY, testTexture); + + // Should maintain reasonable floating point precision + EXPECT_NEAR(entity.getX(), preciseX, 0.000001f); + EXPECT_NEAR(entity.getY(), preciseY, 0.000001f); +} \ No newline at end of file diff --git a/tests/unit/test_GameConfig.cpp b/tests/unit/test_GameConfig.cpp new file mode 100644 index 0000000..60c3b4f --- /dev/null +++ b/tests/unit/test_GameConfig.cpp @@ -0,0 +1,273 @@ +#include +#include "GameConfig.hpp" + +// Test fixture for GameConfig tests +class GameConfigTest : public ::testing::Test { +protected: + void SetUp() override { + // Get reference to the singleton instance + config = &GameConfig::getInstance(); + } + + GameConfig* config; +}; + +// Test singleton pattern - should return same instance +TEST_F(GameConfigTest, SingletonPattern) { + GameConfig& instance1 = GameConfig::getInstance(); + GameConfig& instance2 = GameConfig::getInstance(); + + // Should be the same instance + EXPECT_EQ(&instance1, &instance2); + EXPECT_EQ(config, &instance1); +} + +// Test WindowConfig default values +TEST_F(GameConfigTest, WindowConfigDefaults) { + const auto& windowConfig = config->getWindowConfig(); + + EXPECT_STREQ(windowConfig.title, "Meowstro"); + EXPECT_EQ(windowConfig.width, 1920); + EXPECT_EQ(windowConfig.height, 1080); + EXPECT_EQ(windowConfig.flags, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); +} + +// Test AudioConfig default values +TEST_F(GameConfigTest, AudioConfigDefaults) { + const auto& audioConfig = config->getAudioConfig(); + + EXPECT_EQ(audioConfig.bpm, 147); + EXPECT_DOUBLE_EQ(audioConfig.travelDuration, 2000.0); + EXPECT_EQ(audioConfig.backgroundMusicPath, "./assets/audio/meowstro_short_ver.mp3"); +} + +// Test VisualConfig default values +TEST_F(GameConfigTest, VisualConfigDefaults) { + const auto& visualConfig = config->getVisualConfig(); + + // Test colors + EXPECT_EQ(visualConfig.YELLOW.r, 255); + EXPECT_EQ(visualConfig.YELLOW.g, 255); + EXPECT_EQ(visualConfig.YELLOW.b, 100); + EXPECT_EQ(visualConfig.YELLOW.a, 255); + + EXPECT_EQ(visualConfig.BLACK.r, 0); + EXPECT_EQ(visualConfig.BLACK.g, 0); + EXPECT_EQ(visualConfig.BLACK.b, 0); + EXPECT_EQ(visualConfig.BLACK.a, 255); + + EXPECT_EQ(visualConfig.RED.r, 255); + EXPECT_EQ(visualConfig.RED.g, 0); + EXPECT_EQ(visualConfig.RED.b, 0); + EXPECT_EQ(visualConfig.RED.a, 255); + + EXPECT_EQ(visualConfig.frameDelay, 75); +} + +// Test AssetPaths default values +TEST_F(GameConfigTest, AssetPathsDefaults) { + const auto& assetPaths = config->getAssetPaths(); + + // Font path + EXPECT_EQ(assetPaths.fontPath, "./assets/fonts/Comic Sans MS.ttf"); + + // Main textures + EXPECT_EQ(assetPaths.oceanTexture, "./assets/images/Ocean.png"); + EXPECT_EQ(assetPaths.boatTexture, "./assets/images/boat.png"); + EXPECT_EQ(assetPaths.fisherTexture, "./assets/images/fisher.png"); + EXPECT_EQ(assetPaths.hookTexture, "./assets/images/hook.png"); + EXPECT_EQ(assetPaths.menuCatTexture, "./assets/images/menu_cat.png"); + EXPECT_EQ(assetPaths.selectCatTexture, "./assets/images/select_cat.png"); + + // Fish textures + EXPECT_EQ(assetPaths.blueFishTexture, "./assets/images/blue_fish.png"); + EXPECT_EQ(assetPaths.greenFishTexture, "./assets/images/green_fish.png"); + EXPECT_EQ(assetPaths.goldFishTexture, "./assets/images/gold_fish.png"); +} + +// Test GameplayConfig default values +TEST_F(GameConfigTest, GameplayConfigDefaults) { + const auto& gameplayConfig = config->getGameplayConfig(); + + EXPECT_EQ(gameplayConfig.numBeats, 25); + EXPECT_EQ(gameplayConfig.numFishTextures, 3); + EXPECT_EQ(gameplayConfig.throwDuration, 200); + EXPECT_EQ(gameplayConfig.hookTargetX, 650); + EXPECT_EQ(gameplayConfig.hookTargetY, 625); + EXPECT_EQ(gameplayConfig.fishTargetX, 660); + + // Test fish start locations + const auto& fishLocations = gameplayConfig.fishStartXLocations; + EXPECT_EQ(fishLocations.size(), 25); + + // Test some specific locations + EXPECT_EQ(fishLocations[0], 1352); + EXPECT_EQ(fishLocations[1], 2350); + EXPECT_EQ(fishLocations[24], 10160); // Last element + + // Test that all values are positive + for (int location : fishLocations) { + EXPECT_GT(location, 0); + } + + // Test that locations are generally increasing (rough check) + EXPECT_LT(fishLocations[0], fishLocations[24]); +} + +// Test FontSizes default values +TEST_F(GameConfigTest, FontSizesDefaults) { + const auto& fontSizes = config->getFontSizes(); + + EXPECT_EQ(fontSizes.menuLogo, 75); + EXPECT_EQ(fontSizes.menuButtons, 55); + EXPECT_EQ(fontSizes.quitButton, 65); + EXPECT_EQ(fontSizes.gameScore, 40); + EXPECT_EQ(fontSizes.gameNumbers, 35); + EXPECT_EQ(fontSizes.gameStats, 55); + EXPECT_EQ(fontSizes.hitFeedback, 30); + + // Test that all font sizes are positive + EXPECT_GT(fontSizes.menuLogo, 0); + EXPECT_GT(fontSizes.menuButtons, 0); + EXPECT_GT(fontSizes.quitButton, 0); + EXPECT_GT(fontSizes.gameScore, 0); + EXPECT_GT(fontSizes.gameNumbers, 0); + EXPECT_GT(fontSizes.gameStats, 0); + EXPECT_GT(fontSizes.hitFeedback, 0); +} + +// Test beat timing initialization +TEST_F(GameConfigTest, BeatTimingInitialization) { + // Before initialization, noteBeats should be empty + const auto& gameplayConfig = config->getGameplayConfig(); + + // Initialize beat timings + config->initializeBeatTimings(); + + // After initialization, should have 25 beats + const auto& noteBeats = gameplayConfig.noteBeats; + EXPECT_EQ(noteBeats.size(), 25); + + // Test that all beat times are positive + for (double beat : noteBeats) { + EXPECT_GT(beat, 0.0); + } + + // Test that beat times are generally increasing + for (size_t i = 1; i < noteBeats.size(); ++i) { + EXPECT_GE(noteBeats[i], noteBeats[i-1]); + } +} + +// Test multiple calls to initializeBeatTimings doesn't reinitialize +TEST_F(GameConfigTest, BeatTimingInitializationIdempotent) { + config->initializeBeatTimings(); + + const auto& gameplayConfig = config->getGameplayConfig(); + const auto& noteBeats = gameplayConfig.noteBeats; + + // Store the first beat time + double firstBeat = noteBeats[0]; + + // Call initialization again + config->initializeBeatTimings(); + + // Should still have same values + EXPECT_EQ(noteBeats.size(), 25); + EXPECT_DOUBLE_EQ(noteBeats[0], firstBeat); +} + +// Test consistency between numBeats and fish locations +TEST_F(GameConfigTest, ConsistencyBetweenBeatsAndFishLocations) { + const auto& gameplayConfig = config->getGameplayConfig(); + + EXPECT_EQ(gameplayConfig.numBeats, gameplayConfig.fishStartXLocations.size()); + + config->initializeBeatTimings(); + EXPECT_EQ(gameplayConfig.numBeats, gameplayConfig.noteBeats.size()); + EXPECT_EQ(gameplayConfig.fishStartXLocations.size(), gameplayConfig.noteBeats.size()); +} + +// Test that configuration values are reasonable for a game +TEST_F(GameConfigTest, ReasonableConfigurationValues) { + const auto& windowConfig = config->getWindowConfig(); + const auto& audioConfig = config->getAudioConfig(); + const auto& visualConfig = config->getVisualConfig(); + const auto& gameplayConfig = config->getGameplayConfig(); + + // Window dimensions should be reasonable + EXPECT_GE(windowConfig.width, 800); + EXPECT_GE(windowConfig.height, 600); + EXPECT_LE(windowConfig.width, 4000); + EXPECT_LE(windowConfig.height, 4000); + + // BPM should be reasonable for music + EXPECT_GE(audioConfig.bpm, 60); + EXPECT_LE(audioConfig.bpm, 300); + + // Travel duration should be positive + EXPECT_GT(audioConfig.travelDuration, 0.0); + + // Frame delay should be reasonable + EXPECT_GE(visualConfig.frameDelay, 1); + EXPECT_LE(visualConfig.frameDelay, 1000); + + // Game mechanics should be reasonable + EXPECT_GT(gameplayConfig.numBeats, 0); + EXPECT_LE(gameplayConfig.numBeats, 100); + EXPECT_GT(gameplayConfig.numFishTextures, 0); + EXPECT_GT(gameplayConfig.throwDuration, 0); +} + +// Test path strings are not empty +TEST_F(GameConfigTest, PathsNotEmpty) { + const auto& assetPaths = config->getAssetPaths(); + const auto& audioConfig = config->getAudioConfig(); + + EXPECT_FALSE(assetPaths.fontPath.empty()); + EXPECT_FALSE(assetPaths.oceanTexture.empty()); + EXPECT_FALSE(assetPaths.boatTexture.empty()); + EXPECT_FALSE(assetPaths.fisherTexture.empty()); + EXPECT_FALSE(assetPaths.hookTexture.empty()); + EXPECT_FALSE(assetPaths.menuCatTexture.empty()); + EXPECT_FALSE(assetPaths.selectCatTexture.empty()); + EXPECT_FALSE(assetPaths.blueFishTexture.empty()); + EXPECT_FALSE(assetPaths.greenFishTexture.empty()); + EXPECT_FALSE(assetPaths.goldFishTexture.empty()); + EXPECT_FALSE(audioConfig.backgroundMusicPath.empty()); +} + +// Test SDL color values are within valid range +TEST_F(GameConfigTest, ColorValuesValid) { + const auto& visualConfig = config->getVisualConfig(); + + // Test YELLOW color + EXPECT_GE(visualConfig.YELLOW.r, 0); + EXPECT_LE(visualConfig.YELLOW.r, 255); + EXPECT_GE(visualConfig.YELLOW.g, 0); + EXPECT_LE(visualConfig.YELLOW.g, 255); + EXPECT_GE(visualConfig.YELLOW.b, 0); + EXPECT_LE(visualConfig.YELLOW.b, 255); + EXPECT_GE(visualConfig.YELLOW.a, 0); + EXPECT_LE(visualConfig.YELLOW.a, 255); + + // Test BLACK color + EXPECT_GE(visualConfig.BLACK.r, 0); + EXPECT_LE(visualConfig.BLACK.r, 255); + EXPECT_GE(visualConfig.BLACK.g, 0); + EXPECT_LE(visualConfig.BLACK.g, 255); + EXPECT_GE(visualConfig.BLACK.b, 0); + EXPECT_LE(visualConfig.BLACK.b, 255); + EXPECT_GE(visualConfig.BLACK.a, 0); + EXPECT_LE(visualConfig.BLACK.a, 255); + + // Test RED color + EXPECT_GE(visualConfig.RED.r, 0); + EXPECT_LE(visualConfig.RED.r, 255); + EXPECT_GE(visualConfig.RED.g, 0); + EXPECT_LE(visualConfig.RED.g, 255); + EXPECT_GE(visualConfig.RED.b, 0); + EXPECT_LE(visualConfig.RED.b, 255); + EXPECT_GE(visualConfig.RED.a, 0); + EXPECT_LE(visualConfig.RED.a, 255); +} \ No newline at end of file diff --git a/tests/unit/test_GameStats.cpp b/tests/unit/test_GameStats.cpp new file mode 100644 index 0000000..5bf3dee --- /dev/null +++ b/tests/unit/test_GameStats.cpp @@ -0,0 +1,177 @@ +#include +#include +#include "GameStats.hpp" + +// Basic smoke test to verify Google Test setup is working +TEST(GameStatsTest, CanCreateGameStats) { + GameStats stats; + EXPECT_EQ(stats.getScore(), 0); + EXPECT_EQ(stats.getHits(), 0); + EXPECT_EQ(stats.getMisses(), 0); + EXPECT_EQ(stats.getCombo(), 0); + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 0.0); +} + +// Test the parameterized constructor +TEST(GameStatsTest, ParameterizedConstructor) { + GameStats stats(100, 5, 8, 2); + EXPECT_EQ(stats.getScore(), 100); + EXPECT_EQ(stats.getCombo(), 5); + EXPECT_EQ(stats.getHits(), 8); + EXPECT_EQ(stats.getMisses(), 2); + // 8 hits out of 10 total = 80% accuracy + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 80.0); +} + +// Test accuracy calculation edge cases +TEST(GameStatsTest, AccuracyWithZeroHits) { + GameStats stats(0, 0, 0, 5); + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 0.0); +} + +TEST(GameStatsTest, AccuracyWithZeroMisses) { + GameStats stats(0, 0, 5, 0); + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 100.0); +} + +TEST(GameStatsTest, AccuracyWithZeroHitsAndMisses) { + GameStats stats(0, 0, 0, 0); + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 0.0); +} + +// Test setters and getters +TEST(GameStatsTest, SettersAndGetters) { + GameStats stats; + + stats.setScore(250); + EXPECT_EQ(stats.getScore(), 250); + + stats.setCombo(15); + EXPECT_EQ(stats.getCombo(), 15); + + stats.setHits(20); + EXPECT_EQ(stats.getHits(), 20); + + stats.setMisses(5); + EXPECT_EQ(stats.getMisses(), 5); + + stats.setAccuracy(85.5); + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 85.5); +} + +// Test increaseScore functionality +TEST(GameStatsTest, IncreaseScore) { + GameStats stats; + stats.setScore(100); + + stats.increaseScore(50); + EXPECT_EQ(stats.getScore(), 150); + + stats.increaseScore(25); + EXPECT_EQ(stats.getScore(), 175); + + // Test with negative values + stats.increaseScore(-25); + EXPECT_EQ(stats.getScore(), 150); +} + +// Test resetStats functionality +TEST(GameStatsTest, ResetStats) { + GameStats stats(100, 10, 15, 3); + EXPECT_NE(stats.getScore(), 0); + EXPECT_NE(stats.getCombo(), 0); + EXPECT_NE(stats.getHits(), 0); + EXPECT_NE(stats.getMisses(), 0); + + stats.resetStats(); + EXPECT_EQ(stats.getScore(), 0); + EXPECT_EQ(stats.getCombo(), 0); + EXPECT_EQ(stats.getHits(), 0); + EXPECT_EQ(stats.getMisses(), 0); + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 0.0); +} + +// Test post-increment operator (adds hit) +TEST(GameStatsTest, PostIncrementOperator) { + GameStats stats; + stats.setHits(5); + stats.setMisses(2); + + GameStats result = stats++; + + // The implementation modifies the object and returns it + // Both result and stats should have the new values + EXPECT_EQ(result.getHits(), 6); + EXPECT_EQ(stats.getHits(), 6); + EXPECT_EQ(stats.getMisses(), 2); + + // Accuracy should be recalculated: 6/(6+2) = 75% + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 75.0); +} + +// Test post-decrement operator (adds miss) +TEST(GameStatsTest, PostDecrementOperator) { + GameStats stats; + stats.setHits(8); + stats.setMisses(1); + + GameStats result = stats--; + + // The implementation modifies the object and returns it + // Both result and stats should have the new values + EXPECT_EQ(result.getMisses(), 2); + EXPECT_EQ(stats.getHits(), 8); + EXPECT_EQ(stats.getMisses(), 2); + + // Accuracy should be recalculated: 8/(8+2) = 80% + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 80.0); +} + +// Test post-decrement with zero hits (edge case) +TEST(GameStatsTest, PostDecrementWithZeroHits) { + GameStats stats; + stats.setHits(0); + stats.setMisses(2); + + stats--; + + EXPECT_EQ(stats.getHits(), 0); + EXPECT_EQ(stats.getMisses(), 3); + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 0.0); +} + +// Test stream output operator +TEST(GameStatsTest, StreamOutputOperator) { + GameStats stats(500, 25, 18, 2); + std::ostringstream oss; + + oss << stats; + std::string output = oss.str(); + + // Check that key information is present in output + EXPECT_TRUE(output.find("Final Stats") != std::string::npos); + EXPECT_TRUE(output.find("Score: 500") != std::string::npos); + EXPECT_TRUE(output.find("Combo: 25") != std::string::npos); + EXPECT_TRUE(output.find("Accuracy: 90") != std::string::npos); + EXPECT_TRUE(output.find("Well done!") != std::string::npos); +} + +// Test stream output with low accuracy +TEST(GameStatsTest, StreamOutputLowAccuracy) { + GameStats stats(100, 5, 2, 8); // 20% accuracy + std::ostringstream oss; + + oss << stats; + std::string output = oss.str(); + + EXPECT_TRUE(output.find("Oh.. thats kinda bad..") != std::string::npos); +} + +// Test accuracy calculation precision +TEST(GameStatsTest, AccuracyPrecision) { + GameStats stats(0, 0, 1, 3); // 1/(1+3) = 0.25 = 25% + EXPECT_DOUBLE_EQ(stats.getAccuracy(), 25.0); + + GameStats stats2(0, 0, 2, 1); // 2/(2+1) = 0.6667 = 66.67% + EXPECT_NEAR(stats2.getAccuracy(), 66.666666666666671, 0.000001); +} \ No newline at end of file diff --git a/tests/unit/test_InputHandler.cpp b/tests/unit/test_InputHandler.cpp new file mode 100644 index 0000000..b81e4fa --- /dev/null +++ b/tests/unit/test_InputHandler.cpp @@ -0,0 +1,316 @@ +#include +#include +#include "InputHandler.hpp" + +// Test fixture for InputHandler tests that handles SDL initialization +class InputHandlerTest : public ::testing::Test { +protected: + void SetUp() override { + // Initialize SDL for input handling + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + FAIL() << "SDL_Init failed: " << SDL_GetError(); + } + + // Create minimal window for SDL context + window = SDL_CreateWindow("Test Window", + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + 100, 100, + SDL_WINDOW_HIDDEN); + if (!window) { + SDL_Quit(); + FAIL() << "SDL_CreateWindow failed: " << SDL_GetError(); + } + + inputHandler = std::make_unique(); + } + + void TearDown() override { + inputHandler.reset(); + if (window) { + SDL_DestroyWindow(window); + } + SDL_Quit(); + } + + // Helper to create SDL_Event structures + SDL_Event createKeyDownEvent(SDL_Keycode key) { + SDL_Event event; + event.type = SDL_KEYDOWN; + event.key.keysym.sym = key; + event.key.repeat = 0; + return event; + } + + SDL_Event createKeyUpEvent(SDL_Keycode key) { + SDL_Event event; + event.type = SDL_KEYUP; + event.key.keysym.sym = key; + return event; + } + + SDL_Event createQuitEvent() { + SDL_Event event; + event.type = SDL_QUIT; + return event; + } + + SDL_Window* window = nullptr; + std::unique_ptr inputHandler; +}; + +// Test InputHandler construction +TEST_F(InputHandlerTest, Construction) { + EXPECT_NE(inputHandler.get(), nullptr); + EXPECT_FALSE(inputHandler->isSpaceHeld()); +} + +// Test SDL_QUIT event handling (universal) +TEST_F(InputHandlerTest, QuitEventUniversal) { + SDL_Event quitEvent = createQuitEvent(); + + // Should return Quit regardless of game state + EXPECT_EQ(inputHandler->processInput(quitEvent, GameState::MainMenu), InputAction::Quit); + EXPECT_EQ(inputHandler->processInput(quitEvent, GameState::Playing), InputAction::Quit); + EXPECT_EQ(inputHandler->processInput(quitEvent, GameState::EndScreen), InputAction::Quit); +} + +// Test MainMenu input handling +TEST_F(InputHandlerTest, MainMenuInputHandling) { + // Test ESCAPE key + SDL_Event escapeEvent = createKeyDownEvent(SDLK_ESCAPE); + EXPECT_EQ(inputHandler->processInput(escapeEvent, GameState::MainMenu), InputAction::Quit); + + // Test SPACE key + SDL_Event spaceEvent = createKeyDownEvent(SDLK_SPACE); + EXPECT_EQ(inputHandler->processInput(spaceEvent, GameState::MainMenu), InputAction::Select); + + // Test UP arrow + SDL_Event upEvent = createKeyDownEvent(SDLK_UP); + EXPECT_EQ(inputHandler->processInput(upEvent, GameState::MainMenu), InputAction::MenuUp); + + // Test DOWN arrow + SDL_Event downEvent = createKeyDownEvent(SDLK_DOWN); + EXPECT_EQ(inputHandler->processInput(downEvent, GameState::MainMenu), InputAction::MenuDown); + + // Test unmapped key + SDL_Event randomEvent = createKeyDownEvent(SDLK_a); + EXPECT_EQ(inputHandler->processInput(randomEvent, GameState::MainMenu), InputAction::None); +} + +// Test Playing state input handling +TEST_F(InputHandlerTest, PlayingStateInputHandling) { + // Test ESCAPE key + SDL_Event escapeEvent = createKeyDownEvent(SDLK_ESCAPE); + EXPECT_EQ(inputHandler->processInput(escapeEvent, GameState::Playing), InputAction::Quit); + + // Test SPACE key initial press + SDL_Event spaceDownEvent = createKeyDownEvent(SDLK_SPACE); + EXPECT_EQ(inputHandler->processInput(spaceDownEvent, GameState::Playing), InputAction::Select); + EXPECT_TRUE(inputHandler->isSpaceHeld()); + + // Test SPACE key while already held down (should return None) + SDL_Event spaceDownEvent2 = createKeyDownEvent(SDLK_SPACE); + EXPECT_EQ(inputHandler->processInput(spaceDownEvent2, GameState::Playing), InputAction::None); + EXPECT_TRUE(inputHandler->isSpaceHeld()); // Should still be held + + // Test SPACE key release + SDL_Event spaceUpEvent = createKeyUpEvent(SDLK_SPACE); + EXPECT_EQ(inputHandler->processInput(spaceUpEvent, GameState::Playing), InputAction::None); + EXPECT_FALSE(inputHandler->isSpaceHeld()); // Should no longer be held + + // Test unmapped key + SDL_Event randomEvent = createKeyDownEvent(SDLK_a); + EXPECT_EQ(inputHandler->processInput(randomEvent, GameState::Playing), InputAction::None); +} + +// Test EndScreen input handling +TEST_F(InputHandlerTest, EndScreenInputHandling) { + // Test ESCAPE key (different behavior in EndScreen) + SDL_Event escapeEvent = createKeyDownEvent(SDLK_ESCAPE); + EXPECT_EQ(inputHandler->processInput(escapeEvent, GameState::EndScreen), InputAction::Escape); + + // Test SPACE key + SDL_Event spaceEvent = createKeyDownEvent(SDLK_SPACE); + EXPECT_EQ(inputHandler->processInput(spaceEvent, GameState::EndScreen), InputAction::Select); + + // Test UP arrow + SDL_Event upEvent = createKeyDownEvent(SDLK_UP); + EXPECT_EQ(inputHandler->processInput(upEvent, GameState::EndScreen), InputAction::MenuUp); + + // Test DOWN arrow + SDL_Event downEvent = createKeyDownEvent(SDLK_DOWN); + EXPECT_EQ(inputHandler->processInput(downEvent, GameState::EndScreen), InputAction::MenuDown); + + // Test unmapped key + SDL_Event randomEvent = createKeyDownEvent(SDLK_a); + EXPECT_EQ(inputHandler->processInput(randomEvent, GameState::EndScreen), InputAction::None); +} + +// Test space key state management in Playing mode +TEST_F(InputHandlerTest, SpaceKeyStateManagement) { + // Initially space should not be held + EXPECT_FALSE(inputHandler->isSpaceHeld()); + + // Press space in Playing mode + SDL_Event spaceDownEvent = createKeyDownEvent(SDLK_SPACE); + EXPECT_EQ(inputHandler->processInput(spaceDownEvent, GameState::Playing), InputAction::Select); + EXPECT_TRUE(inputHandler->isSpaceHeld()); + + // Press space again while held (should not trigger action) + SDL_Event spaceDownEvent2 = createKeyDownEvent(SDLK_SPACE); + EXPECT_EQ(inputHandler->processInput(spaceDownEvent2, GameState::Playing), InputAction::None); + EXPECT_TRUE(inputHandler->isSpaceHeld()); + + // Release space + SDL_Event spaceUpEvent = createKeyUpEvent(SDLK_SPACE); + EXPECT_EQ(inputHandler->processInput(spaceUpEvent, GameState::Playing), InputAction::None); + EXPECT_FALSE(inputHandler->isSpaceHeld()); + + // Press space again after release (should work) + SDL_Event spaceDownEvent3 = createKeyDownEvent(SDLK_SPACE); + EXPECT_EQ(inputHandler->processInput(spaceDownEvent3, GameState::Playing), InputAction::Select); + EXPECT_TRUE(inputHandler->isSpaceHeld()); +} + +// Test space key behavior in different states +TEST_F(InputHandlerTest, SpaceKeyDifferentStates) { + // Space in MainMenu doesn't affect isSpaceHeld + SDL_Event spaceEvent = createKeyDownEvent(SDLK_SPACE); + EXPECT_EQ(inputHandler->processInput(spaceEvent, GameState::MainMenu), InputAction::Select); + EXPECT_FALSE(inputHandler->isSpaceHeld()); // Should remain false + + // Space in EndScreen doesn't affect isSpaceHeld + EXPECT_EQ(inputHandler->processInput(spaceEvent, GameState::EndScreen), InputAction::Select); + EXPECT_FALSE(inputHandler->isSpaceHeld()); // Should remain false + + // Only Playing state manages space key state + EXPECT_EQ(inputHandler->processInput(spaceEvent, GameState::Playing), InputAction::Select); + EXPECT_TRUE(inputHandler->isSpaceHeld()); // Should be true now +} + +// Test isKeyPressed functionality +TEST_F(InputHandlerTest, IsKeyPressedFunctionality) { + // Note: This test may be limited by the SDL test environment + // We can test that the method doesn't crash and returns consistently + + bool spacePressed1 = inputHandler->isKeyPressed(SDL_SCANCODE_SPACE); + bool spacePressed2 = inputHandler->isKeyPressed(SDL_SCANCODE_SPACE); + + // Should be consistent between calls + EXPECT_EQ(spacePressed1, spacePressed2); + + // Test with different scancodes + bool escapePressed = inputHandler->isKeyPressed(SDL_SCANCODE_ESCAPE); + bool aPressed = inputHandler->isKeyPressed(SDL_SCANCODE_A); + + // Should not crash and should return boolean values + EXPECT_TRUE(escapePressed == true || escapePressed == false); + EXPECT_TRUE(aPressed == true || aPressed == false); +} + +// Test key up events for non-space keys (should not affect space state) +TEST_F(InputHandlerTest, NonSpaceKeyUpEvents) { + // Set space as held + SDL_Event spaceDownEvent = createKeyDownEvent(SDLK_SPACE); + inputHandler->processInput(spaceDownEvent, GameState::Playing); + EXPECT_TRUE(inputHandler->isSpaceHeld()); + + // Release other keys (should not affect space state) + SDL_Event escapeUpEvent = createKeyUpEvent(SDLK_ESCAPE); + EXPECT_EQ(inputHandler->processInput(escapeUpEvent, GameState::Playing), InputAction::None); + EXPECT_TRUE(inputHandler->isSpaceHeld()); // Should still be held + + SDL_Event aUpEvent = createKeyUpEvent(SDLK_a); + EXPECT_EQ(inputHandler->processInput(aUpEvent, GameState::Playing), InputAction::None); + EXPECT_TRUE(inputHandler->isSpaceHeld()); // Should still be held +} + +// Test multiple consecutive key presses +TEST_F(InputHandlerTest, MultipleConsecutiveKeyPresses) { + // Multiple different keys in MainMenu + SDL_Event spaceEvent = createKeyDownEvent(SDLK_SPACE); + SDL_Event upEvent = createKeyDownEvent(SDLK_UP); + SDL_Event downEvent = createKeyDownEvent(SDLK_DOWN); + SDL_Event escapeEvent = createKeyDownEvent(SDLK_ESCAPE); + + EXPECT_EQ(inputHandler->processInput(spaceEvent, GameState::MainMenu), InputAction::Select); + EXPECT_EQ(inputHandler->processInput(upEvent, GameState::MainMenu), InputAction::MenuUp); + EXPECT_EQ(inputHandler->processInput(downEvent, GameState::MainMenu), InputAction::MenuDown); + EXPECT_EQ(inputHandler->processInput(escapeEvent, GameState::MainMenu), InputAction::Quit); +} + +// Test switching between game states +TEST_F(InputHandlerTest, StateSwitching) { + SDL_Event escapeEvent = createKeyDownEvent(SDLK_ESCAPE); + + // Same key, different behavior based on state + EXPECT_EQ(inputHandler->processInput(escapeEvent, GameState::MainMenu), InputAction::Quit); + EXPECT_EQ(inputHandler->processInput(escapeEvent, GameState::Playing), InputAction::Quit); + EXPECT_EQ(inputHandler->processInput(escapeEvent, GameState::EndScreen), InputAction::Escape); +} + +// Test invalid/unknown game state +TEST_F(InputHandlerTest, InvalidGameState) { + SDL_Event spaceEvent = createKeyDownEvent(SDLK_SPACE); + + // Cast to invalid state + GameState invalidState = static_cast(999); + EXPECT_EQ(inputHandler->processInput(spaceEvent, invalidState), InputAction::None); +} + +// Test rhythm game timing scenario +TEST_F(InputHandlerTest, RhythmGameTimingScenario) { + // Simulate rhythm game beat timing + SDL_Event spaceDown = createKeyDownEvent(SDLK_SPACE); + SDL_Event spaceUp = createKeyUpEvent(SDLK_SPACE); + + // Beat 1 + EXPECT_EQ(inputHandler->processInput(spaceDown, GameState::Playing), InputAction::Select); + EXPECT_TRUE(inputHandler->isSpaceHeld()); + EXPECT_EQ(inputHandler->processInput(spaceUp, GameState::Playing), InputAction::None); + EXPECT_FALSE(inputHandler->isSpaceHeld()); + + // Beat 2 + EXPECT_EQ(inputHandler->processInput(spaceDown, GameState::Playing), InputAction::Select); + EXPECT_TRUE(inputHandler->isSpaceHeld()); + EXPECT_EQ(inputHandler->processInput(spaceUp, GameState::Playing), InputAction::None); + EXPECT_FALSE(inputHandler->isSpaceHeld()); + + // Held down through multiple beats (should only trigger once) + EXPECT_EQ(inputHandler->processInput(spaceDown, GameState::Playing), InputAction::Select); + EXPECT_TRUE(inputHandler->isSpaceHeld()); + EXPECT_EQ(inputHandler->processInput(spaceDown, GameState::Playing), InputAction::None); // Ignored + EXPECT_EQ(inputHandler->processInput(spaceDown, GameState::Playing), InputAction::None); // Ignored + EXPECT_TRUE(inputHandler->isSpaceHeld()); + EXPECT_EQ(inputHandler->processInput(spaceUp, GameState::Playing), InputAction::None); + EXPECT_FALSE(inputHandler->isSpaceHeld()); +} + +// Test all InputAction enum values +TEST_F(InputHandlerTest, InputActionEnumValues) { + // Test that all enum values are distinct + EXPECT_NE(InputAction::None, InputAction::Quit); + EXPECT_NE(InputAction::None, InputAction::Select); + EXPECT_NE(InputAction::None, InputAction::MenuUp); + EXPECT_NE(InputAction::None, InputAction::MenuDown); + EXPECT_NE(InputAction::None, InputAction::Escape); + + EXPECT_NE(InputAction::Quit, InputAction::Select); + EXPECT_NE(InputAction::Select, InputAction::MenuUp); + EXPECT_NE(InputAction::MenuUp, InputAction::MenuDown); + EXPECT_NE(InputAction::MenuDown, InputAction::Escape); +} + +// Test all GameState enum values +TEST_F(InputHandlerTest, GameStateEnumValues) { + // Test that all enum values are distinct + EXPECT_NE(GameState::MainMenu, GameState::Playing); + EXPECT_NE(GameState::MainMenu, GameState::EndScreen); + EXPECT_NE(GameState::MainMenu, GameState::Quit); + + EXPECT_NE(GameState::Playing, GameState::EndScreen); + EXPECT_NE(GameState::Playing, GameState::Quit); + + EXPECT_NE(GameState::EndScreen, GameState::Quit); +} \ No newline at end of file diff --git a/tests/unit/test_Logger.cpp b/tests/unit/test_Logger.cpp new file mode 100644 index 0000000..37b53ff --- /dev/null +++ b/tests/unit/test_Logger.cpp @@ -0,0 +1,224 @@ +#include +#include +#include +#include "Logger.hpp" +#include "GameStats.hpp" + +// Test fixture for Logger tests that can capture output +class LoggerTest : public ::testing::Test { +protected: + void SetUp() override { + // Save original stream buffers + original_cout = std::cout.rdbuf(); + original_cerr = std::cerr.rdbuf(); + + // Redirect cout and cerr to our string streams + std::cout.rdbuf(cout_buffer.rdbuf()); + std::cerr.rdbuf(cerr_buffer.rdbuf()); + } + + void TearDown() override { + // Restore original stream buffers + std::cout.rdbuf(original_cout); + std::cerr.rdbuf(original_cerr); + } + + std::stringstream cout_buffer; + std::stringstream cerr_buffer; + std::streambuf* original_cout; + std::streambuf* original_cerr; +}; + +// Test LogLevel enum values +TEST_F(LoggerTest, LogLevelEnumValues) { + EXPECT_EQ(static_cast(LogLevel::ERROR), 0); + EXPECT_EQ(static_cast(LogLevel::WARNING), 1); + EXPECT_EQ(static_cast(LogLevel::INFO), 2); + EXPECT_EQ(static_cast(LogLevel::DEBUG), 3); +} + +// Test basic log functionality with different levels +TEST_F(LoggerTest, BasicLogFunctionality) { + Logger::log(LogLevel::INFO, "Test info message"); + std::string output = cout_buffer.str(); + EXPECT_TRUE(output.find("[INFO] Test info message") != std::string::npos); + + cout_buffer.str(""); // Clear buffer + cout_buffer.clear(); + + Logger::log(LogLevel::DEBUG, "Test debug message"); + output = cout_buffer.str(); + EXPECT_TRUE(output.find("[DEBUG] Test debug message") != std::string::npos); +} + +// Test error and warning messages go to stderr +TEST_F(LoggerTest, ErrorWarningToStderr) { + Logger::log(LogLevel::ERROR, "Test error message"); + std::string error_output = cerr_buffer.str(); + EXPECT_TRUE(error_output.find("[ERROR] Test error message") != std::string::npos); + EXPECT_TRUE(cout_buffer.str().empty()); // Should not appear in cout + + cerr_buffer.str(""); // Clear buffer + cerr_buffer.clear(); + + Logger::log(LogLevel::WARNING, "Test warning message"); + error_output = cerr_buffer.str(); + EXPECT_TRUE(error_output.find("[WARN] Test warning message") != std::string::npos); + EXPECT_TRUE(cout_buffer.str().empty()); // Should not appear in cout +} + +// Test convenience methods +TEST_F(LoggerTest, ConvenienceMethods) { + Logger::error("Error via convenience method"); + std::string error_output = cerr_buffer.str(); + EXPECT_TRUE(error_output.find("[ERROR] Error via convenience method") != std::string::npos); + + cerr_buffer.str(""); + cerr_buffer.clear(); + + Logger::warning("Warning via convenience method"); + error_output = cerr_buffer.str(); + EXPECT_TRUE(error_output.find("[WARN] Warning via convenience method") != std::string::npos); + + Logger::info("Info via convenience method"); + std::string info_output = cout_buffer.str(); + EXPECT_TRUE(info_output.find("[INFO] Info via convenience method") != std::string::npos); + + cout_buffer.str(""); + cout_buffer.clear(); + + Logger::debug("Debug via convenience method"); + info_output = cout_buffer.str(); + EXPECT_TRUE(info_output.find("[DEBUG] Debug via convenience method") != std::string::npos); +} + +// Test logObject template method +TEST_F(LoggerTest, LogObjectTemplate) { + // Test with integer + Logger::logObject(LogLevel::INFO, 42); + std::string output = cout_buffer.str(); + EXPECT_TRUE(output.find("[INFO] 42") != std::string::npos); + + cout_buffer.str(""); + cout_buffer.clear(); + + // Test with double + Logger::logObject(LogLevel::DEBUG, 3.14159); + output = cout_buffer.str(); + EXPECT_TRUE(output.find("[DEBUG] 3.14159") != std::string::npos); + + cout_buffer.str(""); + cout_buffer.clear(); + + // Test with string + std::string test_string = "Hello World"; + Logger::logObject(LogLevel::INFO, test_string); + output = cout_buffer.str(); + EXPECT_TRUE(output.find("[INFO] Hello World") != std::string::npos); +} + +// Test logObject with GameStats (assuming it has operator<< overloaded) +TEST_F(LoggerTest, LogObjectWithGameStats) { + GameStats stats(100, 5, 8, 2); + Logger::logObject(LogLevel::INFO, stats); + std::string output = cout_buffer.str(); + + // Should contain the formatted GameStats output + EXPECT_TRUE(output.find("[INFO]") != std::string::npos); + EXPECT_TRUE(output.find("Final Stats") != std::string::npos); + EXPECT_TRUE(output.find("Score: 100") != std::string::npos); +} + +// Test empty message handling +TEST_F(LoggerTest, EmptyMessage) { + Logger::log(LogLevel::INFO, ""); + std::string output = cout_buffer.str(); + EXPECT_TRUE(output.find("[INFO]") != std::string::npos); + + Logger::info(""); + cout_buffer.str(""); + cout_buffer.clear(); + + Logger::info(""); + output = cout_buffer.str(); + EXPECT_TRUE(output.find("[INFO]") != std::string::npos); +} + +// Test message with special characters +TEST_F(LoggerTest, SpecialCharacters) { + Logger::info("Message with newlines\nand tabs\tand symbols!@#$%"); + std::string output = cout_buffer.str(); + EXPECT_TRUE(output.find("[INFO] Message with newlines\nand tabs\tand symbols!@#$%") != std::string::npos); +} + +// Test very long message +TEST_F(LoggerTest, LongMessage) { + std::string long_message(1000, 'A'); + Logger::info(long_message); + std::string output = cout_buffer.str(); + EXPECT_TRUE(output.find("[INFO] " + long_message) != std::string::npos); +} + +// Test multiple consecutive log calls +TEST_F(LoggerTest, MultipleLogCalls) { + Logger::info("First message"); + Logger::warning("Second message"); + Logger::debug("Third message"); + + std::string cout_output = cout_buffer.str(); + std::string cerr_output = cerr_buffer.str(); + + // Info and debug should be in cout + EXPECT_TRUE(cout_output.find("[INFO] First message") != std::string::npos); + EXPECT_TRUE(cout_output.find("[DEBUG] Third message") != std::string::npos); + + // Warning should be in cerr + EXPECT_TRUE(cerr_output.find("[WARN] Second message") != std::string::npos); +} + +// Test SDL error logging methods (these will test the method structure, +// but won't test actual SDL errors since SDL may not be initialized in tests) +TEST_F(LoggerTest, SDLErrorLoggingMethods) { + // These methods will call SDL_GetError() etc., but in a test environment + // they might return empty strings or default error messages + + Logger::logSDLError(LogLevel::ERROR, "SDL initialization failed"); + std::string error_output = cerr_buffer.str(); + EXPECT_TRUE(error_output.find("[ERROR] SDL initialization failed:") != std::string::npos); + + cerr_buffer.str(""); + cerr_buffer.clear(); + + Logger::logSDLImageError(LogLevel::WARNING, "Image loading failed"); + error_output = cerr_buffer.str(); + EXPECT_TRUE(error_output.find("[WARN] Image loading failed:") != std::string::npos); + + cerr_buffer.str(""); + cerr_buffer.clear(); + + Logger::logSDLTTFError(LogLevel::ERROR, "Font loading failed"); + error_output = cerr_buffer.str(); + EXPECT_TRUE(error_output.find("[ERROR] Font loading failed:") != std::string::npos); + + cerr_buffer.str(""); + cerr_buffer.clear(); + + Logger::logSDLMixerError(LogLevel::ERROR, "Audio initialization failed"); + error_output = cerr_buffer.str(); + EXPECT_TRUE(error_output.find("[ERROR] Audio initialization failed:") != std::string::npos); +} + +// Test that log calls are thread-safe (basic test) +TEST_F(LoggerTest, BasicThreadSafety) { + // Simple test that multiple calls don't interfere + for (int i = 0; i < 10; ++i) { + Logger::info("Message " + std::to_string(i)); + } + + std::string output = cout_buffer.str(); + + // Check that all messages appear + for (int i = 0; i < 10; ++i) { + EXPECT_TRUE(output.find("Message " + std::to_string(i)) != std::string::npos); + } +} \ No newline at end of file diff --git a/tests/unit/test_ResourceManager.cpp b/tests/unit/test_ResourceManager.cpp new file mode 100644 index 0000000..a4a0de2 --- /dev/null +++ b/tests/unit/test_ResourceManager.cpp @@ -0,0 +1,368 @@ +#include +#include +#include +#include +#include "ResourceManager.hpp" +#include "Font.hpp" + +// Test fixture for ResourceManager tests that handles SDL initialization +class ResourceManagerTest : public ::testing::Test { +protected: + void SetUp() override { + // Initialize SDL for texture operations + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + FAIL() << "SDL_Init failed: " << SDL_GetError(); + } + + if (!(IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG)) { + SDL_Quit(); + FAIL() << "IMG_Init failed: " << IMG_GetError(); + } + + if (TTF_Init() != 0) { + IMG_Quit(); + SDL_Quit(); + FAIL() << "TTF_Init failed: " << TTF_GetError(); + } + + // Create a minimal window and renderer for texture operations + window = SDL_CreateWindow("Test Window", + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + 100, 100, + SDL_WINDOW_HIDDEN); + if (!window) { + TTF_Quit(); + IMG_Quit(); + SDL_Quit(); + FAIL() << "SDL_CreateWindow failed: " << SDL_GetError(); + } + + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE); + if (!renderer) { + SDL_DestroyWindow(window); + TTF_Quit(); + IMG_Quit(); + SDL_Quit(); + FAIL() << "SDL_CreateRenderer failed: " << SDL_GetError(); + } + } + + void TearDown() override { + if (renderer) { + SDL_DestroyRenderer(renderer); + } + if (window) { + SDL_DestroyWindow(window); + } + TTF_Quit(); + IMG_Quit(); + SDL_Quit(); + } + + // Helper function to create a test PNG file + bool createTestImage(const std::string& filename, int width = 32, int height = 32) { + SDL_Surface* surface = SDL_CreateRGBSurface(0, width, height, 32, + 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF); + if (!surface) return false; + + // Fill with a simple pattern + SDL_FillRect(surface, nullptr, SDL_MapRGBA(surface->format, 255, 128, 64, 255)); + + int result = IMG_SavePNG(surface, filename.c_str()); + SDL_FreeSurface(surface); + return result == 0; + } + + SDL_Window* window = nullptr; + SDL_Renderer* renderer = nullptr; +}; + +// Test ResourceManager construction with valid renderer +TEST_F(ResourceManagerTest, ConstructorWithValidRenderer) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); +} + +// Test ResourceManager construction with null renderer +TEST_F(ResourceManagerTest, ConstructorWithNullRenderer) { + ResourceManager resourceManager(nullptr); + EXPECT_FALSE(resourceManager.isValid()); +} + +// Test loadTexture with nonexistent file +TEST_F(ResourceManagerTest, LoadTextureNonexistentFile) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + SDL_Texture* texture = resourceManager.loadTexture("nonexistent_file.png"); + EXPECT_EQ(texture, nullptr); +} + +// Test loadTexture with empty file path +TEST_F(ResourceManagerTest, LoadTextureEmptyPath) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + SDL_Texture* texture = resourceManager.loadTexture(""); + EXPECT_EQ(texture, nullptr); +} + +// Test loadTexture with valid file +TEST_F(ResourceManagerTest, LoadTextureValidFile) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + // Create a test PNG file + std::string testFile = "test_texture.png"; + ASSERT_TRUE(createTestImage(testFile, 64, 48)); + + SDL_Texture* texture = resourceManager.loadTexture(testFile); + EXPECT_NE(texture, nullptr); + + // Verify texture properties + if (texture) { + int width, height; + SDL_QueryTexture(texture, nullptr, nullptr, &width, &height); + EXPECT_EQ(width, 64); + EXPECT_EQ(height, 48); + } + + // Cleanup test file + std::remove(testFile.c_str()); +} + +// Test texture caching - loading same file twice should return same texture +TEST_F(ResourceManagerTest, TextureCaching) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + // Create a test PNG file + std::string testFile = "test_texture_cache.png"; + ASSERT_TRUE(createTestImage(testFile)); + + SDL_Texture* texture1 = resourceManager.loadTexture(testFile); + SDL_Texture* texture2 = resourceManager.loadTexture(testFile); + + EXPECT_NE(texture1, nullptr); + EXPECT_EQ(texture1, texture2); // Should be the same texture object + + // Cleanup test file + std::remove(testFile.c_str()); +} + +// Test loadTexture on invalid ResourceManager +TEST_F(ResourceManagerTest, LoadTextureInvalidResourceManager) { + ResourceManager resourceManager(nullptr); + EXPECT_FALSE(resourceManager.isValid()); + + SDL_Texture* texture = resourceManager.loadTexture("any_file.png"); + EXPECT_EQ(texture, nullptr); +} + +// Test createTextTexture with empty font path +TEST_F(ResourceManagerTest, CreateTextTextureEmptyFontPath) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + SDL_Color white = {255, 255, 255, 255}; + SDL_Texture* texture = resourceManager.createTextTexture("", 20, "Hello", white); + EXPECT_EQ(texture, nullptr); +} + +// Test createTextTexture with empty text +TEST_F(ResourceManagerTest, CreateTextTextureEmptyText) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + SDL_Color white = {255, 255, 255, 255}; + SDL_Texture* texture = resourceManager.createTextTexture("test_font.ttf", 20, "", white); + EXPECT_EQ(texture, nullptr); +} + +// Test createTextTexture on invalid ResourceManager +TEST_F(ResourceManagerTest, CreateTextTextureInvalidResourceManager) { + ResourceManager resourceManager(nullptr); + EXPECT_FALSE(resourceManager.isValid()); + + SDL_Color white = {255, 255, 255, 255}; + SDL_Texture* texture = resourceManager.createTextTexture("test_font.ttf", 20, "Hello", white); + EXPECT_EQ(texture, nullptr); +} + +// Test getFont with nonexistent font file +TEST_F(ResourceManagerTest, GetFontNonexistentFile) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + Font* font = resourceManager.getFont("nonexistent_font.ttf", 20); + EXPECT_EQ(font, nullptr); +} + +// Test font caching - getting same font twice should return same Font object +TEST_F(ResourceManagerTest, FontCaching) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + // This test will fail with nonexistent font, but we can test the caching logic + Font* font1 = resourceManager.getFont("nonexistent_font.ttf", 20); + Font* font2 = resourceManager.getFont("nonexistent_font.ttf", 20); + + // Both should be null, but the caching mechanism should be consistent + EXPECT_EQ(font1, font2); // Both null, so equal +} + +// Test font caching with different sizes +TEST_F(ResourceManagerTest, FontCachingDifferentSizes) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + Font* font1 = resourceManager.getFont("test_font.ttf", 20); + Font* font2 = resourceManager.getFont("test_font.ttf", 24); + + // Different sizes should be treated as different fonts + // Both will be null due to nonexistent file, but they should be cached separately + EXPECT_EQ(font1, nullptr); + EXPECT_EQ(font2, nullptr); +} + +// Test cleanup functionality +TEST_F(ResourceManagerTest, CleanupFunctionality) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + // Create test texture file + std::string testFile = "test_cleanup.png"; + ASSERT_TRUE(createTestImage(testFile)); + + // Load a texture + SDL_Texture* texture = resourceManager.loadTexture(testFile); + EXPECT_NE(texture, nullptr); + + // Manual cleanup should not crash + resourceManager.cleanup(); + + // After cleanup, loading the same texture should create a new one + SDL_Texture* texture2 = resourceManager.loadTexture(testFile); + EXPECT_NE(texture2, nullptr); + // texture2 might be the same pointer due to SDL texture pool, but that's okay + + // Cleanup test file + std::remove(testFile.c_str()); +} + +// Test destructor cleanup (implicit) +TEST_F(ResourceManagerTest, DestructorCleanup) { + std::string testFile = "test_destructor.png"; + ASSERT_TRUE(createTestImage(testFile)); + + { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + SDL_Texture* texture = resourceManager.loadTexture(testFile); + EXPECT_NE(texture, nullptr); + + // ResourceManager goes out of scope here, destructor should clean up + } + + // If we reach here without crash, destructor cleanup worked + EXPECT_TRUE(true); + + // Cleanup test file + std::remove(testFile.c_str()); +} + +// Test multiple different textures +TEST_F(ResourceManagerTest, MultipleDifferentTextures) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + // Create multiple test files + std::string testFile1 = "test_multi1.png"; + std::string testFile2 = "test_multi2.png"; + ASSERT_TRUE(createTestImage(testFile1, 32, 32)); + ASSERT_TRUE(createTestImage(testFile2, 64, 64)); + + SDL_Texture* texture1 = resourceManager.loadTexture(testFile1); + SDL_Texture* texture2 = resourceManager.loadTexture(testFile2); + + EXPECT_NE(texture1, nullptr); + EXPECT_NE(texture2, nullptr); + EXPECT_NE(texture1, texture2); // Should be different textures + + // Verify they have different dimensions + if (texture1 && texture2) { + int w1, h1, w2, h2; + SDL_QueryTexture(texture1, nullptr, nullptr, &w1, &h1); + SDL_QueryTexture(texture2, nullptr, nullptr, &w2, &h2); + + EXPECT_EQ(w1, 32); + EXPECT_EQ(h1, 32); + EXPECT_EQ(w2, 64); + EXPECT_EQ(h2, 64); + } + + // Cleanup test files + std::remove(testFile1.c_str()); + std::remove(testFile2.c_str()); +} + +// Test isValid method consistency +TEST_F(ResourceManagerTest, IsValidConsistency) { + ResourceManager validRM(renderer); + ResourceManager invalidRM(nullptr); + + EXPECT_TRUE(validRM.isValid()); + EXPECT_FALSE(invalidRM.isValid()); + + // Should remain consistent after operations + validRM.loadTexture("nonexistent.png"); + EXPECT_TRUE(validRM.isValid()); + + invalidRM.loadTexture("any_file.png"); + EXPECT_FALSE(invalidRM.isValid()); +} + +// Test edge case with very small texture +TEST_F(ResourceManagerTest, VerySmallTexture) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + std::string testFile = "test_tiny.png"; + ASSERT_TRUE(createTestImage(testFile, 1, 1)); + + SDL_Texture* texture = resourceManager.loadTexture(testFile); + EXPECT_NE(texture, nullptr); + + if (texture) { + int width, height; + SDL_QueryTexture(texture, nullptr, nullptr, &width, &height); + EXPECT_EQ(width, 1); + EXPECT_EQ(height, 1); + } + + std::remove(testFile.c_str()); +} + +// Test key generation for text textures (indirectly through caching behavior) +TEST_F(ResourceManagerTest, TextTextureCaching) { + ResourceManager resourceManager(renderer); + EXPECT_TRUE(resourceManager.isValid()); + + SDL_Color red = {255, 0, 0, 255}; + SDL_Color blue = {0, 0, 255, 255}; + + // These will fail due to nonexistent font, but we can test that different parameters + // are treated as different cache keys + SDL_Texture* tex1 = resourceManager.createTextTexture("font.ttf", 20, "Hello", red); + SDL_Texture* tex2 = resourceManager.createTextTexture("font.ttf", 20, "Hello", blue); // Different color + SDL_Texture* tex3 = resourceManager.createTextTexture("font.ttf", 24, "Hello", red); // Different size + SDL_Texture* tex4 = resourceManager.createTextTexture("font.ttf", 20, "World", red); // Different text + + // All should be null due to nonexistent font + EXPECT_EQ(tex1, nullptr); + EXPECT_EQ(tex2, nullptr); + EXPECT_EQ(tex3, nullptr); + EXPECT_EQ(tex4, nullptr); +} \ No newline at end of file diff --git a/tests/unit/test_Sprite.cpp b/tests/unit/test_Sprite.cpp new file mode 100644 index 0000000..56b3a84 --- /dev/null +++ b/tests/unit/test_Sprite.cpp @@ -0,0 +1,426 @@ +#include +#include +#include +#include "Sprite.hpp" + +// Test fixture for Sprite tests that handles SDL initialization +class SpriteTest : public ::testing::Test { +protected: + void SetUp() override { + // Initialize SDL for texture operations + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + FAIL() << "SDL_Init failed: " << SDL_GetError(); + } + + if (!(IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG)) { + SDL_Quit(); + FAIL() << "IMG_Init failed: " << IMG_GetError(); + } + + // Create a minimal window and renderer for texture operations + window = SDL_CreateWindow("Test Window", + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + 100, 100, + SDL_WINDOW_HIDDEN); + if (!window) { + IMG_Quit(); + SDL_Quit(); + FAIL() << "SDL_CreateWindow failed: " << SDL_GetError(); + } + + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE); + if (!renderer) { + SDL_DestroyWindow(window); + IMG_Quit(); + SDL_Quit(); + FAIL() << "SDL_CreateRenderer failed: " << SDL_GetError(); + } + } + + void TearDown() override { + if (testTexture) { + SDL_DestroyTexture(testTexture); + } + if (renderer) { + SDL_DestroyRenderer(renderer); + } + if (window) { + SDL_DestroyWindow(window); + } + IMG_Quit(); + SDL_Quit(); + } + + // Helper function to create a test texture + SDL_Texture* createTestTexture(int width, int height) { + SDL_Surface* surface = SDL_CreateRGBSurface(0, width, height, 32, 0, 0, 0, 0); + if (!surface) return nullptr; + + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + SDL_FreeSurface(surface); + return texture; + } + + SDL_Window* window = nullptr; + SDL_Renderer* renderer = nullptr; + SDL_Texture* testTexture = nullptr; +}; + +// Test basic Sprite construction +TEST_F(SpriteTest, BasicConstruction) { + testTexture = createTestTexture(100, 80); // 4x2 grid with 25x40 frames + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(10.5f, 20.3f, testTexture, 2, 4); + + // Test inherited Entity properties + EXPECT_FLOAT_EQ(sprite.getX(), 10.5f); + EXPECT_FLOAT_EQ(sprite.getY(), 20.3f); + EXPECT_EQ(sprite.getTexture(), testTexture); + + // Test Sprite-specific properties + EXPECT_EQ(sprite.getRow(), 1); // Should start at (1,1) + EXPECT_EQ(sprite.getCol(), 1); + + // Test frame calculations (100/4 = 25 width, 80/2 = 40 height) + SDL_Rect frame = sprite.getCurrentFrame(); + EXPECT_EQ(frame.w, 25); + EXPECT_EQ(frame.h, 40); + EXPECT_EQ(frame.x, 0); // First frame starts at (0,0) + EXPECT_EQ(frame.y, 0); +} + +// Test Sprite construction with single frame +TEST_F(SpriteTest, SingleFrameConstruction) { + testTexture = createTestTexture(64, 64); + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(0.0f, 0.0f, testTexture, 1, 1); + + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); + + SDL_Rect frame = sprite.getCurrentFrame(); + EXPECT_EQ(frame.w, 64); // Full texture width + EXPECT_EQ(frame.h, 64); // Full texture height + EXPECT_EQ(frame.x, 0); + EXPECT_EQ(frame.y, 0); +} + +// Test setFrame functionality +TEST_F(SpriteTest, SetFrameValid) { + testTexture = createTestTexture(120, 90); // 3x2 grid, 40x45 frames + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(0.0f, 0.0f, testTexture, 2, 3); + + // Test setting valid frames + sprite.setFrame(1, 2); + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 2); + + SDL_Rect frame = sprite.getCurrentFrame(); + EXPECT_EQ(frame.x, 40); // Second column: (2-1) * 40 + EXPECT_EQ(frame.y, 0); // First row: (1-1) * 45 + + sprite.setFrame(2, 3); + EXPECT_EQ(sprite.getRow(), 2); + EXPECT_EQ(sprite.getCol(), 3); + + frame = sprite.getCurrentFrame(); + EXPECT_EQ(frame.x, 80); // Third column: (3-1) * 40 + EXPECT_EQ(frame.y, 45); // Second row: (2-1) * 45 +} + +// Test setFrame with invalid values +TEST_F(SpriteTest, SetFrameInvalid) { + testTexture = createTestTexture(60, 40); // 2x3 grid + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(0.0f, 0.0f, testTexture, 2, 3); + + // Set to valid frame first + sprite.setFrame(2, 2); + EXPECT_EQ(sprite.getRow(), 2); + EXPECT_EQ(sprite.getCol(), 2); + + // Try invalid values - should not change + sprite.setFrame(0, 1); // Row 0 is invalid + EXPECT_EQ(sprite.getRow(), 2); + EXPECT_EQ(sprite.getCol(), 2); + + sprite.setFrame(1, 0); // Col 0 is invalid + EXPECT_EQ(sprite.getRow(), 2); + EXPECT_EQ(sprite.getCol(), 2); + + sprite.setFrame(3, 1); // Row 3 is invalid (maxRow = 2) + EXPECT_EQ(sprite.getRow(), 2); + EXPECT_EQ(sprite.getCol(), 2); + + sprite.setFrame(1, 4); // Col 4 is invalid (maxCol = 3) + EXPECT_EQ(sprite.getRow(), 2); + EXPECT_EQ(sprite.getCol(), 2); + + sprite.setFrame(-1, -1); // Negative values are invalid + EXPECT_EQ(sprite.getRow(), 2); + EXPECT_EQ(sprite.getCol(), 2); +} + +// Test resetFrame functionality +TEST_F(SpriteTest, ResetFrame) { + testTexture = createTestTexture(80, 60); + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(0.0f, 0.0f, testTexture, 3, 4); + + // Move to different frame + sprite.setFrame(3, 4); + EXPECT_EQ(sprite.getRow(), 3); + EXPECT_EQ(sprite.getCol(), 4); + + // Reset should go back to (1,1) + sprite.resetFrame(); + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); + + SDL_Rect frame = sprite.getCurrentFrame(); + EXPECT_EQ(frame.x, 0); + EXPECT_EQ(frame.y, 0); +} + +// Test movement methods +TEST_F(SpriteTest, MovementMethods) { + testTexture = createTestTexture(32, 32); + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(100.0f, 200.0f, testTexture, 1, 1); + + // Test setLoc + sprite.setLoc(50, 75); + EXPECT_FLOAT_EQ(sprite.getX(), 50.0f); + EXPECT_FLOAT_EQ(sprite.getY(), 75.0f); + + // Test moveRight + sprite.moveRight(25); + EXPECT_FLOAT_EQ(sprite.getX(), 75.0f); + EXPECT_FLOAT_EQ(sprite.getY(), 75.0f); + + // Test moveLeft + sprite.moveLeft(10); + EXPECT_FLOAT_EQ(sprite.getX(), 65.0f); + EXPECT_FLOAT_EQ(sprite.getY(), 75.0f); + + // Test moveDown + sprite.moveDown(20); + EXPECT_FLOAT_EQ(sprite.getX(), 65.0f); + EXPECT_FLOAT_EQ(sprite.getY(), 95.0f); + + // Test moveUp + sprite.moveUp(15); + EXPECT_FLOAT_EQ(sprite.getX(), 65.0f); + EXPECT_FLOAT_EQ(sprite.getY(), 80.0f); +} + +// Test movement with negative values +TEST_F(SpriteTest, MovementWithNegativeValues) { + testTexture = createTestTexture(16, 16); + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(50.0f, 50.0f, testTexture, 1, 1); + + // Move with negative deltas (should still move in specified direction) + sprite.moveRight(-10); // Move left + EXPECT_FLOAT_EQ(sprite.getX(), 40.0f); + + sprite.moveLeft(-5); // Move right + EXPECT_FLOAT_EQ(sprite.getX(), 45.0f); + + sprite.moveDown(-20); // Move up + EXPECT_FLOAT_EQ(sprite.getY(), 30.0f); + + sprite.moveUp(-10); // Move down + EXPECT_FLOAT_EQ(sprite.getY(), 40.0f); +} + +// Test post-increment operator +TEST_F(SpriteTest, PostIncrementOperator) { + testTexture = createTestTexture(90, 60); // 3x2 grid + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(0.0f, 0.0f, testTexture, 2, 3); + + // Should start at (1,1) + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); + + // First increment: (1,1) -> (2,1) + Sprite oldSprite = sprite++; + EXPECT_EQ(oldSprite.getRow(), 1); // Returned value should be old + EXPECT_EQ(oldSprite.getCol(), 1); + EXPECT_EQ(sprite.getRow(), 2); // Current should be new + EXPECT_EQ(sprite.getCol(), 1); + + // Second increment: (2,1) -> (1,2) (wrap row, advance column) + sprite++; + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 2); + + // Third increment: (1,2) -> (2,2) + sprite++; + EXPECT_EQ(sprite.getRow(), 2); + EXPECT_EQ(sprite.getCol(), 2); + + // Fourth increment: (2,2) -> (1,3) (wrap row, advance column) + sprite++; + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 3); + + // Fifth increment: (1,3) -> (2,3) + sprite++; + EXPECT_EQ(sprite.getRow(), 2); + EXPECT_EQ(sprite.getCol(), 3); + + // Sixth increment: (2,3) -> (1,1) (wrap both row and column) + sprite++; + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); +} + +// Test post-increment with single row +TEST_F(SpriteTest, PostIncrementSingleRow) { + testTexture = createTestTexture(120, 30); // 1x4 grid + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(0.0f, 0.0f, testTexture, 1, 4); + + // Should advance columns only + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); + + sprite++; + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 2); + + sprite++; + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 3); + + sprite++; + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 4); + + sprite++; // Should wrap to (1,1) + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); +} + +// Test post-increment with single column +TEST_F(SpriteTest, PostIncrementSingleColumn) { + testTexture = createTestTexture(40, 160); // 4x1 grid + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(0.0f, 0.0f, testTexture, 4, 1); + + // Should advance rows, then wrap to (1,1) + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); + + sprite++; + EXPECT_EQ(sprite.getRow(), 2); + EXPECT_EQ(sprite.getCol(), 1); + + sprite++; + EXPECT_EQ(sprite.getRow(), 3); + EXPECT_EQ(sprite.getCol(), 1); + + sprite++; + EXPECT_EQ(sprite.getRow(), 4); + EXPECT_EQ(sprite.getCol(), 1); + + sprite++; // Should wrap to (1,1) and advance column, but col wraps too + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); +} + +// Test frame calculations with various grid sizes +TEST_F(SpriteTest, FrameCalculationsVariousGrids) { + // Test 2x2 grid with 100x100 texture + SDL_Texture* texture2x2 = createTestTexture(100, 100); + ASSERT_NE(texture2x2, nullptr); + + Sprite sprite2x2(0.0f, 0.0f, texture2x2, 2, 2); + sprite2x2.setFrame(2, 2); // Bottom-right frame + + SDL_Rect frame = sprite2x2.getCurrentFrame(); + EXPECT_EQ(frame.w, 50); // 100/2 + EXPECT_EQ(frame.h, 50); // 100/2 + EXPECT_EQ(frame.x, 50); // (2-1) * 50 + EXPECT_EQ(frame.y, 50); // (2-1) * 50 + + SDL_DestroyTexture(texture2x2); + + // Test 1x5 grid with 150x30 texture + SDL_Texture* texture1x5 = createTestTexture(150, 30); + ASSERT_NE(texture1x5, nullptr); + + Sprite sprite1x5(0.0f, 0.0f, texture1x5, 1, 5); + sprite1x5.setFrame(1, 4); // Fourth column + + frame = sprite1x5.getCurrentFrame(); + EXPECT_EQ(frame.w, 30); // 150/5 + EXPECT_EQ(frame.h, 30); // 30/1 + EXPECT_EQ(frame.x, 90); // (4-1) * 30 + EXPECT_EQ(frame.y, 0); // (1-1) * 30 + + SDL_DestroyTexture(texture1x5); +} + +// Test edge case with 1x1 grid +TEST_F(SpriteTest, SingleFrameGrid) { + testTexture = createTestTexture(64, 48); + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(0.0f, 0.0f, testTexture, 1, 1); + + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); + + SDL_Rect frame = sprite.getCurrentFrame(); + EXPECT_EQ(frame.w, 64); + EXPECT_EQ(frame.h, 48); + EXPECT_EQ(frame.x, 0); + EXPECT_EQ(frame.y, 0); + + // Increment should stay at (1,1) + sprite++; + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); + + // Reset should also stay at (1,1) + sprite.resetFrame(); + EXPECT_EQ(sprite.getRow(), 1); + EXPECT_EQ(sprite.getCol(), 1); +} + +// Test that Sprite maintains Entity functionality +TEST_F(SpriteTest, InheritsEntityFunctionality) { + testTexture = createTestTexture(32, 32); + ASSERT_NE(testTexture, nullptr); + + Sprite sprite(25.5f, 35.7f, testTexture, 2, 2); + + // Test Entity getters + EXPECT_FLOAT_EQ(sprite.getX(), 25.5f); + EXPECT_FLOAT_EQ(sprite.getY(), 35.7f); + EXPECT_EQ(sprite.getTexture(), testTexture); + + // Test Entity texture setting + SDL_Texture* newTexture = createTestTexture(64, 64); + ASSERT_NE(newTexture, nullptr); + + sprite.setTexture(newTexture); + EXPECT_EQ(sprite.getTexture(), newTexture); + + SDL_DestroyTexture(newTexture); +} \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..b4bfbf7 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,28 @@ +{ + "name": "meowstro", + "version": "1.0.0", + "dependencies": [ + { + "name": "sdl2", + "version>=": "2.30.0" + }, + { + "name": "sdl2-image", + "version>=": "2.8.2" + }, + { + "name": "sdl2-mixer", + "version>=": "2.8.0", + "features": ["mpg123"] + }, + { + "name": "sdl2-ttf", + "version>=": "2.22.0" + }, + { + "name": "gtest", + "version>=": "1.14.0" + } + ], + "builtin-baseline": "5300a2a461a53b76405db4fcbe6aeb0eea43935d" +} \ No newline at end of file