tdgame test branch by gemini.
This commit is contained in:
333
util/tdgame/map.hpp
Normal file
333
util/tdgame/map.hpp
Normal 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
|
||||
Reference in New Issue
Block a user