#ifndef ENEMY_HPP #define ENEMY_HPP #include "../grid/mesh.hpp" #include "customjson.hpp" #include #include #include #include #include #include #include #include // 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; float maxHp = 100.0f; float speed = 1.0f; int baseDamage = 1; int reward = 5; EnemyType type = EnemyType::GROUND; std::vector abilities; }; // Represents a single active enemy instance in the game. class Enemy { private: std::vector _path; size_t _pathIndex = 0; bool _isAlive = true; public: int instanceId; std::string typeId; std::shared_ptr 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(*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& newPath) { _path = newPath; _pathIndex = 0; } const std::vector& 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 _prototypes; int _nextInstanceId = 0; EnemyRegistry() = default; template T get_value(const std::map& 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) return node.as_bool(); if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) return static_cast(node.as_double()); if constexpr (std::is_same_v) return node.as_string(); return default_val; }; void parseEnemyJson(const std::map& j) { EnemyPrototype p; p.typeId = get_value(j, "id", std::string("unknown")); p.maxHp = get_value(j, "maxHp", 100.0f); p.speed = get_value(j, "speed", 1.0f); p.baseDamage = get_value(j, "baseDamage", 1); p.reward = get_value(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(0, std::vector{}, std::vector>{}, std::vector{}); 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>(&root.value)) { for (const auto& item : *arr) { parseEnemyJson(item.as_object()); } } else if (const auto* obj = std::get_if>(&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 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(_nextInstanceId++, proto, startPosition); } }; #endif