tdgame test branch by gemini.
This commit is contained in:
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