tdgame test branch by gemini.
This commit is contained in:
4
data/enemies/definitions.json
Normal file
4
data/enemies/definitions.json
Normal file
@@ -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"}
|
||||||
|
]
|
||||||
24
data/maps/level1.json
Normal file
24
data/maps/level1.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
data/meshes/cube.mesh
Normal file
BIN
data/meshes/cube.mesh
Normal file
Binary file not shown.
BIN
data/meshes/pyramid.mesh
Normal file
BIN
data/meshes/pyramid.mesh
Normal file
Binary file not shown.
14
data/tiles/definitions.json
Normal file
14
data/tiles/definitions.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{"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}}
|
||||||
|
]
|
||||||
11
data/towers/definitions.json
Normal file
11
data/towers/definitions.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
558
tests/tdgame.cpp
Normal file
558
tests/tdgame.cpp
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
#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
|
||||||
@@ -335,6 +335,129 @@ public:
|
|||||||
else os << " Polys (Cleared) : " << 0 << "\n";
|
else os << " Polys (Cleared) : " << 0 << "\n";
|
||||||
os << " colors : " << _colors.size() << "\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<Mesh> 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<Vector3f> 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<Color> 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<std::vector<int>> 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<Mesh>(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<Vector3f> 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<Color> 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<std::vector<int>> 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 {
|
class Scene {
|
||||||
|
|||||||
124
util/tdgame/customjson.hpp
Normal file
124
util/tdgame/customjson.hpp
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#ifndef TDGAME_CJ_HPP
|
||||||
|
#define TDGAME_CJ_HPP
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <sstream>
|
||||||
|
#include <variant>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct customJson {
|
||||||
|
struct Node {
|
||||||
|
std::variant<std::nullptr_t, bool, double, std::string, std::vector<Node>, std::map<std::string, Node>> 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<Node> a) : value(a) {}
|
||||||
|
Node(std::map<std::string, Node> o) : value(o) {}
|
||||||
|
|
||||||
|
// Accessors with type checking
|
||||||
|
const std::map<std::string, Node>& as_object() const { return std::get<std::map<std::string, Node>>(value); }
|
||||||
|
const std::vector<Node>& as_array() const { return std::get<std::vector<Node>>(value); }
|
||||||
|
const std::string& as_string() const { return std::get<std::string>(value); }
|
||||||
|
double as_double() const { return std::get<double>(value); }
|
||||||
|
bool as_bool() const { return std::get<bool>(value); }
|
||||||
|
|
||||||
|
bool is_null() const { return std::holds_alternative<std::nullptr_t>(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<Node> parse_array(std::string::const_iterator& it, const std::string::const_iterator& end) {
|
||||||
|
std::vector<Node> 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<std::string, Node> parse_object(std::string::const_iterator& it, const std::string::const_iterator& end) {
|
||||||
|
std::map<std::string, Node> 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
|
||||||
240
util/tdgame/enemy.hpp
Normal file
240
util/tdgame/enemy.hpp
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
#ifndef ENEMY_HPP
|
||||||
|
#define ENEMY_HPP
|
||||||
|
|
||||||
|
#include "../grid/mesh.hpp"
|
||||||
|
#include "customjson.hpp"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
// 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> mesh;
|
||||||
|
float maxHp = 100.0f;
|
||||||
|
float speed = 1.0f;
|
||||||
|
int baseDamage = 1;
|
||||||
|
int reward = 5;
|
||||||
|
EnemyType type = EnemyType::GROUND;
|
||||||
|
std::vector<std::string> abilities;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Represents a single active enemy instance in the game.
|
||||||
|
class Enemy {
|
||||||
|
private:
|
||||||
|
std::vector<Vector3f> _path;
|
||||||
|
size_t _pathIndex = 0;
|
||||||
|
bool _isAlive = true;
|
||||||
|
|
||||||
|
public:
|
||||||
|
int instanceId;
|
||||||
|
std::string typeId;
|
||||||
|
std::shared_ptr<Mesh> 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<Mesh>(*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<Vector3f>& newPath) {
|
||||||
|
_path = newPath;
|
||||||
|
_pathIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Vector3f>& 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<std::string, EnemyPrototype> _prototypes;
|
||||||
|
int _nextInstanceId = 0;
|
||||||
|
|
||||||
|
EnemyRegistry() = default;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T get_value(const std::map<std::string, customJson::Node>& 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<T, bool>) return node.as_bool();
|
||||||
|
if constexpr (std::is_same_v<T, double> || std::is_same_v<T, float> || std::is_same_v<T, int>) return static_cast<T>(node.as_double());
|
||||||
|
if constexpr (std::is_same_v<T, std::string>) return node.as_string();
|
||||||
|
return default_val;
|
||||||
|
};
|
||||||
|
|
||||||
|
void parseEnemyJson(const std::map<std::string, customJson::Node>& j) {
|
||||||
|
EnemyPrototype p;
|
||||||
|
p.typeId = get_value(j, "id", std::string("unknown"));
|
||||||
|
p.maxHp = get_value<float>(j, "maxHp", 100.0f);
|
||||||
|
p.speed = get_value<float>(j, "speed", 1.0f);
|
||||||
|
p.baseDamage = get_value<int>(j, "baseDamage", 1);
|
||||||
|
p.reward = get_value<int>(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<Mesh>(0, std::vector<Vector3f>{}, std::vector<std::vector<int>>{}, std::vector<Color>{});
|
||||||
|
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<std::vector<customJson::Node>>(&root.value)) {
|
||||||
|
for (const auto& item : *arr) {
|
||||||
|
parseEnemyJson(item.as_object());
|
||||||
|
}
|
||||||
|
} else if (const auto* obj = std::get_if<std::map<std::string, customJson::Node>>(&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<Enemy> 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<Enemy>(_nextInstanceId++, proto, startPosition);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
68
util/tdgame/game_utils.hpp
Normal file
68
util/tdgame/game_utils.hpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#ifndef GAME_UTILS_HPP
|
||||||
|
#define GAME_UTILS_HPP
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <sstream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <map>
|
||||||
|
#include <iostream>
|
||||||
|
#include <algorithm>
|
||||||
|
#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<std::string, std::string> parseDepth1(const std::string& raw) {
|
||||||
|
std::map<std::string, std::string> 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
|
||||||
|
|
||||||
333
util/tdgame/map.hpp
Normal file
333
util/tdgame/map.hpp
Normal file
@@ -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 <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <map>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <queue>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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<std::vector<Tile>> _tiles;
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<Enemy>> _enemies;
|
||||||
|
std::vector<std::unique_ptr<Tower>> _towers;
|
||||||
|
|
||||||
|
std::vector<GridPoint> _spawnPoints;
|
||||||
|
std::vector<GridPoint> _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<char, std::string> 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<Tile>(_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<Enemy>& 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<float>(x), 0.0f, static_cast<float>(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<Vector3f> findPath(GridPoint start, GridPoint end, EnemyType enemyType) {
|
||||||
|
std::vector<Vector3f> path;
|
||||||
|
std::priority_queue<PathNode, std::vector<PathNode>, std::greater<PathNode>> openSet;
|
||||||
|
std::map<GridPoint, PathNode> 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<std::unique_ptr<Enemy>>& getEnemies() const { return _enemies; }
|
||||||
|
const std::vector<std::unique_ptr<Tower>>& 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<float>(spawnPos.x), 0.0f, static_cast<float>(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<int>(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
|
||||||
222
util/tdgame/tile.hpp
Normal file
222
util/tdgame/tile.hpp
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
#ifndef TDGAME_TILE_HPP
|
||||||
|
#define TDGAME_TILE_HPP
|
||||||
|
|
||||||
|
#include "../grid/mesh.hpp"
|
||||||
|
#include "enemy.hpp"
|
||||||
|
#include "customjson.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <memory>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
struct PathProperties {
|
||||||
|
float speedMultiplier = 1.0f;
|
||||||
|
std::vector<std::string> effects;
|
||||||
|
bool isFlyingPath = true;
|
||||||
|
bool isGroundPath = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WallProperties {
|
||||||
|
bool blocksGround = true;
|
||||||
|
bool blocksAir = false;
|
||||||
|
bool blocksProjectiles = true;
|
||||||
|
std::vector<std::string> 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<WaveDefinition> 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<std::string> 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> mesh;
|
||||||
|
std::optional<PathProperties> path;
|
||||||
|
std::optional<WallProperties> wall;
|
||||||
|
std::optional<BaseProperties> base;
|
||||||
|
std::optional<SpawnProperties> spawn;
|
||||||
|
std::optional<TowerBaseProperties> towerBase;
|
||||||
|
std::map<std::string, float> 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<std::string, Tile> _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<std::vector<customJson::Node>>(&root.value)) {
|
||||||
|
for (const auto& item : *arr) {
|
||||||
|
parseTileJson(item.as_object());
|
||||||
|
}
|
||||||
|
} else if (const auto* obj = std::get_if<std::map<std::string, customJson::Node>>(&root.value)) {
|
||||||
|
parseTileJson(*obj);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "JSON Parse error in " << filepath << ": " << e.what() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T get_value(const std::map<std::string, customJson::Node>& 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<T, bool>) return node.as_bool();
|
||||||
|
if constexpr (std::is_same_v<T, double> || std::is_same_v<T, float> || std::is_same_v<T, int>) return static_cast<T>(node.as_double());
|
||||||
|
if constexpr (std::is_same_v<T, std::string>) return node.as_string();
|
||||||
|
return default_val;
|
||||||
|
};
|
||||||
|
|
||||||
|
void parseTileJson(const std::map<std::string, customJson::Node>& 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<float>(p_obj, "speed_mult", 1.0f);
|
||||||
|
p.isGroundPath = get_value<bool>(p_obj, "ground", true);
|
||||||
|
p.isFlyingPath = get_value<bool>(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<bool>(w_obj, "block_ground", true);
|
||||||
|
w.blocksAir = get_value<bool>(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<bool>(s_obj, "loop", true);
|
||||||
|
sp.loopHealthScaler = get_value<float>(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<std::string>(wj, "enemy_id", "grunt");
|
||||||
|
wd.count = get_value<int>(wj, "count", 5);
|
||||||
|
wd.interval = get_value<float>(wj, "interval", 1.0f);
|
||||||
|
wd.healthMult = get_value<float>(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
|
||||||
317
util/tdgame/tower.hpp
Normal file
317
util/tdgame/tower.hpp
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
#ifndef TDGAME_TOWER_HPP
|
||||||
|
#define TDGAME_TOWER_HPP
|
||||||
|
|
||||||
|
#include "../grid/mesh.hpp"
|
||||||
|
#include "enemy.hpp"
|
||||||
|
#include "customjson.hpp"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
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> 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<TowerUpgrade> 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> 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<Mesh>(*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<std::unique_ptr<Enemy>>& 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<std::unique_ptr<Enemy>>& 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<float>::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<float>::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<int>(cost * 0.75f); }
|
||||||
|
const Enemy* getTarget() const { return _target; }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Manages storage and retrieval of tower types from JSON definitions.
|
||||||
|
class TowerRegistry {
|
||||||
|
private:
|
||||||
|
std::map<std::string, TowerPrototype> _prototypes;
|
||||||
|
int _nextInstanceId = 0;
|
||||||
|
|
||||||
|
TowerRegistry() = default;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T get_value(const std::map<std::string, customJson::Node>& 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<T, bool>) return node.as_bool();
|
||||||
|
if constexpr (std::is_same_v<T, double> || std::is_same_v<T, float> || std::is_same_v<T, int>) return static_cast<T>(node.as_double());
|
||||||
|
if constexpr (std::is_same_v<T, std::string>) return node.as_string();
|
||||||
|
return default_val;
|
||||||
|
};
|
||||||
|
|
||||||
|
void parseTowerJson(const std::map<std::string, customJson::Node>& 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<int>(j, "cost", 100);
|
||||||
|
p.baseRange = get_value<float>(j, "range", 10.0f);
|
||||||
|
p.baseDamage = get_value<float>(j, "damage", 10.0f);
|
||||||
|
p.baseFireRate = get_value<float>(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<int>(up_obj, "cost", 50);
|
||||||
|
up.rangeBonus = get_value<float>(up_obj, "range_bonus", 1.0f);
|
||||||
|
up.damageBonus = get_value<float>(up_obj, "damage_bonus", 5.0f);
|
||||||
|
up.fireRateBonus = get_value<float>(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<Mesh>(0, std::vector<Vector3f>{}, std::vector<std::vector<int>>{}, std::vector<Color>{});
|
||||||
|
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<std::vector<customJson::Node>>(&root.value)) {
|
||||||
|
for (const auto& item : *arr) {
|
||||||
|
parseTowerJson(item.as_object());
|
||||||
|
}
|
||||||
|
} else if (const auto* obj = std::get_if<std::map<std::string, customJson::Node>>(&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<Tower> 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<Tower>(_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
|
||||||
Reference in New Issue
Block a user