Files
stupidsimcpp/tests/tdgame.cpp
2026-02-18 12:28:18 -05:00

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