From faf3f239d85dc1361879d11d56b9213a1909c916 Mon Sep 17 00:00:00 2001 From: five-hundred-eleven Date: Sun, 3 Aug 2025 11:15:54 -0400 Subject: [PATCH] Player's position is reset when close to ghosts --- CLAUDE.md | 46 ++++++++++++++++++++++ Makefile | 7 +++- config.cpp | 103 +++++++++++++++++++++++++++++++++++++++++++++++++ config.h | 20 ++++++++++ ghost.cpp | 12 ++++++ ghost.h | 2 + ghostland.cpp | 84 ++++++++++++++++++++++++++++++++-------- ghostland.json | 3 ++ glad/glad.h | 2 +- maze.txt | 2 +- player.cpp | 6 +++ player.h | 1 + 12 files changed, 268 insertions(+), 20 deletions(-) create mode 100644 CLAUDE.md create mode 100644 config.cpp create mode 100644 config.h create mode 100644 ghostland.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6530f55 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,46 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +- **Build the game**: `make` +- **Clean build artifacts**: `make clean` +- **Run the game**: `./ghostland` + +## Architecture Overview + +This is a C++ OpenGL 3D game called "ghostland-game" - a continuation of a previous Go-based project. The player navigates a maze-like environment while being pursued by ghosts. + +### Core Components + +- **ghostland.cpp**: Main game loop, OpenGL setup, window management, and input handling +- **Player**: First-person camera system with mouse look, movement physics (including jumping), and flashlight mechanics +- **Ghost**: Enemy entities that move autonomously within defined boundaries and face the player +- **Shader system**: OpenGL shader loading and uniform management for different rendering passes +- **Text rendering**: FreeType-based text rendering system for UI elements like FPS display +- **Collision detection**: Ray-triangle intersection testing for wall collisions + +### Key Systems + +- **Maze loading**: The `maze.txt` file contains vertex data for walls parsed at runtime +- **Lighting**: Dynamic lighting system with player-controlled flashlight that responds to mouse movement +- **Rendering pipeline**: Separate shaders for different object types (walls, ghosts, text, trails) +- **Physics**: Simple gravity-based jumping with collision detection against maze geometry + +### Dependencies + +- OpenGL 3.3+ with GLAD loader +- GLFW for window management and input +- GLM for math operations +- FreeType for text rendering +- stb_image for texture loading + +### File Structure + +- Header files (.h) define class interfaces +- Implementation files (.cpp) contain the logic +- Shader files (.glsl) define rendering programs +- Object files (.o) are generated during build +- `maze.txt` contains level geometry data +- `fonts/` contains TrueType fonts for text rendering \ No newline at end of file diff --git a/Makefile b/Makefile index 13989d5..0ec6157 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ CFLAGS := -std=c++17 -O3 LDFLAGS := -ldl -lglfw INCLUDES := -I/usr/include/freetype2/ -I/usr/include/libpng16 -ghostland: glad.o ghostland.o stb_image.o collisions.o player.o shader.o ghost.o text.o - $(CC) -o ghostland glad.o ghostland.o stb_image.o collisions.o player.o shader.o ghost.o text.o /usr/lib/x86_64-linux-gnu/libfreetype.so $(LDFLAGS) $(CFLAGS) +ghostland: glad.o ghostland.o stb_image.o collisions.o player.o shader.o ghost.o text.o config.o + $(CC) -o ghostland glad.o ghostland.o stb_image.o collisions.o player.o shader.o ghost.o text.o config.o /usr/lib/x86_64-linux-gnu/libfreetype.so $(LDFLAGS) $(CFLAGS) glad.o: glad.c $(CC) -c -o glad.o glad.c $(CFLAGS) @@ -30,5 +30,8 @@ ghost.o: ghost.cpp ghost.h text.o: text.cpp text.h $(CC) -c -o text.o text.cpp $(CFLAGS) $(INCLUDES) +config.o: config.cpp config.h + $(CC) -c -o config.o config.cpp $(CFLAGS) + clean: rm -f *.o ghostland diff --git a/config.cpp b/config.cpp new file mode 100644 index 0000000..c99588e --- /dev/null +++ b/config.cpp @@ -0,0 +1,103 @@ +#include "config.h" +#include +#include +#include + +std::map Config::values; + +bool Config::loadFromFile(const std::string& filename) { + std::ifstream file(filename); + if (!file.is_open()) { + std::cout << "Warning: Could not open config file: " << filename << std::endl; + return false; + } + + std::string line; + while (std::getline(file, line)) { + std::string key, value; + if (parseJsonValue(line, key, value)) { + values[key] = value; + } + } + + file.close(); + std::cout << "Loaded " << values.size() << " config values from " << filename << std::endl; + return true; +} + +float Config::getFloat(const std::string& key, float defaultValue) { + auto it = values.find(key); + if (it != values.end()) { + try { + return std::stof(it->second); + } catch (const std::exception& e) { + std::cout << "Warning: Invalid float value for key '" << key << "': " << it->second << std::endl; + } + } + return defaultValue; +} + +int Config::getInt(const std::string& key, int defaultValue) { + auto it = values.find(key); + if (it != values.end()) { + try { + return std::stoi(it->second); + } catch (const std::exception& e) { + std::cout << "Warning: Invalid int value for key '" << key << "': " << it->second << std::endl; + } + } + return defaultValue; +} + +std::string Config::getString(const std::string& key, const std::string& defaultValue) { + auto it = values.find(key); + if (it != values.end()) { + return it->second; + } + return defaultValue; +} + +std::string Config::trim(const std::string& str) { + size_t start = str.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) return ""; + size_t end = str.find_last_not_of(" \t\r\n"); + return str.substr(start, end - start + 1); +} + +bool Config::parseJsonValue(const std::string& line, std::string& key, std::string& value) { + std::string trimmed = trim(line); + + // Skip empty lines, comments, and structural characters + if (trimmed.empty() || trimmed[0] == '{' || trimmed[0] == '}' || trimmed[0] == '/' || trimmed[0] == '*') { + return false; + } + + // Look for key-value pair: "key": value + size_t colonPos = trimmed.find(':'); + if (colonPos == std::string::npos) { + return false; + } + + // Extract key (remove quotes and whitespace) + std::string rawKey = trim(trimmed.substr(0, colonPos)); + if (rawKey.length() >= 2 && rawKey[0] == '"' && rawKey[rawKey.length()-1] == '"') { + key = rawKey.substr(1, rawKey.length()-2); + } else { + key = rawKey; + } + + // Extract value (remove quotes, whitespace, and trailing comma) + std::string rawValue = trim(trimmed.substr(colonPos + 1)); + if (!rawValue.empty() && rawValue.back() == ',') { + rawValue.pop_back(); + rawValue = trim(rawValue); + } + + if (rawValue.length() >= 2 && rawValue[0] == '"' && rawValue[rawValue.length()-1] == '"') { + value = rawValue.substr(1, rawValue.length()-2); + } else { + value = rawValue; + } + + return !key.empty(); +} \ No newline at end of file diff --git a/config.h b/config.h new file mode 100644 index 0000000..7569c39 --- /dev/null +++ b/config.h @@ -0,0 +1,20 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include + +class Config { +public: + static bool loadFromFile(const std::string& filename); + static float getFloat(const std::string& key, float defaultValue); + static int getInt(const std::string& key, int defaultValue); + static std::string getString(const std::string& key, const std::string& defaultValue); + +private: + static std::map values; + static std::string trim(const std::string& str); + static bool parseJsonValue(const std::string& line, std::string& key, std::string& value); +}; + +#endif \ No newline at end of file diff --git a/ghost.cpp b/ghost.cpp index 95780d6..52fb841 100644 --- a/ghost.cpp +++ b/ghost.cpp @@ -111,3 +111,15 @@ float rand_float(float rmin, float rmax) { glm::vec3 Ghost::get_pos() const { return pos; } + +void Ghost::reverse_direction_from_position(glm::vec3 target_pos) { + // Calculate direction from target_pos to ghost position + glm::vec3 direction = pos - target_pos; + // Set yawr to point directly away from target_pos + yawr = atan2(direction.x, direction.z); +} + +void Ghost::regenerate_position() { + pos.x = rand_float(xmin, xmax); + pos.z = rand_float(zmin, zmax); +} diff --git a/ghost.h b/ghost.h index 96b32a1..309a344 100644 --- a/ghost.h +++ b/ghost.h @@ -9,6 +9,8 @@ class Ghost { void apply_movement(float curr_time, float timed); glm::mat4 get_model(glm::vec3 &camera_pos); glm::vec3 get_pos() const; + void reverse_direction_from_position(glm::vec3 target_pos); + void regenerate_position(); private: int ghost_id; diff --git a/ghostland.cpp b/ghostland.cpp index 0a0aee8..27ae78a 100644 --- a/ghostland.cpp +++ b/ghostland.cpp @@ -20,6 +20,7 @@ #include "shader.h" #include "ghost.h" #include "text.h" +#include "config.h" void framebuffer_size_callback(GLFWwindow* window, int width, int height); void mouse_callback(GLFWwindow* window, double xpos, double ypos); @@ -38,7 +39,7 @@ bool first_frame = false; float timed = 0.0f; float last_frame = 0.0f; -float camera_speed = 10.0f; +float camera_speed = 20.0f; const char *projectionC = "projection"; const char *viewC = "view"; @@ -59,12 +60,18 @@ const char *specularC = "light.specular"; int num_walls; float *wall_vertices; -int trailmax = 1800; +int trailmax = 500; int main(int argc, char *argv[]) { int success; glm::vec3 position; + glm::vec3 start_position; + float start_yaw; + + // Load configuration + Config::loadFromFile("ghostland.json"); + camera_speed = Config::getFloat("camera_speed", camera_speed); FILE *fp = fopen("maze.txt", "r"); float yaw; @@ -72,6 +79,8 @@ int main(int argc, char *argv[]) { printf("1st fscanf failed.\n"); return -1; } + start_position = position; + start_yaw = yaw; player = new Player(position, yaw); if (fscanf(fp, "%d\n", &num_walls) == EOF) { printf("2nd fscanf failed.\n"); @@ -151,8 +160,11 @@ int main(int argc, char *argv[]) { WINDOWWIDTH = monitor_mode->width; WINDOWHEIGHT = monitor_mode->height; + //WINDOWWIDTH = 1280; + //WINDOWHEIGHT = 960; GLFWwindow* window = glfwCreateWindow(WINDOWWIDTH, WINDOWHEIGHT, "Ghostland!", monitor, NULL); + //GLFWwindow* window = glfwCreateWindow(WINDOWWIDTH, WINDOWHEIGHT, "Ghostland!", NULL, NULL); if (window == NULL) { printf("Failed to create GLFW window.\n"); @@ -340,7 +352,14 @@ int main(int argc, char *argv[]) { std::vector ghosts; for (int i = 0; i < 800; i++) { - ghosts.push_back(new Ghost(xmin_wall, xmax_wall, zmin_wall, zmax_wall)); + Ghost *ghost = new Ghost(xmin_wall, xmax_wall, zmin_wall, zmax_wall); + + // Ensure ghost is not within 30.0 units of start_position + while (glm::length(ghost->get_pos() - start_position) <= 30.0f) { + ghost->regenerate_position(); + } + + ghosts.push_back(ghost); } player->mouse_callback(window, WINDOWWIDTH/2, WINDOWHEIGHT); @@ -409,6 +428,37 @@ int main(int argc, char *argv[]) { glm::vec3 light_front = player->get_light_front(); //printf("light_pos: (%f, %f, %f)\n", light_front.x, light_front.y, light_front.z); + // Sort ghosts by distance to player for optimization + glm::vec3 player_pos = player->get_pos(); + struct { + bool operator()(const Ghost *a, const Ghost *b) const { + glm::vec3 a_diff, b_diff; + a_diff = player->get_pos() - a->get_pos(); + b_diff = player->get_pos() - b->get_pos(); + float a_dist, b_dist; + a_dist = glm::length(a_diff); + b_dist = glm::length(b_diff); + return a_dist < b_dist; + } + } ghost_compare; + std::partial_sort(ghosts.begin(), ghosts.begin() + ghosts.size() / 5, ghosts.end(), ghost_compare); + + // Calculate nearest ghost distance (first ghost is now closest) + float nearest_ghost_distance = glm::length(ghosts[0]->get_pos() - player_pos); + + // Interpolate flashlight color based on nearest ghost distance + // At 30.0+ units: (0.61, 0.60, 0.59) - normal color + // At 10.0 units: (0.99, 0.60, 0.59) - red shift + float color_factor = 1.0f; + if (nearest_ghost_distance < 30.0f) { + color_factor = (nearest_ghost_distance - 10.0f) / (30.0f - 10.0f); + color_factor = glm::clamp(color_factor, 0.0f, 1.0f); + } + + glm::vec3 normal_color = glm::vec3(0.61f, 0.60f, 0.59f); + glm::vec3 danger_color = glm::vec3(0.99f, 0.60f, 0.59f); + diffuse = glm::mix(danger_color, normal_color, color_factor); + // fragment requirements set_uniform(wall_program, viewposC, camera_pos); set_uniform(wall_program, shininessC, wall_shininess); @@ -468,22 +518,24 @@ int main(int argc, char *argv[]) { set_uniform(ghost_program, projectionC, projection); set_uniform(ghost_program, viewC, view); - - struct { - bool operator()(const Ghost *a, const Ghost *b) const { - glm::vec3 a_diff, b_diff; - a_diff = player->get_pos() - a->get_pos(); - b_diff = player->get_pos() - b->get_pos(); - float a_dist2, b_dist2; - a_dist2 = a_diff.x * a_diff.x + a_diff.y * a_diff.y + a_diff.z * a_diff.z; - b_dist2 = b_diff.x * b_diff.x + b_diff.y * b_diff.y + b_diff.z * b_diff.z; - return a_dist2 < b_dist2; - } - } ghost_compare; - std::partial_sort(ghosts.begin(), ghosts.begin() + ghosts.size() / 5, ghosts.end(), ghost_compare); for (int i = ghosts.size()/5; i >= 0; i--) { glm::mat4 ghost_model = ghosts[i]->get_model(camera_pos); ghosts[i]->apply_movement(current_frame, timed); + + // Check if ghost is within 10.0 units of player + glm::vec3 ghost_pos = ghosts[i]->get_pos(); + glm::vec3 player_pos = player->get_pos(); + float distance = glm::length(ghost_pos - player_pos); + if (distance <= 10.0f) { + player->reset_position(start_position, start_yaw); + } + + // Check if ghost is within 30.0 units of start_position + float start_distance = glm::length(ghost_pos - start_position); + if (start_distance <= 30.0f) { + ghosts[i]->reverse_direction_from_position(start_position); + } + set_uniform(ghost_program, modelC, ghost_model); glBindVertexArray(ghostVAO); glDrawArrays(GL_TRIANGLES, 0, (3 + 3 + 2) * 6); diff --git a/ghostland.json b/ghostland.json new file mode 100644 index 0000000..c4918a7 --- /dev/null +++ b/ghostland.json @@ -0,0 +1,3 @@ +{ + "camera_speed": 30.0 +} diff --git a/glad/glad.h b/glad/glad.h index 5f44580..b5d459a 100644 --- a/glad/glad.h +++ b/glad/glad.h @@ -1,6 +1,6 @@ /* - OpenGL loader generated by glad 0.1.36 on Sat Oct 7 18:46:55 2023. + OpenGL loader generated by glad 0.1.36 on Sun Apr 7 15:29:39 2024. Language/Generator: C/C++ Specification: gl diff --git a/maze.txt b/maze.txt index f320269..c6dc10b 100644 --- a/maze.txt +++ b/maze.txt @@ -1,4 +1,4 @@ -845.0 2.5 -5.0 -128.0 +845.0 2.5 -5.0 0.0 6498 -0.5 0.0 0.5 0.0 0.0 1.0 959.5 0.0 0.5 0.0 0.0 1.0 diff --git a/player.cpp b/player.cpp index a378d6a..3be7b09 100644 --- a/player.cpp +++ b/player.cpp @@ -250,6 +250,12 @@ void Player::apply_movement(float timed) { } +void Player::reset_position(glm::vec3 startpos, float startyaw) { + position = startpos; + yaw = startyaw; + velocity = glm::vec3(0.0f, 0.0f, 0.0f); +} + glm::vec3 Player::get_normal_from_index(int ix) { return glm::vec3( wall_vertices[ix * 6 * 6 + 3], diff --git a/player.h b/player.h index 9e33b24..5236fba 100644 --- a/player.h +++ b/player.h @@ -40,6 +40,7 @@ class Player { void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); bool is_in_air(); void apply_movement(float timed); + void reset_position(glm::vec3 startpos, float startyaw); bool operator()(const Ghost &a, const Ghost &b) const;