558 lines
23 KiB
C++
558 lines
23 KiB
C++
#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 <iostream>
|
|
#include <string>
|
|
#include <memory>
|
|
#include <chrono>
|
|
|
|
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<Vector3f> 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<std::vector<int>> 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<Vector3f> 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<std::vector<int>> 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<Mesh>(0, std::vector<Vector3f>{}, std::vector<std::vector<int>>{}, std::vector<Color>{});
|
|
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<Mesh>(0, std::vector<Vector3f>{}, std::vector<std::vector<int>>{}, std::vector<Color>{});
|
|
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<int>(tower->position.x()) == clickedTile.x &&
|
|
static_cast<int>(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<Mesh>(0, std::vector<Vector3f>{}, std::vector<std::vector<int>>{}, std::vector<Color>{});
|
|
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<Mesh>(0, std::vector<Vector3f>{}, std::vector<std::vector<int>>{}, std::vector<Color>{});
|
|
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<Mesh>(*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<GameMap&>(_map).buildTower("dummy", -1, -1); // Hack to access non-const function context
|
|
const_cast<GameMap&>(_map)._playerMoney += 1; // Reverse the dummy build cost
|
|
if (const_cast<GameMap&>(_map)._playerMoney >= nextUpgrade.cost) {
|
|
const_cast<GameMap&>(_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<int>(floor(intersection_point.x()));
|
|
int gridZ = static_cast<int>(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 |