#ifndef TDGAME_MAIN_HPP #define TDGAME_MAIN_HPP #include "../util/tdgame/tower.hpp" #include "../util/tdgame/enemy.hpp" #include "../util/tdgame/map.hpp" #include "../util/tdgame/tile.hpp" #include "../util/grid/mesh.hpp" #include "../util/tdgame/customjson.hpp" #include #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