Files
Ascent-of-Man/terrainmovementmanager.cpp
T

304 lines
10 KiB
C++

#include "terrainmovementmanager.h"
#include "unitconfig.h"
#include "pathfinder.h"
#include "yamlparser.h"
#include <QDebug>
#include <cmath>
#include <algorithm>
TerrainMovementConfig::TerrainMovementConfig()
: waterHeightThreshold(6),
mountainRockHeightKm(2.3f),
snowHeightKm(3.1f),
polarSnowLineEnabled(true),
polarSnowLineBaseKm(2.5f),
polarSnowFactorStart(0.40f),
polarSnowFactorEnd(0.85f),
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("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<TerrainPoint> *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 + 1.0f;
// 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";
}
}
}
// 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<TerrainPoint> *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<TerrainPoint> *mapPoints, int numPoints05) const
{
QString tType = getTerrainType(tx, ty, mapPoints, numPoints05);
return getTerrainMovementCost(tType);
}
float TerrainMovementManager::getHexElevationKm(int tx, int ty, Matrix2D<TerrainPoint> *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<TerrainPoint> *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;
}