Files
stupidsimcpp/util/tdgame/tower.hpp
2026-02-18 12:28:18 -05:00

317 lines
11 KiB
C++

#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