tdgame test branch by gemini.

This commit is contained in:
Yggdrasil75
2026-02-18 12:28:18 -05:00
parent 14b4d06495
commit 2ad5f13596
14 changed files with 2038 additions and 0 deletions

View File

@@ -335,6 +335,129 @@ public:
else os << " Polys (Cleared) : " << 0 << "\n";
os << " colors : " << _colors.size() << "\n";
}
void writeTo(FILE* f) const {
if (!f) return;
fwrite(&id, sizeof(int), 1, f);
fwrite(&_subId, sizeof(int), 1, f);
size_t vCount = _vertices.size();
fwrite(&vCount, sizeof(size_t), 1, f);
if (vCount > 0) {
fwrite(_vertices.data(), sizeof(Vector3f), vCount, f);
}
size_t cCount = _colors.size();
fwrite(&cCount, sizeof(size_t), 1, f);
if (cCount > 0) {
fwrite(_colors.data(), sizeof(Color), cCount, f);
}
size_t pCount = _polys.size();
fwrite(&pCount, sizeof(size_t), 1, f);
for (const auto& p : _polys) {
size_t idxCount = p.size();
fwrite(&idxCount, sizeof(size_t), 1, f);
if (idxCount > 0) {
fwrite(p.data(), sizeof(int), idxCount, f);
}
}
}
static std::shared_ptr<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 {

124
util/tdgame/customjson.hpp Normal file
View 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
View 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

View 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
View 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
View 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
View 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