#include "terrainmovementmanager.h" #include "unitconfig.h" #include "pathfinder.h" #include "yamlparser.h" #include #include #include TerrainMovementConfig::TerrainMovementConfig() : waterHeightThreshold(6), mountainRockHeightKm(2.3f), snowHeightKm(3.1f), polarSnowLineEnabled(true), polarSnowLineBaseKm(2.5f), polarSnowFactorStart(0.40f), polarSnowFactorEnd(0.85f), polarLandIceThreshold(0.72f), uphillSlopeMultiplier(4.0f), downhillSlopeBonusMultiplier(2.0f), minStepCost(0.25f), maxClimbableSlopeKm(1.2f) { movementCosts["grasland"] = 1.0f; movementCosts["bos"] = 2.0f; movementCosts["steen"] = 2.5f; movementCosts["sahara"] = 1.5f; movementCosts["tundra"] = 1.5f; movementCosts["red_canyon"] = 2.0f; movementCosts["sneeuw"] = 3.0f; movementCosts["water"] = 1.0f; DomainMovementRule landRule; landRule.impassableTerrains = QStringList{"water", "sneeuw"}; domainRules["land"] = landRule; DomainMovementRule navalRule; navalRule.passableTerrains = QStringList{"water"}; domainRules["naval"] = navalRule; } TerrainMovementManager& TerrainMovementManager::instance() { static TerrainMovementManager s_instance; return s_instance; } TerrainMovementManager::TerrainMovementManager() : m_loaded(false) { } bool TerrainMovementManager::loadConfig(const QString &filePath) { YamlNode root = YamlParser::parseFile(filePath); if (root.children.isEmpty()) { qWarning() << "Failed to parse terrain movement config YAML or empty:" << filePath; return false; } if (root.hasChild("water_height_threshold")) { m_config.waterHeightThreshold = root["water_height_threshold"].asInt(6); } if (root.hasChild("mountain_rock_height_km")) { m_config.mountainRockHeightKm = root["mountain_rock_height_km"].asFloat(2.3f); } if (root.hasChild("snow_height_km")) { m_config.snowHeightKm = root["snow_height_km"].asFloat(3.1f); } if (root.hasChild("polar_snow_line_enabled")) { m_config.polarSnowLineEnabled = root["polar_snow_line_enabled"].asBool(true); } if (root.hasChild("polar_snow_line_base_km")) { m_config.polarSnowLineBaseKm = root["polar_snow_line_base_km"].asFloat(2.5f); } if (root.hasChild("polar_snow_factor_start")) { m_config.polarSnowFactorStart = root["polar_snow_factor_start"].asFloat(0.40f); } if (root.hasChild("polar_snow_factor_end")) { m_config.polarSnowFactorEnd = root["polar_snow_factor_end"].asFloat(0.85f); } if (root.hasChild("polar_land_ice_threshold")) { m_config.polarLandIceThreshold = root["polar_land_ice_threshold"].asFloat(0.72f); } if (root.hasChild("uphill_slope_multiplier")) { m_config.uphillSlopeMultiplier = root["uphill_slope_multiplier"].asFloat(4.0f); } if (root.hasChild("downhill_slope_bonus_multiplier")) { m_config.downhillSlopeBonusMultiplier = root["downhill_slope_bonus_multiplier"].asFloat(2.0f); } if (root.hasChild("min_step_cost")) { m_config.minStepCost = root["min_step_cost"].asFloat(0.25f); } if (root.hasChild("max_climbable_slope_km")) { m_config.maxClimbableSlopeKm = root["max_climbable_slope_km"].asFloat(1.2f); } if (root.hasChild("movement_costs")) { const YamlNode &costsNode = root["movement_costs"]; for (auto it = costsNode.children.begin(); it != costsNode.children.end(); ++it) { m_config.movementCosts[it.key()] = it.value().asFloat(1.0f); } } if (root.hasChild("domains")) { const YamlNode &domainsNode = root["domains"]; for (auto it = domainsNode.children.begin(); it != domainsNode.children.end(); ++it) { QString domainName = it.key().trimmed().toLower(); const YamlNode &dNode = it.value(); DomainMovementRule rule; if (dNode.hasChild("impassable_terrains")) { rule.impassableTerrains = dNode["impassable_terrains"].listValues; } if (dNode.hasChild("passable_terrains")) { rule.passableTerrains = dNode["passable_terrains"].listValues; } m_config.domainRules[domainName] = rule; } } m_loaded = true; qDebug() << "TerrainMovementManager successfully loaded movement config from:" << filePath; return true; } QString TerrainMovementManager::getTerrainType(int tx, int ty, Matrix2D *mapPoints, int numPoints05) const { if (!mapPoints || numPoints05 <= 0) return "grasland"; QVector2D w = Pathfinder::hexToWorld(tx, ty); float scaleX = numPoints05 * 2.0f; float scaleY = numPoints05 / LOD_0_SIZE_Y; int mapX = (int) std::round(w.x() * scaleX); int mapY = (int) std::round(w.y() * scaleY); if (mapX < 0) mapX = 0; if (mapX >= (int)mapPoints->getWidth()) mapX = (int)mapPoints->getWidth() - 1; if (mapY < 0) mapY = 0; if (mapY >= (int)mapPoints->getHeight()) mapY = (int)mapPoints->getHeight() - 1; TerrainPoint tp = mapPoints->operator()(mapX, mapY); // 1. Water if (tp.height < m_config.waterHeightThreshold) { return "water"; } float heightKm = (tp.height / 255.0f) * 3.5f; // 2. High peak snow if (heightKm >= m_config.snowHeightKm) { return "sneeuw"; } // 3. Polar latitude snowline if (m_config.polarSnowLineEnabled) { float normY = std::max(0.0f, std::min(1.0f, w.y() / 108.25f)); float distFromCenter = std::abs(normY - 0.5f) * 2.0f; if (distFromCenter >= m_config.polarSnowFactorStart) { float t = std::max(0.0f, std::min(1.0f, (distFromCenter - m_config.polarSnowFactorStart) / (m_config.polarSnowFactorEnd - m_config.polarSnowFactorStart))); float poleFactor = t * t * (3.0f - 2.0f * t); float snowLine = m_config.polarSnowLineBaseKm * (1.0f - poleFactor); if (heightKm >= snowLine) { return "sneeuw"; } } if (distFromCenter >= m_config.polarLandIceThreshold) { float tIce = std::max(0.0f, std::min(1.0f, (distFromCenter - m_config.polarLandIceThreshold) / 0.20f)); float polarLandIce = tIce * tIce * (3.0f - 2.0f * tIce); if (polarLandIce > 0.5f) { return "sneeuw"; } } } // 4. Rocky mountain if (heightKm >= m_config.mountainRockHeightKm) { return "steen"; } // 5. Typemap biomes switch (tp.type) { case 0: return "grasland"; case 1: return "sahara"; case 2: return "tundra"; case 3: return "red_canyon"; case 4: return "bos"; default: return "grasland"; } } bool TerrainMovementManager::isTerrainPassable( const QString &terrainType, MovementDomain domain, const UnitConfig *unitConfig) const { // Unit-specific whitelist overrides if (unitConfig && !unitConfig->passableTerrains.isEmpty()) { return unitConfig->passableTerrains.contains(terrainType); } // Unit-specific blacklist overrides if (unitConfig && unitConfig->impassableTerrains.contains(terrainType)) { return false; } // Domain-level rules QString domainStr = movementDomainToString(domain); auto it = m_config.domainRules.find(domainStr); if (it != m_config.domainRules.end()) { const DomainMovementRule &rule = it.value(); if (!rule.passableTerrains.isEmpty()) { return rule.passableTerrains.contains(terrainType); } if (rule.impassableTerrains.contains(terrainType)) { return false; } } return true; } bool TerrainMovementManager::isHexPassable( int tx, int ty, MovementDomain domain, Matrix2D *mapPoints, int numPoints05, const UnitConfig *unitConfig) const { if (tx < 0 || tx >= MAP_WIDTH || ty < 0 || ty >= MAP_HEIGHT) { return false; } QString tType = getTerrainType(tx, ty, mapPoints, numPoints05); return isTerrainPassable(tType, domain, unitConfig); } float TerrainMovementManager::getTerrainMovementCost(const QString &terrainType) const { return m_config.movementCosts.value(terrainType, 1.0f); } float TerrainMovementManager::getHexMovementCost(int tx, int ty, Matrix2D *mapPoints, int numPoints05) const { QString tType = getTerrainType(tx, ty, mapPoints, numPoints05); return getTerrainMovementCost(tType); } float TerrainMovementManager::getHexElevationKm(int tx, int ty, Matrix2D *mapPoints, int numPoints05) const { if (!mapPoints || numPoints05 <= 0) return 0.0f; QVector2D w = Pathfinder::hexToWorld(tx, ty); float scaleX = numPoints05 * 2.0f; float scaleY = numPoints05 / LOD_0_SIZE_Y; int mapX = (int) std::round(w.x() * scaleX); int mapY = (int) std::round(w.y() * scaleY); if (mapX < 0) mapX = 0; if (mapX >= (int)mapPoints->getWidth()) mapX = (int)mapPoints->getWidth() - 1; if (mapY < 0) mapY = 0; if (mapY >= (int)mapPoints->getHeight()) mapY = (int)mapPoints->getHeight() - 1; unsigned char h = mapPoints->operator()(mapX, mapY).height; return (h / 255.0f) * 3.5f; } float TerrainMovementManager::calculateStepCost( int fromX, int fromY, int toX, int toY, MovementDomain domain, Matrix2D *mapPoints, int numPoints05, const UnitConfig *unitConfig) const { // Check general hex passability of destination first if (!isHexPassable(toX, toY, domain, mapPoints, numPoints05, unitConfig)) { return -1.0f; } QString toTerrain = getTerrainType(toX, toY, mapPoints, numPoints05); float baseCost = getTerrainMovementCost(toTerrain); // Ships only move on flat water if (domain == MovementDomain::Naval) { return baseCost; } // For land units: compute elevation difference float hFrom = getHexElevationKm(fromX, fromY, mapPoints, numPoints05); float hTo = getHexElevationKm(toX, toY, mapPoints, numPoints05); float deltaH = hTo - hFrom; // in kilometers // Impassable cliff check if (deltaH > m_config.maxClimbableSlopeKm) { return -1.0f; } if (deltaH > 0.001f) { // Uphill penalty: going uphill increases movement cost float penalty = deltaH * m_config.uphillSlopeMultiplier; return baseCost + penalty; } else if (deltaH < -0.001f) { // Downhill bonus: going downhill decreases movement cost (negative penalty) float bonus = (-deltaH) * m_config.downhillSlopeBonusMultiplier; return std::max(m_config.minStepCost, baseCost - bonus); } return baseCost; }