diff --git a/src/ai/ai.cpp b/src/ai/ai.cpp index 37a9fbefea..49d35dbbc5 100644 --- a/src/ai/ai.cpp +++ b/src/ai/ai.cpp @@ -500,25 +500,133 @@ void ShipAI::runOrders() } } +static glm::vec2 calculateInterceptPosition(glm::vec2 target_position, glm::vec2 target_velocity, float interceptor_speed) +{ + // Calculate the position vector of the point of interception. + // If interception is not possible, return the target position. + const float target_distance = glm::length(target_position); + const float target_speed = glm::length(target_velocity); + const float a = target_speed*target_speed - interceptor_speed*interceptor_speed; + const float b = 2.0f * glm::dot(target_position, target_velocity); + const float c = target_distance * target_distance; + const float det = b*b - 4.0f*a*c; + float intercept_time; + if (det > 0.0f) + intercept_time = (-b - glm::sqrt(det)) / 2.0f / a; + else + intercept_time = 0.0f; + return target_position + target_velocity*intercept_time; +} + void ShipAI::runAttack(P target) { - float attack_distance = 4000.0; + const glm::vec2 target_position = target->getPosition() - owner->getPosition(); + const glm::vec2 target_velocity = target->getVelocity(); + const float target_distance = glm::length(target_position); + const float target_heading = vec2ToAngle(target_position); + + float attack_distance = 4000.0f; if (has_missiles && best_missile_type == MW_HVLI) - attack_distance = 2500.0; + attack_distance = 2500.0f; if (has_beams) attack_distance = beam_weapon_range * 0.7f; - auto position_diff = target->getPosition() - owner->getPosition(); - float distance = glm::length(position_diff); + float attack_distance_tolerance; + if (owner->turn_speed > 0.0f && owner->impulse_max_speed > 0.0f) + attack_distance_tolerance = owner->impulse_max_speed / glm::radians(owner->turn_speed); // Match ship turn radius + else + attack_distance_tolerance = 1000.0f; // Null and divide-by-zero error protection + + float weapon_angle; + if ((weapon_direction == EWeaponDirection::Side && angleDifference(target_heading, owner->getRotation()) >= 0.0f) || weapon_direction == EWeaponDirection::Left) + weapon_angle = -90.0f; + else if ((weapon_direction == EWeaponDirection::Side && angleDifference(target_heading, owner->getRotation()) < 0.0f) || weapon_direction == EWeaponDirection::Right) + weapon_angle = 90.0f; + else + weapon_angle = 0.0f; + + // WARNING: + // Special logic to detect when we should aim specifically for HVLI. + // Not robust to changes in HVLI missile! + + // HVLI angle tolerance of +-30deg means that no matter the placement of + // the weapon tube, one of the cardinal directions will always be inside + // the tolerance arc. + const float hvli_tolerance = 30.0f; + const float hvli_min_distance = 1000.0f; // Min distance for full damage + bool aim_for_hvli = false; + int hvli_tube_index; + float hvli_speed; + for (int n=0; n < owner->weapon_tube_count && aim_for_hvli == false; n++) + { + WeaponTube &tube = owner->weapon_tube[n]; + const float tube_angle = owner->getRotation() + tube.getDirection(); + if ( + tube.getLoadType() == MW_HVLI && + ( + ( + tube.isLoaded() && + std::abs(angleDifference(target_heading, tube_angle)) < hvli_tolerance && + target_distance < attack_distance + attack_distance_tolerance && + target_distance > hvli_min_distance + ) || + tube.isFiring() + ) + ) + { + aim_for_hvli = true; + hvli_tube_index = n; + hvli_speed = MissileWeaponData::getDataFor(MW_HVLI).speed / MissileWeaponData::convertSizeToCategoryModifier(owner->weapon_tube[hvli_tube_index].getSize()); + weapon_angle = tube.getDirection(); + } + } + + // Maneuver orders + if (owner->getOrder() == AI_StandGround) + { + // Aim weapons at target + if (aim_for_hvli == true) + owner->target_rotation = vec2ToAngle(calculateInterceptPosition(target_position, target_velocity, hvli_speed)) - weapon_angle; + else + owner->target_rotation = target_heading - weapon_angle; + } + else if (std::abs(target_distance - attack_distance) < attack_distance_tolerance || aim_for_hvli == true) + { + // Aim weapons at target + if (aim_for_hvli == true) + owner->target_rotation = vec2ToAngle(calculateInterceptPosition(target_position, target_velocity, hvli_speed)) - weapon_angle; + else + owner->target_rotation = target_heading - weapon_angle; + + // Maintain distance from target + const float normalized_target_distance = (target_distance - attack_distance) / attack_distance_tolerance; + if (weapon_direction == EWeaponDirection::Front) + owner->impulse_request = normalized_target_distance; + else if (weapon_direction == EWeaponDirection::Rear) + owner->impulse_request = -normalized_target_distance; + else + owner->impulse_request = 1.0f; + } + else + { + // Move to attack position + // For side firing, fly into an orbit around the target at attack + // distance, while pointing weapons at the target. Otherwise, plot an + // intercept course. + if (weapon_direction == EWeaponDirection::Side || weapon_direction == EWeaponDirection::Left || weapon_direction == EWeaponDirection::Right) + flyTowards(target->getPosition() - attack_distance*target_position/target_distance + vec2FromAngle(target_heading - weapon_angle)*attack_distance_tolerance, 0.0f); + else + flyTowards(calculateInterceptPosition(target->getPosition(), target_velocity, owner->impulse_max_speed), attack_distance); + } - // missile attack - if (distance < 4500 && has_missiles) + // Fire missiles if there is a valid firing solution + if (target_distance < 4500.0f && has_missiles) { for(int n=0; nweapon_tube_count; n++) { if (owner->weapon_tube[n].isLoaded() && missile_fire_delay <= 0.0f) { - float target_angle = calculateFiringSolution(target, n); + const float target_angle = calculateFiringSolution(target, n); if (target_angle != std::numeric_limits::infinity()) { owner->weapon_tube[n].fire(target_angle); @@ -527,27 +635,6 @@ void ShipAI::runAttack(P target) } } } - - if (owner->getOrder() == AI_StandGround) - { - owner->target_rotation = vec2ToAngle(position_diff); - }else{ - if (weapon_direction == EWeaponDirection::Side || weapon_direction == EWeaponDirection::Left || weapon_direction == EWeaponDirection::Right) - { - //We have side beams, find out where we want to attack from. - auto target_position = target->getPosition(); - auto diff = target_position - owner->getPosition(); - float angle = vec2ToAngle(diff); - if ((weapon_direction == EWeaponDirection::Side && angleDifference(angle, owner->getRotation()) > 0) || weapon_direction == EWeaponDirection::Left) - angle += 160; - else - angle -= 160; - target_position += vec2FromAngle(angle) * (attack_distance + target->getRadius()); - flyTowards(target_position, 0); - }else{ - flyTowards(target->getPosition(), attack_distance); - } - } } void ShipAI::flyTowards(glm::vec2 target, float keep_distance) @@ -768,28 +855,6 @@ float ShipAI::calculateFiringSolution(P target, int tube_index) } } - if (type == MW_HVLI) //Custom HVLI targeting for AI, as the calculate firing solution - { - const MissileWeaponData& data = MissileWeaponData::getDataFor(type); - - auto target_position = target->getPosition(); - float target_angle = vec2ToAngle(target_position - owner->getPosition()); - float fire_angle = owner->getRotation() + owner->weapon_tube[tube_index].getDirection(); - - //HVLI missiles do not home or turn. So use a different targeting mechanism. - float angle_diff = angleDifference(target_angle, fire_angle); - - //Target is moving. Estimate where he will be when the missile hits. - float fly_time = target_distance / data.speed; - target_position += target->getVelocity() * fly_time; - - //If our "error" of hitting is less then double the radius of the target, fire. - if (std::abs(angle_diff) < 80.0f && target_distance * glm::degrees(tanf(fabs(angle_diff))) < target->getRadius() * 2.0f) - return fire_angle; - - return std::numeric_limits::infinity(); - } - if (type == MW_Nuke || type == MW_EMP) { auto target_position = target->getPosition(); @@ -849,5 +914,4 @@ P ShipAI::findBestMissileRestockTarget(glm::vec2 position, float ra } } return target; -} - +} \ No newline at end of file diff --git a/src/ai/fighterAI.cpp b/src/ai/fighterAI.cpp index 893d19d5b5..87a04a80d8 100644 --- a/src/ai/fighterAI.cpp +++ b/src/ai/fighterAI.cpp @@ -52,7 +52,7 @@ void FighterAI::runAttack(P target) { if (owner->weapon_tube[n].isLoaded() && missile_fire_delay <= 0.0f) { - float target_angle = calculateFiringSolution(target, owner->weapon_tube[n].getLoadType()); + float target_angle = calculateFiringSolution(target, n); if (target_angle != std::numeric_limits::infinity()) { owner->weapon_tube[n].fire(target_angle); diff --git a/src/spaceObjects/spaceshipParts/weaponTube.cpp b/src/spaceObjects/spaceshipParts/weaponTube.cpp index 02c0ba2db3..056fa34bf3 100644 --- a/src/spaceObjects/spaceshipParts/weaponTube.cpp +++ b/src/spaceObjects/spaceshipParts/weaponTube.cpp @@ -327,17 +327,16 @@ static float calculateTurnAngle(glm::vec2 aim_position, float turn_direction, fl float WeaponTube::calculateFiringSolution(P target) { - const MissileWeaponData& missile = MissileWeaponData::getDataFor(type_loaded); float missile_angle; - if (!target || missile.turnrate == 0.0f) + if (!target) { missile_angle = std::numeric_limits::infinity(); } else { + const MissileWeaponData& missile = MissileWeaponData::getDataFor(type_loaded); + const float missile_speed = missile.speed / MissileWeaponData::convertSizeToCategoryModifier(size); // Adjust for missile size const float tube_angle = parent->getRotation() + direction; // Degrees - const float turn_rate = glm::radians(missile.turnrate); - const float turn_radius = missile.speed / turn_rate; // Get target parameters in the tube centered reference frame: // X axis pointing in direction of fire @@ -345,63 +344,94 @@ float WeaponTube::calculateFiringSolution(P target) const glm::vec2 target_position = rotateVec2(target->getPosition() - parent->getPosition(), -tube_angle); const glm::vec2 target_velocity = rotateVec2(target->getVelocity(), -tube_angle); - const int MAX_ITER = 10; - const float tolerance = 0.1f * target->getRadius(); - bool converged = false; - glm::vec2 aim_position = target_position; // Set initial aim point - float turn_direction; // Left: -1, Right: +1, No turn: 0 - float turn_angle; // In radians. Value of 0 means no turn. - for (int iterations=0; iterations= turn_radius && (aim_position.y < 0.0f || d_right < turn_radius)) - { - turn_direction = -1.0f; - turn_angle = calculateTurnAngle(aim_position, turn_direction, turn_radius); - } - else if (d_right >= turn_radius && (aim_position.y >= 0.0f || d_left < turn_radius)) - { - turn_direction = 1.0f; - turn_angle = calculateTurnAngle(aim_position, turn_direction, turn_radius); - } - else - { - turn_direction = 0.0f; - turn_angle = 0.0f; - } - - // Calculate missile and target parameters at turn exit - const float exit_time = turn_angle / turn_rate; - const glm::vec2 missile_position_exit = turn_radius * glm::vec2(glm::sin(turn_angle), turn_direction * (1.0f - glm::cos(turn_angle))); - const glm::vec2 missile_velocity = missile.speed * glm::vec2(glm::cos(turn_angle), turn_direction * glm::sin(turn_angle)); - const glm::vec2 target_position_exit = glm::vec2(target_position + target_velocity*exit_time); - - // Calculate nearest approach - const glm::vec2 relative_position_exit = target_position_exit - missile_position_exit; + // For missiles that cannot turn (e.g. HVLI), check if the missile + // will intercept the target within the specified distance + // tolerance. + const glm::vec2 missile_velocity = missile_speed * glm::vec2(1.0f, 0.0f); + const float tolerance = target->getRadius(); const glm::vec2 relative_velocity = target_velocity - missile_velocity; const float relative_speed = glm::length(relative_velocity); - float nearest_time; // Time after turn exit when nearest approach occurs + float intercept_time; if (relative_speed == 0.0f) - nearest_time = 0.0f; + intercept_time = 0.0f; else - nearest_time = -glm::dot(relative_position_exit, relative_velocity) / relative_speed / relative_speed; - const float nearest_distance = glm::length(relative_position_exit + relative_velocity*nearest_time); + intercept_time = -glm::dot(target_position, relative_velocity) / relative_speed / relative_speed; + const float intercept_error = glm::length(target_position + relative_velocity*intercept_time); - // Check if solution has converged or if we must adjust aim - if (nearest_distance < tolerance && nearest_time >= 0.0f) - converged = true; + if (intercept_error < tolerance && intercept_time >= 0.0f) + missile_angle = tube_angle; else - aim_position = target_position + target_velocity*(exit_time + nearest_time); + missile_angle = std::numeric_limits::infinity(); } - if (converged == true && turn_angle < float(M_PI)) - missile_angle = tube_angle + glm::degrees(turn_direction*turn_angle); else - missile_angle = std::numeric_limits::infinity(); + { + // For missiles that can turn (e.g. homing missiles), calculate the + // turn angle required to intercept the target within the specified + // distance tolerance. + const float turn_rate = glm::radians(missile.turnrate); + const float turn_radius = missile_speed / turn_rate; + const int MAX_ITER = 10; + const float tolerance = 0.1f * target->getRadius(); + bool converged = false; + glm::vec2 aim_position = target_position; // Set initial aim point + float turn_direction; // Left: -1, Right: +1, No turn: 0 + float turn_angle; // In radians. Value of 0 means no turn. + float intercept_time; + for (int iterations=0; iterations= turn_radius && (aim_position.y < 0.0f || d_right < turn_radius)) + { + turn_direction = -1.0f; + turn_angle = calculateTurnAngle(aim_position, turn_direction, turn_radius); + } + else if (d_right >= turn_radius && (aim_position.y >= 0.0f || d_left < turn_radius)) + { + turn_direction = 1.0f; + turn_angle = calculateTurnAngle(aim_position, turn_direction, turn_radius); + } + else + { + turn_direction = 0.0f; + turn_angle = 0.0f; + } + + // Calculate missile and target parameters at turn exit + const float exit_time = turn_angle / turn_rate; + const glm::vec2 missile_position_exit = turn_radius * glm::vec2(glm::sin(turn_angle), turn_direction * (1.0f - glm::cos(turn_angle))); + const glm::vec2 missile_velocity = missile_speed * glm::vec2(glm::cos(turn_angle), turn_direction * glm::sin(turn_angle)); + const glm::vec2 target_position_exit = glm::vec2(target_position + target_velocity*exit_time); + + // Calculate nearest approach + const glm::vec2 relative_position_exit = target_position_exit - missile_position_exit; + const glm::vec2 relative_velocity = target_velocity - missile_velocity; + const float relative_speed = glm::length(relative_velocity); + float nearest_time; // Time after turn exit when nearest approach occurs + if (relative_speed == 0.0f) + nearest_time = 0.0f; + else + nearest_time = -glm::dot(relative_position_exit, relative_velocity) / relative_speed / relative_speed; + const float nearest_distance = glm::length(relative_position_exit + relative_velocity*nearest_time); + + // Check if solution has converged or if we must adjust aim + intercept_time = exit_time + nearest_time; + if (nearest_distance < tolerance && intercept_time > exit_time) + converged = true; + else + aim_position = target_position + target_velocity*intercept_time; + } + if (converged == true && turn_angle < float(M_PI) && missile.lifetime > intercept_time) + missile_angle = tube_angle + glm::degrees(turn_direction*turn_angle); + else + missile_angle = std::numeric_limits::infinity(); + } } return missile_angle; }