#ifndef TDGAME_MAP_HPP #define TDGAME_MAP_HPP #include "tile.hpp" #include "enemy.hpp" #include "tower.hpp" #include "customjson.hpp" #include #include #include #include #include #include #include #include #include #include #include #include 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> _tiles; std::vector> _enemies; std::vector> _towers; std::vector _spawnPoints; std::vector _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 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(_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& 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(x), 0.0f, static_cast(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 findPath(GridPoint start, GridPoint end, EnemyType enemyType) { std::vector path; std::priority_queue, std::greater> openSet; std::map 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>& getEnemies() const { return _enemies; } const std::vector>& 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(spawnPos.x), 0.0f, static_cast(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(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