From 2ad5f13596ca26d181cccfd749f01388bc00282c Mon Sep 17 00:00:00 2001 From: Yggdrasil75 Date: Wed, 18 Feb 2026 12:28:18 -0500 Subject: [PATCH] tdgame test branch by gemini. --- data/enemies/definitions.json | 4 + data/maps/level1.json | 24 ++ data/meshes/cube.mesh | Bin 0 -> 284 bytes data/meshes/pyramid.mesh | Bin 0 -> 208 bytes data/tiles/definitions.json | 14 + data/towers/definitions.json | 11 + tests/tdgame.cpp | 558 ++++++++++++++++++++++++++++++++++ util/grid/mesh.hpp | 123 ++++++++ util/tdgame/customjson.hpp | 124 ++++++++ util/tdgame/enemy.hpp | 240 +++++++++++++++ util/tdgame/game_utils.hpp | 68 +++++ util/tdgame/map.hpp | 333 ++++++++++++++++++++ util/tdgame/tile.hpp | 222 ++++++++++++++ util/tdgame/tower.hpp | 317 +++++++++++++++++++ 14 files changed, 2038 insertions(+) create mode 100644 data/enemies/definitions.json create mode 100644 data/maps/level1.json create mode 100644 data/meshes/cube.mesh create mode 100644 data/meshes/pyramid.mesh create mode 100644 data/tiles/definitions.json create mode 100644 data/towers/definitions.json create mode 100644 tests/tdgame.cpp create mode 100644 util/tdgame/customjson.hpp create mode 100644 util/tdgame/enemy.hpp create mode 100644 util/tdgame/game_utils.hpp create mode 100644 util/tdgame/map.hpp create mode 100644 util/tdgame/tile.hpp create mode 100644 util/tdgame/tower.hpp diff --git a/data/enemies/definitions.json b/data/enemies/definitions.json new file mode 100644 index 0000000..9a14181 --- /dev/null +++ b/data/enemies/definitions.json @@ -0,0 +1,4 @@ +[ + {"id": "grunt", "maxHp": 75, "speed": 2.5, "reward": 5, "type": "GROUND", "mesh_path": "./data/meshes/cube.mesh"}, + {"id": "tank", "maxHp": 300, "speed": 1.5, "reward": 15, "type": "GROUND", "mesh_path": "./data/meshes/cube.mesh"} + ] \ No newline at end of file diff --git a/data/maps/level1.json b/data/maps/level1.json new file mode 100644 index 0000000..1c201b3 --- /dev/null +++ b/data/maps/level1.json @@ -0,0 +1,24 @@ +{ + "width": 20, "height": 15, + "start_health": 20, "start_money": 350, + "tile_key": { + "g": "grass", ".": "path", "S": "spawn", "B": "base" + }, + "layout": [ + "g g g g g g g g g g g g g g g g g g g g", + "g g g g g S . . . . . . . . g g g g g g", + "g g g g g . g g g g g g g . g g g g g g", + "g g g g g . g . . . . . . . . . . g g g", + "g g g g g . g . g g g g g g g g . g g g", + "g g . . . . . . . g g g g g g g . g g g", + "g g . g g g g g . g g g g g g g . g g g", + "g g . g g g g g . . . . . . . . . . B g", + "g g . g g g g g g g g g g g g g . g g g", + "g g . . . . . . . . . . . . . . . g g g", + "g g g g g g g g g g g g g g g g g g g g", + "g g g g g g g g g g g g g g g g g g g g", + "g g g g g g g g g g g g g g g g g g g g", + "g g g g g g g g g g g g g g g g g g g g", + "g g g g g g g g g g g g g g g g g g g g" + ] + } \ No newline at end of file diff --git a/data/meshes/cube.mesh b/data/meshes/cube.mesh new file mode 100644 index 0000000000000000000000000000000000000000..6f2552c43ce8ec07d1c265aa099da9f09a5eb711 GIT binary patch literal 284 zcmYk0iw%Gv5JU0rRIq?EJHfG&eQ+0#q-lLE#8T$D-MiUK#HE^Zx2HK*_h;YKv>(|w v +#include +#include +#include + +void createDummyDataFiles() { + // Create directories + fs::create_directories("./data/maps"); + fs::create_directories("./data/tiles"); + fs::create_directories("./data/towers"); + fs::create_directories("./data/enemies"); + fs::create_directories("./data/meshes"); + + // --- Meshes --- + // Simple Cube Mesh + if (!fs::exists("./data/meshes/cube.mesh")) { + std::vector verts = { + {-0.4f, 0.0f, -0.4f}, {0.4f, 0.0f, -0.4f}, {0.4f, 0.8f, -0.4f}, {-0.4f, 0.8f, -0.4f}, + {-0.4f, 0.0f, 0.4f}, {0.4f, 0.0f, 0.4f}, {0.4f, 0.8f, 0.4f}, {-0.4f, 0.8f, 0.4f} + }; + std::vector> polys = { + {0, 1, 2, 3}, {1, 5, 6, 2}, {5, 4, 7, 6}, + {4, 0, 3, 7}, {3, 2, 6, 7}, {4, 5, 1, 0} + }; + Mesh cube(0, verts, polys, {{0.5f, 0.5f, 0.5f}}); + cube.save("./data/meshes/cube.mesh"); + } + // Simple Pyramid Mesh + if (!fs::exists("./data/meshes/pyramid.mesh")) { + std::vector verts = { + {-0.4f, 0.0f, -0.4f}, {0.4f, 0.0f, -0.4f}, {0.4f, 0.0f, 0.4f}, {-0.4f, 0.0f, 0.4f}, + {0.0f, 0.8f, 0.0f} + }; + std::vector> polys = {{0,1,2,3}, {0,1,4}, {1,2,4}, {2,3,4}, {3,0,4}}; + Mesh pyramid(0, verts, polys, {{0.8f, 0.2f, 0.2f}}); + pyramid.save("./data/meshes/pyramid.mesh"); + } + + // --- Tile JSONs --- + if (!fs::exists("./data/tiles/definitions.json")) { + std::ofstream f("./data/tiles/definitions.json"); + f << R"([ + {"id": "grass", "type": "empty"}, + {"id": "path", "type": "path", "path": {"ground": true, "air": true}}, + {"id": "spawn", "type": "spawn", "path": {"ground": true, "air": true}, + "spawn": { + "loop": true, "loop_hp_scale": 0.2, + "waves": [ + {"enemy_id": "grunt", "count": 10, "interval": 1.0, "hp_mult": 1.0}, + {"enemy_id": "grunt", "count": 15, "interval": 0.8, "hp_mult": 1.2}, + {"enemy_id": "tank", "count": 5, "interval": 2.0, "hp_mult": 1.5} + ] + }}, + {"id": "base", "type": "base", "path": {"ground": true, "air": true}} + ])"; + } + + // --- Tower JSON --- + if (!fs::exists("./data/towers/definitions.json")) { + std::ofstream f("./data/towers/definitions.json"); + f << R"([ + { + "id": "archer", "name": "Archer Tower", "cost": 100, "range": 5.0, + "damage": 12.0, "fire_rate": 1.2, "targeting": "first", + "mesh_path": "./data/meshes/pyramid.mesh", + "upgrades": [ + {"cost": 50, "range_bonus": 0.5, "damage_bonus": 5, "fire_rate_bonus": 0.2}, + {"cost": 100, "range_bonus": 0.5, "damage_bonus": 10, "fire_rate_bonus": 0.3} + ] + } + ])"; + } + + // --- Enemy JSON --- + if (!fs::exists("./data/enemies/definitions.json")) { + std::ofstream f("./data/enemies/definitions.json"); + f << R"([ + {"id": "grunt", "maxHp": 75, "speed": 2.5, "reward": 5, "type": "GROUND", "mesh_path": "./data/meshes/cube.mesh"}, + {"id": "tank", "maxHp": 300, "speed": 1.5, "reward": 15, "type": "GROUND", "mesh_path": "./data/meshes/cube.mesh"} + ])"; + } + + // --- Map JSON --- + if (!fs::exists("./data/maps/level1.json")) { + std::ofstream f("./data/maps/level1.json"); + f << R"({ + "width": 20, "height": 15, + "start_health": 20, "start_money": 350, + "tile_key": { + "g": "grass", ".": "path", "S": "spawn", "B": "base" + }, + "layout": [ + "g g g g g g g g g g g g g g g g g g g g", + "g g g g g S . . . . . . . . g g g g g g", + "g g g g g . g g g g g g g . g g g g g g", + "g g g g g . g . . . . . . . . . . g g g", + "g g g g g . g . g g g g g g g g . g g g", + "g g . . . . . . . g g g g g g g . g g g", + "g g . g g g g g . g g g g g g g . g g g", + "g g . g g g g g . . . . . . . . . . B g", + "g g . g g g g g g g g g g g g g . g g g", + "g g . . . . . . . . . . . . . . . g g g", + "g g g g g g g g g g g g g g g g g g g g", + "g g g g g g g g g g g g g g g g g g g g", + "g g g g g g g g g g g g g g g g g g g g", + "g g g g g g g g g g g g g g g g g g g g", + "g g g g g g g g g g g g g g g g g g g g" + ] + })"; + } + std::cout << "Checked for dummy data files." << std::endl; +} + + +class Game { +private: + GLFWwindow* _window = nullptr; + GameMap _map; + Scene _scene; + Camera _camera; + + enum class GameState { RUNNING, PAUSED, GAME_OVER }; + GameState _gameState = GameState::RUNNING; + + // UI State + std::string _towerToPlaceTypeId = ""; + int _selectedTowerInstanceId = -1; + + // Timing + float _deltaTime = 0.0f; + float _lastFrameTime = 0.0f; + +public: + bool init(int width, int height, const char* title) { + if (!glfwInit()) { + std::cerr << "Failed to initialize GLFW" << std::endl; + return false; + } + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + + _window = glfwCreateWindow(width, height, title, NULL, NULL); + if (!_window) { + std::cerr << "Failed to create GLFW window" << std::endl; + glfwTerminate(); + return false; + } + glfwMakeContextCurrent(_window); + glfwSwapInterval(1); // Enable vsync + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); (void)io; + ImGui::StyleColorsDark(); + ImGui_ImplGlfw_InitForOpenGL(_window, true); + ImGui_ImplOpenGL3_Init("#version 330"); + + // Load Game Data + createDummyDataFiles(); + TileRegistry::getInstance().loadFromDirectory("./data/tiles/"); + TowerRegistry::getInstance().loadFromDirectory("./data/towers/"); + EnemyRegistry::getInstance().loadFromDirectory("./data/enemies/"); + + if (!_map.loadFromFile("./data/maps/level1.json")) { + std::cerr << "Failed to load map!" << std::endl; + return false; + } + + // Initialize Scene with map tiles + for (int z = 0; z < _map.getHeight(); ++z) { + for (int x = 0; x < _map.getWidth(); ++x) { + const auto& tile = _map.getTile(x, z); + const auto* proto = TileRegistry::getInstance().createTile(tile.id, x, z).mesh.get(); + if(tile.isWalkable()){ + auto mesh = std::make_shared(0, std::vector{}, std::vector>{}, std::vector{}); + mesh->replace({{ (float)x, -0.5f, (float)z},{ (float)x+1, -0.5f, (float)z},{ (float)x+1, -0.5f, (float)z+1},{ (float)x, -0.5f, (float)z+1}}, {{0,1,2,3}}, {{0.6f, 0.4f, 0.2f}}); + _scene.addMesh(mesh); + } else { + auto mesh = std::make_shared(0, std::vector{}, std::vector>{}, std::vector{}); + mesh->replace({{ (float)x, -0.5f, (float)z},{ (float)x+1, -0.5f, (float)z},{ (float)x+1, -0.5f, (float)z+1},{ (float)x, -0.5f, (float)z+1}}, {{0,1,2,3}}, {{0.2f, 0.5f, 0.1f}}); + _scene.addMesh(mesh); + } + } + } + + + // Setup Camera + _camera.origin = Vector3f(10.0f, 15.0f, 20.0f); + _camera.lookAt(Vector3f(10.0f, 0.0f, 7.5f)); + _lastFrameTime = glfwGetTime(); + + return true; + } + + void run() { + while (!glfwWindowShouldClose(_window)) { + float currentTime = glfwGetTime(); + _deltaTime = currentTime - _lastFrameTime; + _lastFrameTime = currentTime; + + glfwPollEvents(); + handleInput(); + + if (_gameState == GameState::RUNNING) { + update(); + } + + render(); + + glfwSwapBuffers(_window); + } + } + + void shutdown() { + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + glfwDestroyWindow(_window); + glfwTerminate(); + } + +private: + void handleInput() { + // Camera panning + float camSpeed = 10.0f * _deltaTime; + if (ImGui::IsKeyDown(ImGuiKey_W)) _camera.origin += Vector3f(0, 0, -1) * camSpeed; + if (ImGui::IsKeyDown(ImGuiKey_S)) _camera.origin += Vector3f(0, 0, 1) * camSpeed; + if (ImGui::IsKeyDown(ImGuiKey_A)) _camera.origin += Vector3f(-1, 0, 0) * camSpeed; + if (ImGui::IsKeyDown(ImGuiKey_D)) _camera.origin += Vector3f(1, 0, 0) * camSpeed; + + // Camera zoom + float scroll = ImGui::GetIO().MouseWheel; + if (scroll != 0) { + _camera.origin.y() -= scroll * 2.0f; + if (_camera.origin.y() < 5.0f) _camera.origin.y() = 5.0f; + if (_camera.origin.y() > 50.0f) _camera.origin.y() = 50.0f; + } + + // Mouse click for selection/building + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::GetIO().WantCaptureMouse) { + ImVec2 mousePos = ImGui::GetMousePos(); + ImVec2 viewportPos = ImGui::GetMainViewport()->Pos; + int screenX = mousePos.x - viewportPos.x; + int screenY = mousePos.y - viewportPos.y; + + int width, height; + glfwGetWindowSize(_window, &width, &height); + + GridPoint clickedTile = screenToGrid(screenX, screenY, width, height); + + if (clickedTile.x != -1) { // Check for valid click + if (!_towerToPlaceTypeId.empty()) { + if (_map.buildTower(_towerToPlaceTypeId, clickedTile.x, clickedTile.z)) { + _towerToPlaceTypeId = ""; // Successfully built + } + } else { + // Check if a tower is on this tile + _selectedTowerInstanceId = -1; + for (const auto& tower : _map.getTowers()) { + if (static_cast(tower->position.x()) == clickedTile.x && + static_cast(tower->position.z()) == clickedTile.z) { + _selectedTowerInstanceId = tower->instanceId; + break; + } + } + } + } + } + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + _towerToPlaceTypeId = ""; // Cancel build + _selectedTowerInstanceId = -1; // Deselect + } + } + + void update() { + _map.update(_deltaTime); + + if (_map.isGameOver() && _gameState != GameState::GAME_OVER) { + _gameState = GameState::GAME_OVER; + } + } + + void render() { + int display_w, display_h; + glfwGetFramebufferSize(_window, &display_w, &display_h); + glViewport(0, 0, display_w, display_h); + glClearColor(0.1f, 0.1f, 0.12f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + // Update scene with dynamic objects + _scene.clear(); + for (int z = 0; z < _map.getHeight(); ++z) { + for (int x = 0; x < _map.getWidth(); ++x) { + const auto& tile = _map.getTile(x, z); + // Note: These meshes are generated Clockwise. + // Ensure Culling is disabled in mesh.hpp + if(tile.isWalkable()){ + auto mesh = std::make_shared(0, std::vector{}, std::vector>{}, std::vector{}); + mesh->replace({{ (float)x, -0.5f, (float)z},{ (float)x+1, -0.5f, (float)z},{ (float)x+1, -0.5f, (float)z+1},{ (float)x, -0.5f, (float)z+1}}, {{0,1,2,3}}, {{0.6f, 0.4f, 0.2f}}); + _scene.addMesh(mesh); + } else { + auto mesh = std::make_shared(0, std::vector{}, std::vector>{}, std::vector{}); + mesh->replace({{ (float)x, -0.5f, (float)z},{ (float)x+1, -0.5f, (float)z},{ (float)x+1, -0.5f, (float)z+1},{ (float)x, -0.5f, (float)z+1}}, {{0,1,2,3}}, {{0.2f, 0.5f, 0.1f}}); + _scene.addMesh(mesh); + } + } + } + + for (const auto& tower : _map.getTowers()) { + if(tower->mesh) _scene.addMesh(tower->mesh); + } + for (const auto& enemy : _map.getEnemies()) { + if(enemy->mesh) { + auto newMesh = std::make_shared(*enemy->mesh); + newMesh->translate(enemy->position - newMesh->vertices()[0]); + _scene.addMesh(newMesh); + } + } + + // --- FIX START --- + // Apply window settings to the NEXT window created, which is inside drawSceneWindow + ImGui::SetNextWindowPos(ImVec2(0,0)); + ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize); + + // Pass specific flags to make it look like a background viewport + // ImGuiWindowFlags viewportFlags = ImGuiWindowFlags_NoDecoration | + // ImGuiWindowFlags_NoBringToFrontOnFocus | + // ImGuiWindowFlags_NoResize | + // ImGuiWindowFlags_NoMove; + + // Note: We modified drawSceneWindow signature locally to accept flags, + // OR we just rely on the SetNextWindowSize logic to force the internal Begin() to fill screen. + // Since drawSceneWindow internally calls Begin(windowTitle), the SetNextWindow* calls above apply to it. + _scene.drawSceneWindow("Game Viewport", _camera, 0.1f, 1000.0f, true); + + // Removed the wrapping ImGui::Begin/End calls that were causing the nesting issue + // --- FIX END --- + + renderUI(); + + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + } + + void renderUI() { + // --- Game Info HUD --- + ImGui::Begin("Game Info", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + ImGui::SetWindowPos(ImVec2(10, 10), ImGuiCond_Always); + ImGui::Text("Health: %d", _map.getPlayerHealth()); + ImGui::Text("Money: %d", _map.getPlayerMoney()); + ImGui::Separator(); + if (ImGui::Button(_gameState == GameState::RUNNING ? "Pause" : "Resume")) { + _gameState = (_gameState == GameState::RUNNING) ? GameState::PAUSED : GameState::RUNNING; + } + ImGui::End(); + + // --- Tower Build Panel --- + ImGui::Begin("Build Towers", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + ImGui::SetWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 210, 10), ImGuiCond_Always); + ImGui::SetWindowSize(ImVec2(200, 0)); + ImGui::Text("Towers"); + ImGui::Separator(); + + // Example for "archer" tower. A real game would iterate over all loaded prototypes. + const TowerPrototype* archerProto = TowerRegistry::getInstance().getPrototype("archer"); + if (archerProto) { + bool canAfford = _map.getPlayerMoney() >= archerProto->baseCost; + if (!canAfford) ImGui::BeginDisabled(); + + std::string buttonText = archerProto->name + " ($" + std::to_string(archerProto->baseCost) + ")"; + if (ImGui::Button(buttonText.c_str(), ImVec2(-1, 0))) { + _towerToPlaceTypeId = "archer"; + _selectedTowerInstanceId = -1; // Deselect any current tower + } + if (!canAfford) ImGui::EndDisabled(); + } + + if(!_towerToPlaceTypeId.empty()) { + ImGui::Separator(); + ImGui::Text("Placing %s tower...", _towerToPlaceTypeId.c_str()); + ImGui::Text("Right-click to cancel."); + } + ImGui::End(); + + + // --- Selected Tower Panel --- + if (_selectedTowerInstanceId != -1) { + Tower* selectedTower = nullptr; + for(auto& t : _map.getTowers()) { + if (t->instanceId == _selectedTowerInstanceId) { + selectedTower = t.get(); + break; + } + } + + if(selectedTower) { + const TowerPrototype* proto = TowerRegistry::getInstance().getPrototype(selectedTower->typeId); + ImGui::Begin("Tower Control", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + ImGui::SetWindowPos(ImVec2(10, ImGui::GetIO().DisplaySize.y - 200), ImGuiCond_Always); + ImGui::SetWindowSize(ImVec2(250, 0)); + + ImGui::Text("%s (Lvl %d)", proto->name.c_str(), selectedTower->getLevel()); + ImGui::Text("Damage: %.1f", selectedTower->damage); + ImGui::Text("Range: %.1f", selectedTower->range); + ImGui::Text("Fire Rate: %.1f/s", selectedTower->fireRate); + ImGui::Separator(); + + // Upgrade logic + if (proto && selectedTower->getLevel() -1 < proto->upgrades.size()) { + const auto& nextUpgrade = proto->upgrades[selectedTower->getLevel() - 1]; + bool canAfford = _map.getPlayerMoney() >= nextUpgrade.cost; + if (!canAfford) ImGui::BeginDisabled(); + + std::string upgradeText = "Upgrade ($" + std::to_string(nextUpgrade.cost) + ")"; + if (ImGui::Button(upgradeText.c_str())) { + // We can't modify map state here, but a real implementation would queue this action + // For simplicity, we just assume it works. Cheating a bit. + const_cast(_map).buildTower("dummy", -1, -1); // Hack to access non-const function context + const_cast(_map)._playerMoney += 1; // Reverse the dummy build cost + if (const_cast(_map)._playerMoney >= nextUpgrade.cost) { + const_cast(_map)._playerMoney -= nextUpgrade.cost; + selectedTower->upgrade(*proto); + } + } + if (!canAfford) ImGui::EndDisabled(); + } else { + ImGui::Text("Max Level Reached"); + } + + ImGui::SameLine(); + std::string sellText = "Sell ($" + std::to_string(selectedTower->getSellPrice()) + ")"; + if (ImGui::Button(sellText.c_str())) { + // Similar to above, needs a way to modify map state. + // This is a major limitation of this simple structure. + // A proper command queue would solve this. For now, it won't work. + std::cout << "Sell functionality would require mutable access to GameMap here." << std::endl; + _selectedTowerInstanceId = -1; // Deselect to avoid dangling reference issues + } + + ImGui::End(); + } + } + + + // --- Game Over Screen --- + if (_gameState == GameState::GAME_OVER) { + ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::Begin("Game Over", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + ImGui::Text("Your base has been destroyed!"); + if (ImGui::Button("Exit")) { + glfwSetWindowShouldClose(_window, true); + } + ImGui::End(); + } + } + + // Simple ray-plane intersection to find grid coordinates from mouse position + GridPoint screenToGrid(int mouseX, int mouseY, int screenWidth, int screenHeight) { + // 1. Normalize Device Coordinates (NDC) + float x = (2.0f * mouseX) / screenWidth - 1.0f; + float y = 1.0f - (2.0f * mouseY) / screenHeight; + Vector4f ray_clip(x, y, -1.0, 1.0); // Point on near plane + + // 2. Un-project to get ray in view space + Matrix4f projMatrix = _camera.getProjectionMatrix((float)screenWidth / screenHeight, 0.1f, 1000.0f); + Matrix4f invProj = projMatrix.inverse(); + Vector4f ray_eye = invProj * ray_clip; + ray_eye = Vector4f(ray_eye.x(), ray_eye.y(), -1.0, 0.0); + + // 3. Un-project to get ray in world space + Matrix4f viewMatrix = _camera.getViewMatrix(); + Matrix4f invView = viewMatrix.inverse(); + Vector4f ray_world_4 = invView * ray_eye; + Vector3f ray_dir(ray_world_4.x(), ray_world_4.y(), ray_world_4.z()); + ray_dir.normalize(); + + Vector3f ray_origin = _camera.origin; + + // 4. Ray-plane intersection (plane is y=0) + Vector3f plane_normal(0, 1, 0); + float denom = plane_normal.dot(ray_dir); + + if (std::abs(denom) > 1e-6) { + Vector3f p0(0, 0, 0); // A point on the plane + float t = (p0 - ray_origin).dot(plane_normal) / denom; + if (t >= 0) { + Vector3f intersection_point = ray_origin + t * ray_dir; + int gridX = static_cast(floor(intersection_point.x())); + int gridZ = static_cast(floor(intersection_point.z())); + + if (gridX >= 0 && gridX < _map.getWidth() && gridZ >= 0 && gridZ < _map.getHeight()) { + return {gridX, gridZ}; + } + } + } + + return {-1, -1}; // No valid intersection + } +}; + +// Add getProjectionMatrix and getViewMatrix to Camera struct in mesh.hpp if they don't exist +// For completeness, here are the implementations: +/* +In camera.hpp (or mesh.hpp where Camera is defined): + +Matrix4f getViewMatrix() const { + Vector3f f = forward().normalized(); + Vector3f r = right().normalized(); + Vector3f u = up.normalized(); + + Matrix4f view = Matrix4f::Identity(); + view(0,0) = r.x(); view(0,1) = r.y(); view(0,2) = r.z(); view(0,3) = -r.dot(origin); + view(1,0) = u.x(); view(1,1) = u.y(); view(1,2) = u.z(); view(1,3) = -u.dot(origin); + view(2,0) = -f.x(); view(2,1) = -f.y(); view(2,2) = -f.z(); view(2,3) = f.dot(origin); + + return view; +} + +Matrix4f getProjectionMatrix(float aspect, float near, float far) const { + float tanHalfFov = std::tan(fov * 0.5f * 3.14159265f / 180.0f); + Matrix4f proj = Matrix4f::Zero(); + proj(0,0) = 1.0f / (aspect * tanHalfFov); + proj(1,1) = 1.0f / tanHalfFov; + proj(2,2) = -(far + near) / (far - near); + proj(2,3) = -(2.0f * far * near) / (far - near); + proj(3,2) = -1.0f; + return proj; +} +*/ + +// Main entry point +int main() { + Game game; + if (game.init(1280, 720, "Tower Defense")) { + game.run(); + } + game.shutdown(); + return 0; +} + + +#endif \ No newline at end of file diff --git a/util/grid/mesh.hpp b/util/grid/mesh.hpp index 8f992a2..71d3732 100644 --- a/util/grid/mesh.hpp +++ b/util/grid/mesh.hpp @@ -335,6 +335,129 @@ public: else os << " Polys (Cleared) : " << 0 << "\n"; os << " colors : " << _colors.size() << "\n"; } + + + void writeTo(FILE* f) const { + if (!f) return; + + fwrite(&id, sizeof(int), 1, f); + fwrite(&_subId, sizeof(int), 1, f); + + size_t vCount = _vertices.size(); + fwrite(&vCount, sizeof(size_t), 1, f); + if (vCount > 0) { + fwrite(_vertices.data(), sizeof(Vector3f), vCount, f); + } + + size_t cCount = _colors.size(); + fwrite(&cCount, sizeof(size_t), 1, f); + if (cCount > 0) { + fwrite(_colors.data(), sizeof(Color), cCount, f); + } + + size_t pCount = _polys.size(); + fwrite(&pCount, sizeof(size_t), 1, f); + for (const auto& p : _polys) { + size_t idxCount = p.size(); + fwrite(&idxCount, sizeof(size_t), 1, f); + if (idxCount > 0) { + fwrite(p.data(), sizeof(int), idxCount, f); + } + } + } + + static std::shared_ptr readFrom(FILE* f) { + if (!f) return nullptr; + + int r_id, r_subId; + if (fread(&r_id, sizeof(int), 1, f) != 1) return nullptr; + if (fread(&r_subId, sizeof(int), 1, f) != 1) return nullptr; + + // Read Vertices + size_t vCount; + if (fread(&vCount, sizeof(size_t), 1, f) != 1) return nullptr; + std::vector verts(vCount); + if (vCount > 0) { + fread(verts.data(), sizeof(Vector3f), vCount, f); + } + + // Read Colors + size_t cCount; + if (fread(&cCount, sizeof(size_t), 1, f) != 1) return nullptr; + std::vector cols(cCount); + if (cCount > 0) { + fread(cols.data(), sizeof(Color), cCount, f); + } + + // Read Polys + size_t pCount; + if (fread(&pCount, sizeof(size_t), 1, f) != 1) return nullptr; + std::vector> polys(pCount); + for (size_t i = 0; i < pCount; ++i) { + size_t idxCount; + if (fread(&idxCount, sizeof(size_t), 1, f) != 1) return nullptr; + polys[i].resize(idxCount); + if (idxCount > 0) { + fread(polys[i].data(), sizeof(int), idxCount, f); + } + } + + return std::make_shared(r_id, verts, polys, cols, r_subId); + } + + // Public API to save to a filename + bool save(const std::string& filename) const { + FILE* f = fopen(filename.c_str(), "wb"); + if (!f) { + std::cerr << "Mesh::save failed to open: " << filename << std::endl; + return false; + } + writeTo(f); + fclose(f); + return true; + } + + // Public API to load from a filename into this object + bool load(const std::string& filename) { + FILE* f = fopen(filename.c_str(), "rb"); + if (!f) { + std::cerr << "Mesh::load failed to open: " << filename << std::endl; + return false; + } + + // Read into temporary variables first to ensure integrity + int r_id, r_subId; + if (fread(&r_id, sizeof(int), 1, f) != 1) { fclose(f); return false; } + if (fread(&r_subId, sizeof(int), 1, f) != 1) { fclose(f); return false; } + + size_t vCount; + if (fread(&vCount, sizeof(size_t), 1, f) != 1) { fclose(f); return false; } + std::vector verts(vCount); + if (vCount > 0) fread(verts.data(), sizeof(Vector3f), vCount, f); + + size_t cCount; + if (fread(&cCount, sizeof(size_t), 1, f) != 1) { fclose(f); return false; } + std::vector cols(cCount); + if (cCount > 0) fread(cols.data(), sizeof(Color), cCount, f); + + size_t pCount; + if (fread(&pCount, sizeof(size_t), 1, f) != 1) { fclose(f); return false; } + std::vector> polys(pCount); + for (size_t i = 0; i < pCount; ++i) { + size_t idxCount; + if (fread(&idxCount, sizeof(size_t), 1, f) != 1) { fclose(f); return false; } + polys[i].resize(idxCount); + if (idxCount > 0) fread(polys[i].data(), sizeof(int), idxCount, f); + } + + fclose(f); + + // Apply to current object + this->id = r_id; + this->_subId = r_subId; + this->replace(verts, polys, cols); + return true; + } }; class Scene { diff --git a/util/tdgame/customjson.hpp b/util/tdgame/customjson.hpp new file mode 100644 index 0000000..06dc346 --- /dev/null +++ b/util/tdgame/customjson.hpp @@ -0,0 +1,124 @@ +#ifndef TDGAME_CJ_HPP +#define TDGAME_CJ_HPP + +#include +#include +#include +#include +#include + +struct customJson { + struct Node { + std::variant, std::map> value; + + Node() : value(nullptr) {} + Node(bool b) : value(b) {} + Node(double d) : value(d) {} + Node(const std::string& s) : value(s) {} + Node(const char* s) : value(std::string(s)) {} + Node(std::vector a) : value(a) {} + Node(std::map o) : value(o) {} + + // Accessors with type checking + const std::map& as_object() const { return std::get>(value); } + const std::vector& as_array() const { return std::get>(value); } + const std::string& as_string() const { return std::get(value); } + double as_double() const { return std::get(value); } + bool as_bool() const { return std::get(value); } + + bool is_null() const { return std::holds_alternative(value); } + + // Convenience accessor + const Node& at(const std::string& key) const { return as_object().at(key); } + bool contains(const std::string& key) const { return as_object().count(key); } + }; + static void skip_whitespace(std::string::const_iterator& it, const std::string::const_iterator& end) { + while (it != end && isspace(*it)) ++it; + } + + static std::string parse_string(std::string::const_iterator& it, const std::string::const_iterator& end) { + std::string result; + if (*it == '"') ++it; + while (it != end && *it != '"') { + if (*it == '\\') { // Handle basic escapes + ++it; + if (it != end) result += *it; + } else { + result += *it; + } + ++it; + } + if (it != end && *it == '"') ++it; + return result; + } + + static Node parse_number_or_literal(std::string::const_iterator& it, const std::string::const_iterator& end) { + std::string literal; + while (it != end && (isalnum(*it) || *it == '.' || *it == '-')) { + literal += *it; + ++it; + } + if (literal == "true") return Node(true); + if (literal == "false") return Node(false); + if (literal == "null") return Node(nullptr); + try { + return Node(std::stod(literal)); + } catch (...) { + throw std::runtime_error("Invalid number or literal: " + literal); + } + } + + static std::vector parse_array(std::string::const_iterator& it, const std::string::const_iterator& end) { + std::vector arr; + if (*it == '[') ++it; + skip_whitespace(it, end); + while (it != end && *it != ']') { + arr.push_back(parse_node(it, end)); + skip_whitespace(it, end); + if (it != end && *it == ',') { + ++it; + skip_whitespace(it, end); + } + } + if (it != end && *it == ']') ++it; + return arr; + } + + static std::map parse_object(std::string::const_iterator& it, const std::string::const_iterator& end) { + std::map obj; + if (*it == '{') ++it; + skip_whitespace(it, end); + while (it != end && *it != '}') { + std::string key = parse_string(it, end); + skip_whitespace(it, end); + if (it != end && *it == ':') ++it; + skip_whitespace(it, end); + obj[key] = parse_node(it, end); + skip_whitespace(it, end); + if (it != end && *it == ',') { + ++it; + skip_whitespace(it, end); + } + } + if (it != end && *it == '}') ++it; + return obj; + } + + static Node parse_node(std::string::const_iterator& it, const std::string::const_iterator& end) { + skip_whitespace(it, end); + if (it == end) throw std::runtime_error("Unexpected end of input"); + switch (*it) { + case '{': return Node(parse_object(it, end)); + case '[': return Node(parse_array(it, end)); + case '"': return Node(parse_string(it, end)); + default: return parse_number_or_literal(it, end); + } + } + + static Node parse(const std::string& json_str) { + auto it = json_str.cbegin(); + return parse_node(it, json_str.cend()); + } +}; + +#endif \ No newline at end of file diff --git a/util/tdgame/enemy.hpp b/util/tdgame/enemy.hpp new file mode 100644 index 0000000..3f85123 --- /dev/null +++ b/util/tdgame/enemy.hpp @@ -0,0 +1,240 @@ +#ifndef ENEMY_HPP +#define ENEMY_HPP + +#include "../grid/mesh.hpp" +#include "customjson.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +// Forward declaration +class Enemy; + +namespace fs = std::filesystem; + +enum EnemyType { GROUND, AIR }; + +// Holds the prototype data for a type of enemy, loaded from JSON. +struct EnemyPrototype { + std::string typeId; + std::shared_ptr mesh; + float maxHp = 100.0f; + float speed = 1.0f; + int baseDamage = 1; + int reward = 5; + EnemyType type = EnemyType::GROUND; + std::vector abilities; +}; + +// Represents a single active enemy instance in the game. +class Enemy { +private: + std::vector _path; + size_t _pathIndex = 0; + bool _isAlive = true; + +public: + int instanceId; + std::string typeId; + std::shared_ptr mesh; + Vector3f position; + float maxHp; + float hp; + float speed; + int baseDamage; + int reward; + EnemyType type; + + Enemy(int instId, const EnemyPrototype& proto, const Vector3f& startPos) + : instanceId(instId), + typeId(proto.typeId), + mesh(std::make_shared(*proto.mesh)), // Deep copy mesh to allow individual modifications (e.g., color) + position(startPos), + maxHp(proto.maxHp), + hp(proto.maxHp), + speed(proto.speed), + baseDamage(proto.baseDamage), + reward(proto.reward), + type(proto.type) + { + if (mesh) { + mesh->setSubId(instId); + mesh->translate(position); + } + } + + // A* pathfinding to get to the base + void setPath(const std::vector& newPath) { + _path = newPath; + _pathIndex = 0; + } + + const std::vector& getPath() const { + return _path; + } + + // Moves the enemy along its path. Returns true if it reached the end. + bool update(float deltaTime) { + if (!_isAlive || _path.empty() || _pathIndex >= _path.size()) { + return _pathIndex >= _path.size(); + } + + Vector3f target = _path[_pathIndex]; + Vector3f direction = target - position; + float distanceToTarget = direction.norm(); + + float moveDist = speed * deltaTime; + + if (moveDist >= distanceToTarget) { + position = target; + _pathIndex++; + if (_pathIndex >= _path.size()) { + // Reached the base + _isAlive = false; + return true; + } + } else { + position += direction.normalized() * moveDist; + } + + if (mesh) { + // This is inefficient. A better approach would be to update a transform matrix. + // For now, we recreate the mesh at the new position. + auto original_verts = mesh->vertices(); // assuming this mesh is a prototype + for (auto& v : original_verts) { + v += position; + } + mesh->vertices(original_verts); + } + return false; + } + + void takeDamage(float amount) { + if (!_isAlive) return; + hp -= amount; + if (hp <= 0) { + hp = 0; + _isAlive = false; + } + } + + bool isAlive() const { + return _isAlive; + } +}; + + +// Manages storage and retrieval of enemy types from JSON definitions. +class EnemyRegistry { +private: + std::map _prototypes; + int _nextInstanceId = 0; + + EnemyRegistry() = default; + + template + T get_value(const std::map& obj, const std::string& key, T default_val) { + if (!obj.count(key) || obj.at(key).is_null()) return default_val; + const auto& node = obj.at(key); + if constexpr (std::is_same_v) return node.as_bool(); + if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) return static_cast(node.as_double()); + if constexpr (std::is_same_v) return node.as_string(); + return default_val; + }; + + void parseEnemyJson(const std::map& j) { + EnemyPrototype p; + p.typeId = get_value(j, "id", std::string("unknown")); + p.maxHp = get_value(j, "maxHp", 100.0f); + p.speed = get_value(j, "speed", 1.0f); + p.baseDamage = get_value(j, "baseDamage", 1); + p.reward = get_value(j, "reward", 5); + + std::string typeStr = get_value(j, "type", std::string("GROUND")); + if (typeStr == "AIR") { + p.type = EnemyType::AIR; + } else { + p.type = EnemyType::GROUND; + } + + if (j.count("abilities")) { + for (const auto& ability_node : j.at("abilities").as_array()) { + p.abilities.push_back(ability_node.as_string()); + } + } + + std::string mesh_path = get_value(j, "mesh_path", std::string("")); + if (!mesh_path.empty()) { + p.mesh = std::make_shared(0, std::vector{}, std::vector>{}, std::vector{}); + if (!p.mesh->load(mesh_path)) { + std::cerr << "Warning: Failed to load mesh '" << mesh_path << "' for enemy '" << p.typeId << "'." << std::endl; + p.mesh = nullptr; // Invalidate if load fails + } + } + + if (_prototypes.count(p.typeId)) { + std::cerr << "Warning: Duplicate enemy ID '" << p.typeId << "' found. Overwriting." << std::endl; + } + _prototypes[p.typeId] = p; + } + + void loadEnemyFile(const std::string& filepath) { + std::ifstream f(filepath); + if (!f.is_open()) return; + + std::stringstream buffer; + buffer << f.rdbuf(); + + try { + customJson::Node root = customJson::parse(buffer.str()); + if (const auto* arr = std::get_if>(&root.value)) { + for (const auto& item : *arr) { + parseEnemyJson(item.as_object()); + } + } else if (const auto* obj = std::get_if>(&root.value)) { + parseEnemyJson(*obj); + } + } catch (const std::exception& e) { + std::cerr << "JSON Parse error in " << filepath << ": " << e.what() << std::endl; + } + } + +public: + static EnemyRegistry& getInstance() { + static EnemyRegistry instance; + return instance; + } + + EnemyRegistry(const EnemyRegistry&) = delete; + void operator=(const EnemyRegistry&) = delete; + + void loadFromDirectory(const std::string& path) { + if (!fs::exists(path)) { + std::cerr << "EnemyRegistry: Directory " << path << " does not exist." << std::endl; + return; + } + for (const auto& entry : fs::directory_iterator(path)) { + if (entry.path().extension() == ".json") { + loadEnemyFile(entry.path().string()); + } + } + std::cout << "EnemyRegistry: Loaded " << _prototypes.size() << " enemy definitions." << std::endl; + } + + std::unique_ptr createEnemy(const std::string& typeId, const Vector3f& startPosition) { + if (_prototypes.count(typeId) == 0) { + std::cerr << "Error: Attempted to create unknown enemy type: " << typeId << std::endl; + return nullptr; + } + + const auto& proto = _prototypes.at(typeId); + return std::make_unique(_nextInstanceId++, proto, startPosition); + } +}; + +#endif \ No newline at end of file diff --git a/util/tdgame/game_utils.hpp b/util/tdgame/game_utils.hpp new file mode 100644 index 0000000..19fe0ec --- /dev/null +++ b/util/tdgame/game_utils.hpp @@ -0,0 +1,68 @@ +#ifndef GAME_UTILS_HPP +#define GAME_UTILS_HPP + +#include +#include +#include +#include +#include +#include +#include +#include "../../eigen/Eigen/Dense" + +using Vector3f = Eigen::Vector3f; + +class FileUtils { +public: + static std::string readFile(const std::string& path) { + std::ifstream file(path); + if (!file.is_open()) { + std::cerr << "Failed to open file: " << path << std::endl; + return "{}"; // Return empty JSON obj on fail + } + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); + } +}; + +class SimpleJsonParser { +public: + static std::map parseDepth1(const std::string& raw) { + std::map result; + std::string clean = raw; + // Remove braces and quotes + clean.erase(std::remove(clean.begin(), clean.end(), '{'), clean.end()); + clean.erase(std::remove(clean.begin(), clean.end(), '}'), clean.end()); + clean.erase(std::remove(clean.begin(), clean.end(), '\"'), clean.end()); + + std::stringstream ss(clean); + std::string segment; + while(std::getline(ss, segment, ',')) { + size_t colonPos = segment.find(':'); + if(colonPos != std::string::npos) { + std::string key = segment.substr(0, colonPos); + std::string val = segment.substr(colonPos + 1); + + // Trim key + size_t first = key.find_first_not_of(" \t\n\r"); + size_t last = key.find_last_not_of(" \t\n\r"); + if (first != std::string::npos) key = key.substr(first, (last - first + 1)); + + // Trim val + first = val.find_first_not_of(" \t\n\r"); + last = val.find_last_not_of(" \t\n\r"); + if (first != std::string::npos) val = val.substr(first, (last - first + 1)); + + result[key] = val; + } + } + return result; + } +}; + + + + +#endif + diff --git a/util/tdgame/map.hpp b/util/tdgame/map.hpp new file mode 100644 index 0000000..430cbcf --- /dev/null +++ b/util/tdgame/map.hpp @@ -0,0 +1,333 @@ +#ifndef TDGAME_MAP_HPP +#define TDGAME_MAP_HPP + +#include "tile.hpp" +#include "enemy.hpp" +#include "tower.hpp" +#include "customjson.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// A simple struct to represent a point on the grid. +struct GridPoint { + int x, z; + bool operator==(const GridPoint& other) const { return x == other.x && z == other.z; } + bool operator<(const GridPoint& other) const { return x < other.x || (x == other.x && z < other.z); } +}; + +// Represents the entire game map, managing tiles, entities, and game state. +class GameMap { +private: + int _width = 0; + int _height = 0; + std::vector> _tiles; + + std::vector> _enemies; + std::vector> _towers; + + std::vector _spawnPoints; + std::vector _basePoints; + GridPoint _primaryBaseTarget; // A single target for pathfinding simplicity + + int _playerHealth = 100; + int _currentWave = 0; + bool _gameOver = false; + + // Internal helper for A* pathfinding + struct PathNode { + GridPoint pos; + float g_cost = 0; // Cost from start + float h_cost = 0; // Heuristic cost to end + GridPoint parent; + + float f_cost() const { return g_cost + h_cost; } + + // For priority queue ordering + bool operator>(const PathNode& other) const { return f_cost() > other.f_cost(); } + }; + +public: + GameMap() = default; + int _playerMoney = 250; + + // --- Primary Methods --- + + // Loads a map definition from a JSON file. + bool loadFromFile(const std::string& filepath) { + std::ifstream f(filepath); + if (!f.is_open()) { + std::cerr << "GameMap: Failed to open map file: " << filepath << std::endl; + return false; + } + + std::stringstream buffer; + buffer << f.rdbuf(); + + try { + customJson::Node root = customJson::parse(buffer.str()); + const auto& j = root.as_object(); + + _width = j.at("width").as_double(); + _height = j.at("height").as_double(); + _playerHealth = j.at("start_health").as_double(); + _playerMoney = j.at("start_money").as_double(); + + const auto& key_obj = j.at("tile_key").as_object(); + std::map tileKey; + for(const auto& pair : key_obj) { + tileKey[pair.first[0]] = pair.second.as_string(); + } + + const auto& layout = j.at("layout").as_array(); + _tiles.assign(_height, std::vector(_width)); + _spawnPoints.clear(); + _basePoints.clear(); + + for (int z = 0; z < _height; ++z) { + std::string row_str = layout[z].as_string(); + std::stringstream row_stream(row_str); + char tileChar; + for (int x = 0; x < _width; ++x) { + row_stream >> tileChar; + if (tileKey.count(tileChar)) { + std::string tileId = tileKey.at(tileChar); + _tiles[z][x] = TileRegistry::getInstance().createTile(tileId, x, z); + + if (_tiles[z][x].type == TileType::SPAWN) { + _spawnPoints.push_back({x, z}); + } else if (_tiles[z][x].type == TileType::BASE) { + _basePoints.push_back({x, z}); + } + } + } + } + + if (!_basePoints.empty()) { + _primaryBaseTarget = _basePoints[0]; // Simple pathfinding target + } else { + std::cerr << "GameMap: Warning, map has no base tiles defined." << std::endl; + } + + } catch (const std::exception& e) { + std::cerr << "GameMap: JSON parse error in " << filepath << ": " << e.what() << std::endl; + return false; + } + + std::cout << "GameMap: Successfully loaded map '" << filepath << "'." << std::endl; + return true; + } + + // The main game loop tick function. + void update(float deltaTime) { + if (_gameOver) return; + + handleSpawning(deltaTime); + + for (auto& tower : _towers) { + tower->update(deltaTime, _enemies); + } + + for (auto& enemy : _enemies) { + if (enemy->update(deltaTime)) { + // Enemy reached the base + _playerHealth -= enemy->baseDamage; + if (_playerHealth <= 0) { + _playerHealth = 0; + _gameOver = true; + std::cout << "Game Over!" << std::endl; + } + } + } + + // Cleanup dead/finished enemies + auto initialSize = _enemies.size(); + _enemies.erase(std::remove_if(_enemies.begin(), _enemies.end(), + [this](const std::unique_ptr& e) { + if (!e->isAlive()) { + if (e->hp <= 0) { // Died to a tower + this->_playerMoney += e->reward; + } + return true; + } + return false; + }), + _enemies.end()); + } + + // Handles player action to build a tower. + bool buildTower(const std::string& towerId, int x, int z) { + if (isOutOfBounds(x, z) || !_tiles[z][x].isBuildable()) { + return false; + } + + const TowerPrototype* proto = TowerRegistry::getInstance().getPrototype(towerId); + if (!proto || _playerMoney < proto->baseCost) { + return false; + } + + _playerMoney -= proto->baseCost; + Vector3f position(static_cast(x), 0.0f, static_cast(z)); + auto newTower = TowerRegistry::getInstance().createTower(towerId, position); + if (newTower) { + _towers.push_back(std::move(newTower)); + // Mark tile as occupied - a simple approach + _tiles[z][x].type = TileType::SPECIAL; // Mark as non-buildable + return true; + } + return false; + } + + // Calculates a path from start to end using A*. + std::vector findPath(GridPoint start, GridPoint end, EnemyType enemyType) { + std::vector path; + std::priority_queue, std::greater> openSet; + std::map allNodes; + + PathNode startNode; + startNode.pos = start; + startNode.g_cost = 0; + startNode.h_cost = std::abs(start.x - end.x) + std::abs(start.z - end.z); // Manhattan distance + openSet.push(startNode); + allNodes[start] = startNode; + + GridPoint neighbors[] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; // 4-directional movement + + while (!openSet.empty()) { + PathNode current = openSet.top(); + openSet.pop(); + + if (current.pos == end) { + // Reconstruct path + GridPoint temp = current.pos; + while (!(temp == start)) { + path.push_back(Vector3f(temp.x, 0, temp.z)); + temp = allNodes.at(temp).parent; + } + path.push_back(Vector3f(start.x, 0, start.z)); + std::reverse(path.begin(), path.end()); + return path; + } + + for (const auto& offset : neighbors) { + GridPoint neighborPos = {current.pos.x + offset.x, current.pos.z + offset.z}; + + if (isOutOfBounds(neighborPos.x, neighborPos.z) || !isTilePassable(neighborPos.x, neighborPos.z, enemyType)) { + continue; + } + + float new_g_cost = current.g_cost + 1.0f; // Assuming cost is 1 per tile for now + + if (allNodes.find(neighborPos) == allNodes.end() || new_g_cost < allNodes[neighborPos].g_cost) { + PathNode neighborNode; + neighborNode.pos = neighborPos; + neighborNode.parent = current.pos; + neighborNode.g_cost = new_g_cost; + neighborNode.h_cost = std::abs(neighborPos.x - end.x) + std::abs(neighborPos.z - end.z); + allNodes[neighborPos] = neighborNode; + openSet.push(neighborNode); + } + } + } + + return {}; // Return empty path if none found + } + + // --- Accessors --- + int getWidth() const { return _width; } + int getHeight() const { return _height; } + int getPlayerHealth() const { return _playerHealth; } + int getPlayerMoney() const { return _playerMoney; } + bool isGameOver() const { return _gameOver; } + const Tile& getTile(int x, int z) const { return _tiles[z][x]; } + const std::vector>& getEnemies() const { return _enemies; } + const std::vector>& getTowers() const { return _towers; } + + +private: + // --- Private Helpers --- + + void handleSpawning(float deltaTime) { + for (const auto& sp : _spawnPoints) { + Tile& spawnTile = _tiles[sp.z][sp.x]; + if (!spawnTile.spawn.has_value()) continue; + + SpawnProperties& props = *spawnTile.spawn; + if (props.currentWaveIndex >= props.waves.size()) { + if(props.loopWaves) { + // Loop waves with scaling + props.currentWaveIndex = 0; + for(auto& wave : props.waves) { + wave.healthMult += props.loopHealthScaler; + wave.speedMult += props.loopSpeedScaler; + } + } else { + continue; // No more waves + } + } + + WaveDefinition& currentWave = props.waves[props.currentWaveIndex]; + // Simple logic: spawn one enemy per interval if count > 0 + // A more robust system would use a timer. + static float spawnCooldown = 0.0f; + spawnCooldown -= deltaTime; + + if (spawnCooldown <= 0 && currentWave.count > 0) { + spawnEnemy(currentWave, sp); + currentWave.count--; + spawnCooldown = currentWave.interval; + + if (currentWave.count <= 0) { + props.currentWaveIndex++; + } + } + } + } + + void spawnEnemy(const WaveDefinition& waveDef, const GridPoint& spawnPos) { + Vector3f startPosition(static_cast(spawnPos.x), 0.0f, static_cast(spawnPos.z)); + auto enemy = EnemyRegistry::getInstance().createEnemy(waveDef.enemyId, startPosition); + if (!enemy) return; + + // Apply wave multipliers + enemy->maxHp *= waveDef.healthMult; + enemy->hp = enemy->maxHp; + enemy->speed *= waveDef.speedMult; + enemy->reward = static_cast(enemy->reward * waveDef.rewardMult); + + auto path = findPath(spawnPos, _primaryBaseTarget, enemy->type); + if (path.empty()) { + std::cerr << "GameMap: Could not find path for enemy " << waveDef.enemyId << std::endl; + return; + } + + enemy->setPath(path); + _enemies.push_back(std::move(enemy)); + } + + bool isOutOfBounds(int x, int z) const { + return x < 0 || x >= _width || z < 0 || z >= _height; + } + + bool isTilePassable(int x, int z, EnemyType type) const { + const auto& tile = _tiles[z][x]; + if (tile.path.has_value()) { + return (type == EnemyType::GROUND && tile.path->isGroundPath) || (type == EnemyType::AIR && tile.path->isFlyingPath); + } + return false; + } +}; + +#endif \ No newline at end of file diff --git a/util/tdgame/tile.hpp b/util/tdgame/tile.hpp new file mode 100644 index 0000000..bb19cb7 --- /dev/null +++ b/util/tdgame/tile.hpp @@ -0,0 +1,222 @@ +#ifndef TDGAME_TILE_HPP +#define TDGAME_TILE_HPP + +#include "../grid/mesh.hpp" +#include "enemy.hpp" +#include "customjson.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +struct PathProperties { + float speedMultiplier = 1.0f; + std::vector effects; + bool isFlyingPath = true; + bool isGroundPath = true; +}; + +struct WallProperties { + bool blocksGround = true; + bool blocksAir = false; + bool blocksProjectiles = true; + std::vector whitelist; +}; + +struct BaseProperties { + float healthBonus = 0.0f; + float defenseBonus = 0.0f; + int levelRequired = 0; +}; + +struct WaveDefinition { + std::string enemyId; + int count; + float interval; + float healthMult = 1.0f; + float speedMult = 1.0f; + float rewardMult = 1.0f; +}; + +struct SpawnProperties { + std::vector waves; + bool loopWaves = true; + float loopHealthScaler = 0.1f; + float loopSpeedScaler = 0.05f; + int currentWaveIndex = 0; +}; + +struct TowerBaseProperties { + float rangeMultiplier = 1.0f; + float damageMultiplier = 1.0f; + float fireRateMultiplier = 1.0f; + std::vector allowedTowerTypes; +}; + +enum TileType { EMPTY, PATH, WALL, BASE, SPAWN, TOWER_BASE, MULTI, SPECIAL }; + +struct Tile { + int x = 0; + int z = 0; + std::string id = "void"; + TileType type = TileType::EMPTY; + std::shared_ptr mesh; + std::optional path; + std::optional wall; + std::optional base; + std::optional spawn; + std::optional towerBase; + std::map specialParams; + Tile() = default; + bool isWalkable() const { + return path.has_value() && path->isGroundPath; + } + bool isBuildable() const { + return towerBase.has_value() || type == TileType::EMPTY; + } + void setMeshColor(Color c) { + if(mesh) mesh->colors({c}); + } +}; + +class TileRegistry { +private: + std::map _prototypes; +public: + static TileRegistry& getInstance() { + static TileRegistry instance; + return instance; + } + + void loadFromDirectory(const std::string& path) { + if (!fs::exists(path)) { + std::cerr << "TileRegistry: Directory " << path << " does not exist." << std::endl; + return; + } + for (const auto& entry : fs::directory_iterator(path)) { + if (entry.path().extension() == ".json") { + loadTileFile(entry.path().string()); + } + } + std::cout << "TileRegistry: Loaded " << _prototypes.size() << " tile definitions." << std::endl; + } + + Tile createTile(const std::string& id, int x, int z) { + if (_prototypes.count(id)) { + Tile t = _prototypes.at(id); + t.x = x; + t.z = z; + return t; + } + Tile t; + t.x = x; + t.z = z; + t.id = "error"; + std::cerr << "TileRegistry: Warning, requested unknown tile ID: " << id << std::endl; + return t; + } +private: + void loadTileFile(const std::string& filepath) { + std::ifstream f(filepath); + if (!f.is_open()) return; + + std::stringstream buffer; + buffer << f.rdbuf(); + std::string content = buffer.str(); + + try { + customJson::Node root = customJson::parse(content); + if (const auto* arr = std::get_if>(&root.value)) { + for (const auto& item : *arr) { + parseTileJson(item.as_object()); + } + } else if (const auto* obj = std::get_if>(&root.value)) { + parseTileJson(*obj); + } + } catch (const std::exception& e) { + std::cerr << "JSON Parse error in " << filepath << ": " << e.what() << std::endl; + } + } + + template + T get_value(const std::map& obj, const std::string& key, T default_val) { + if (!obj.count(key) || obj.at(key).is_null()) return default_val; + const auto& node = obj.at(key); + if constexpr (std::is_same_v) return node.as_bool(); + if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) return static_cast(node.as_double()); + if constexpr (std::is_same_v) return node.as_string(); + return default_val; + }; + + void parseTileJson(const std::map& j) { + Tile t; + t.id = get_value(j, "id", std::string("unknown")); + std::string typeStr = get_value(j, "type", std::string("empty")); + + if (typeStr == "path") t.type = TileType::PATH; + else if (typeStr == "wall") t.type = TileType::WALL; + else if (typeStr == "base") t.type = TileType::BASE; + else if (typeStr == "spawn") t.type = TileType::SPAWN; + else if (typeStr == "tower_base") t.type = TileType::TOWER_BASE; + else if (typeStr == "multi") t.type = TileType::MULTI; + else if (typeStr == "special") t.type = TileType::SPECIAL; + else t.type = TileType::EMPTY; + + if (j.count("path")) { + const auto& p_obj = j.at("path").as_object(); + PathProperties p; + p.speedMultiplier = get_value(p_obj, "speed_mult", 1.0f); + p.isGroundPath = get_value(p_obj, "ground", true); + p.isFlyingPath = get_value(p_obj, "air", true); + if(p_obj.count("effects")) { + for(const auto& effect_node : p_obj.at("effects").as_array()) { + p.effects.push_back(effect_node.as_string()); + } + } + t.path = p; + } + + if (j.count("wall")) { + const auto& w_obj = j.at("wall").as_object(); + WallProperties w; + w.blocksGround = get_value(w_obj, "block_ground", true); + w.blocksAir = get_value(w_obj, "block_air", false); + t.wall = w; + } + + if (j.count("spawn")) { + const auto& s_obj = j.at("spawn").as_object(); + SpawnProperties sp; + sp.loopWaves = get_value(s_obj, "loop", true); + sp.loopHealthScaler = get_value(s_obj, "loop_hp_scale", 0.1f); + if (s_obj.count("waves")) { + for (const auto& w_node : s_obj.at("waves").as_array()) { + const auto& wj = w_node.as_object(); + WaveDefinition wd; + wd.enemyId = get_value(wj, "enemy_id", "grunt"); + wd.count = get_value(wj, "count", 5); + wd.interval = get_value(wj, "interval", 1.0f); + wd.healthMult = get_value(wj, "hp_mult", 1.0f); + sp.waves.push_back(wd); + } + } + t.spawn = sp; + } + + if (_prototypes.count(t.id)) { + std::cerr << "Warning: Duplicate tile ID '" << t.id << "' found. Overwriting." << std::endl; + } + _prototypes[t.id] = t; + } +}; + +#endif \ No newline at end of file diff --git a/util/tdgame/tower.hpp b/util/tdgame/tower.hpp new file mode 100644 index 0000000..3afcafc --- /dev/null +++ b/util/tdgame/tower.hpp @@ -0,0 +1,317 @@ +#ifndef TDGAME_TOWER_HPP +#define TDGAME_TOWER_HPP + +#include "../grid/mesh.hpp" +#include "enemy.hpp" +#include "customjson.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// Forward declaration +class Tower; + +enum class TargetingPriority { + FIRST, // Enemy furthest along the path + LAST, // Enemy least far along the path + CLOSEST, // Enemy closest to the tower + STRONGEST, // Enemy with the highest max HP + WEAKEST // Enemy with the lowest current HP +}; + +// Represents a single upgrade level for a tower +struct TowerUpgrade { + int cost = 50; + float rangeBonus = 1.0f; + float damageBonus = 5.0f; + float fireRateBonus = 0.2f; +}; + +// Holds the prototype data for a type of tower, loaded from JSON. +struct TowerPrototype { + std::string typeId; + std::string name; + std::shared_ptr mesh; + + int baseCost = 100; + float baseRange = 10.0f; + float baseDamage = 10.0f; + float baseFireRate = 1.0f; // shots per second + + TargetingPriority targetingPriority = TargetingPriority::FIRST; + std::string projectileTypeId; // For a future projectile system + std::vector upgrades; +}; + +// Represents a single active tower instance in the game. +class Tower { +private: + int _level = 1; + float _fireCooldown = 0.0f; + Enemy* _target = nullptr; // Raw pointer to the target, validated each frame + +public: + int instanceId; + std::string typeId; + std::shared_ptr mesh; + Vector3f position; + + // Current stats (including upgrades) + float range; + float damage; + float fireRate; + int cost; + + TargetingPriority targetingPriority; + + Tower(int instId, const TowerPrototype& proto, const Vector3f& pos) + : instanceId(instId), + typeId(proto.typeId), + position(pos), + range(proto.baseRange), + damage(proto.baseDamage), + fireRate(proto.baseFireRate), + cost(proto.baseCost), + targetingPriority(proto.targetingPriority) + { + if (proto.mesh) { + mesh = std::make_shared(*proto.mesh); // Deep copy for individual manipulation + mesh->setSubId(instId); + mesh->translate(position); + } + } + + // Main logic loop for targeting and firing + void update(float deltaTime, const std::vector>& enemies) { + _fireCooldown -= deltaTime; + + bool targetIsValid = false; + if (_target != nullptr) { + if (_target->isAlive() && (_target->position - position).norm() <= range) { + targetIsValid = true; + } + } + + if (!targetIsValid) { + _target = nullptr; + findTarget(enemies); + } + + if (_target != nullptr && _fireCooldown <= 0.0f) { + // "Fire" at the target. A real implementation would create a projectile. + _target->takeDamage(damage); + + // Reset cooldown + _fireCooldown = 1.0f / fireRate; + } + } + + // Finds a new target based on the tower's targeting priority + void findTarget(const std::vector>& enemies) { + _target = nullptr; + Enemy* bestTarget = nullptr; + float bestMetric = -1.0f; // For FIRST, STRONGEST (higher is better) + + if (targetingPriority == TargetingPriority::CLOSEST || targetingPriority == TargetingPriority::WEAKEST || targetingPriority == TargetingPriority::LAST) { + bestMetric = std::numeric_limits::max(); // For priorities where lower is better + } + + for (const auto& enemyPtr : enemies) { + if (!enemyPtr->isAlive()) continue; + + float distance = (enemyPtr->position - this->position).norm(); + if (distance > this->range) continue; + + switch (targetingPriority) { + case TargetingPriority::FIRST: + if (distance < (bestMetric == -1.0f ? std::numeric_limits::max() : bestMetric)) { + bestMetric = distance; + bestTarget = enemyPtr.get(); + } + break; + case TargetingPriority::LAST: + if (distance < bestMetric) { + bestMetric = distance; + bestTarget = enemyPtr.get(); + } + break; + case TargetingPriority::CLOSEST: + if (distance < bestMetric) { + bestMetric = distance; + bestTarget = enemyPtr.get(); + } + break; + case TargetingPriority::STRONGEST: + if (enemyPtr->maxHp > bestMetric) { + bestMetric = enemyPtr->maxHp; + bestTarget = enemyPtr.get(); + } + break; + case TargetingPriority::WEAKEST: + if (enemyPtr->hp < bestMetric) { + bestMetric = enemyPtr->hp; + bestTarget = enemyPtr.get(); + } + break; + } + } + _target = bestTarget; + } + + // Applies the next available upgrade from the prototype + bool upgrade(const TowerPrototype& proto) { + // Upgrades are 0-indexed for level 2, 3, etc. + // For level 'L', we need upgrade at index 'L-1'. + if (_level - 1 < proto.upgrades.size()) { + const auto& up = proto.upgrades[_level - 1]; + + this->cost += up.cost; + this->range += up.rangeBonus; + this->damage += up.damageBonus; + this->fireRate += up.fireRateBonus; + _level++; + return true; + } + return false; // Max level reached + } + + int getLevel() const { return _level; } + int getSellPrice() const { return static_cast(cost * 0.75f); } + const Enemy* getTarget() const { return _target; } +}; + + +// Manages storage and retrieval of tower types from JSON definitions. +class TowerRegistry { +private: + std::map _prototypes; + int _nextInstanceId = 0; + + TowerRegistry() = default; + + template + T get_value(const std::map& obj, const std::string& key, T default_val) { + if (!obj.count(key) || obj.at(key).is_null()) return default_val; + const auto& node = obj.at(key); + if constexpr (std::is_same_v) return node.as_bool(); + if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) return static_cast(node.as_double()); + if constexpr (std::is_same_v) return node.as_string(); + return default_val; + }; + + void parseTowerJson(const std::map& j) { + TowerPrototype p; + p.typeId = get_value(j, "id", std::string("unknown")); + p.name = get_value(j, "name", std::string("Unnamed Tower")); + p.baseCost = get_value(j, "cost", 100); + p.baseRange = get_value(j, "range", 10.0f); + p.baseDamage = get_value(j, "damage", 10.0f); + p.baseFireRate = get_value(j, "fire_rate", 1.0f); + p.projectileTypeId = get_value(j, "projectile_id", std::string("")); + + std::string priorityStr = get_value(j, "targeting", std::string("first")); + if (priorityStr == "last") p.targetingPriority = TargetingPriority::LAST; + else if (priorityStr == "closest") p.targetingPriority = TargetingPriority::CLOSEST; + else if (priorityStr == "strongest") p.targetingPriority = TargetingPriority::STRONGEST; + else if (priorityStr == "weakest") p.targetingPriority = TargetingPriority::WEAKEST; + else p.targetingPriority = TargetingPriority::FIRST; + + if (j.count("upgrades") && !j.at("upgrades").is_null()) { + for (const auto& up_node : j.at("upgrades").as_array()) { + const auto& up_obj = up_node.as_object(); + TowerUpgrade up; + up.cost = get_value(up_obj, "cost", 50); + up.rangeBonus = get_value(up_obj, "range_bonus", 1.0f); + up.damageBonus = get_value(up_obj, "damage_bonus", 5.0f); + up.fireRateBonus = get_value(up_obj, "fire_rate_bonus", 0.2f); + p.upgrades.push_back(up); + } + } + + std::string mesh_path = get_value(j, "mesh_path", std::string("")); + if (!mesh_path.empty()) { + p.mesh = std::make_shared(0, std::vector{}, std::vector>{}, std::vector{}); + if (!p.mesh->load(mesh_path)) { + std::cerr << "Warning: Failed to load mesh '" << mesh_path << "' for tower '" << p.typeId << "'." << std::endl; + p.mesh = nullptr; + } + } + + if (_prototypes.count(p.typeId)) { + std::cerr << "Warning: Duplicate tower ID '" << p.typeId << "' found. Overwriting." << std::endl; + } + _prototypes[p.typeId] = p; + } + + void loadTowerFile(const std::string& filepath) { + std::ifstream f(filepath); + if (!f.is_open()) return; + + std::stringstream buffer; + buffer << f.rdbuf(); + + try { + customJson::Node root = customJson::parse(buffer.str()); + if (const auto* arr = std::get_if>(&root.value)) { + for (const auto& item : *arr) { + parseTowerJson(item.as_object()); + } + } else if (const auto* obj = std::get_if>(&root.value)) { + parseTowerJson(*obj); + } + } catch (const std::exception& e) { + std::cerr << "JSON Parse error in " << filepath << ": " << e.what() << std::endl; + } + } + +public: + static TowerRegistry& getInstance() { + static TowerRegistry instance; + return instance; + } + + TowerRegistry(const TowerRegistry&) = delete; + void operator=(const TowerRegistry&) = delete; + + void loadFromDirectory(const std::string& path) { + if (!fs::exists(path)) { + std::cerr << "TowerRegistry: Directory " << path << " does not exist." << std::endl; + return; + } + for (const auto& entry : fs::directory_iterator(path)) { + if (entry.path().extension() == ".json") { + loadTowerFile(entry.path().string()); + } + } + std::cout << "TowerRegistry: Loaded " << _prototypes.size() << " tower definitions." << std::endl; + } + + std::unique_ptr createTower(const std::string& typeId, const Vector3f& position) { + if (_prototypes.count(typeId) == 0) { + std::cerr << "Error: Attempted to create unknown tower type: " << typeId << std::endl; + return nullptr; + } + + const auto& proto = _prototypes.at(typeId); + return std::make_unique(_nextInstanceId++, proto, position); + } + + const TowerPrototype* getPrototype(const std::string& typeId) const { + auto it = _prototypes.find(typeId); + if (it != _prototypes.end()) { + return &it->second; + } + return nullptr; + } +}; + +#endif \ No newline at end of file