// ai_tactical.cpp of Heroes of Might and Magic III Exp 2. // decompiled by void_17 in may 2023. // bool SoD_SpellExpert( SpellID spell, int ground_type ) { if ( ground_type == 7 ) return (akSpellTraits[spell].m_school & eSchoolFire) != 0; if ( ground_type >= 7 ) { if ( ground_type == 9 ) return (akSpellTraits[spell].m_school & eSchoolAir) != 0; if ( ground_type < 9 ) return (akSpellTraits[spell].m_school & eSchoolEarth) != 0; } else { if ( ground_type == 1 ) return 1; if ( ground_type >= 1 && ground_type >= 6 ) return (akSpellTraits[spell].m_school & eSchoolWater) != 0; } return 0; } // double value_of_luck_and_morale(int value, int change, double good_value_multiplier, double bad_value_multiplier) { double result; if ( change <= 0 ) { if ( value > -3 ) { if ( value + change < -3 ) change = -3 - value; if ( value > 0 ) { if ( -change > value ) result = -good_value_multiplier * static_cast(value) - bad_value_multiplier * static_cast(change + value); else result = good_value_multiplier * static_cast(change); } else { result = -bad_value_multiplier * static_cast(change); } } else { result = 0.0; } } else if ( value < 3 ) { if ( value + change > 3 ) change = 3 - value; if ( value < 0 ) { if ( change > value ) result = bad_value_multiplier * static_cast(value) + good_value_multiplier * static_cast(change + value); else result = -bad_value_multiplier * static_cast(change); } else { result = static_cast(change) * good_value_multiplier; } } else { result = 0.0; } return result; } double AI_value_of_morale( int value, int change ) { return value_of_luck_and_morale( value, change, 0.0173, -0.0833 ); } double AI_value_of_luck( int value, int change ) { return value_of_luck_and_morale(value, change, 0.0173, -0.0122); } int get_multi_head_bonus(int our_group, const army * our_army, int our_hex, int troop_count, const army * enemy, int enemy_hex, const type_AI_combat_parameters * estimate ) { const int directions = our_army->get_multi_head_directions( our_hex, enemy, enemy_hex ); int mask = 1 << enemy->index; int result = 0; int dmg = 0; army * adj_army = nullptr; army * adj = nullptr; for ( int i = 0; i < 8; ++i ) { if ( ((1 << i) & directions) != 0 ) { if ( combatManager::ValidHex( our_army->get_adjacent_hex(our_hex, i) ) ) { adj_army = gpCombatManager->cell[our_army->get_adjacent_hex(our_hex, i)].get_army(); if ( adj_army ) { if ( adj_army->group != our_group && (mask & (1 << adj_army->index)) == 0 ) { dmg = our_army->get_average_damage(adj_army, false, troop_count, true, 0); if ( estimate->simulated ) dmg = std::min(dmg, get_total_hit_points(adj_army, true)); result += adj_army->get_loss_combat_value(estimate->lowest_attack, estimate->lowest_defense, 0, dmg, false); mask |= 1 << adj_army->index; } } } } } return result; } int get_breath_bonus( int our_group, const army * our_army, int our_hex, int troop_count, const army * enemy, int enemy_hex, const type_AI_combat_parameters * estimate ) { int attack_direction = our_army->get_attack_direction( our_hex, enemy, enemy_hex ); int adj_hex = GetAdjacentCellIndex( our_army, our_army->get_adjacent_hex(our_hex, attack_direction), attack_direction ); if ( !combatManager::ValidHex(adj_hex) ) return 0; army * adj_army = gpCombatManager->cell[adj_hex].get_army(); if ( !adj_army || adj_army == enemy ) return 0; int dmg = our_army->get_average_damage( adj_army, false, troop_count, true, 0 ); if ( estimate->simulated ) { dmg = std::min(dmg, adj_army->get_total_hit_points(true)); } int result = adj_army->get_loss_combat_value(estimate->lowest_attack, estimate->lowest_defense, 0, dmg, estimate->kills_only); if ( adj_army->group == our_group ) result = -result; return result; } int AI_get_attack_damage( const army & current_army, int our_hits, const army & enemy, bool ranged, int distance) { return current_army.get_average_damage( enemy, ranged, (our_hits + current_army.sMonInfo.hitPoints - 1) / current_army.sMonInfo.hitPoints, true, distance ); } long type_AI_combat_parameters::get_enemy_group() const { return enemy_group; } long type_AI_combat_parameters::get_group() const { return our_group; } void type_AI_combat_parameters::simulate_single_attack( army const & current_army, long & our_hits, army const & enemy, long & enemy_hits, bool ranged, long distance ) const { long dmg = AI_get_attack_damage(current_army, our_hits, enemy, ranged, distance); if ( !ranged ) { long fireshield_dmg = gpCombatManager->compute_fire_shield_damage(dmg, current_army, enemy, enemy_hits); if ( fireshield_dmg > 0 ) { our_hits -= fireshield_dmg; if ( our_hits < 0 ) our_hits = 0; } } enemy_hits -= dmg; if ( enemy_hits < 0 ) enemy_hits = 0; } void type_AI_combat_parameters::simulate_attack( army const & current_army, long & our_hits, army const & enemy, long & enemy_hits, bool ranged, long distance ) const { bool can_shoot = ranged; if ( ranged ) can_shoot = current_army.can_shoot(); simulate_single_attack(current_army, our_hits, enemy, enemy_hits, can_shoot, distance); if ( our_hits && enemy_hits > 0 && !can_shoot ) { if ( !enemy.can_retaliate(current_army) || gpGame->sSetup.difficulty <= 0 && !gpCombatManager->IsHuman[get_group()] || (simulate_single_attack(enemy, enemy_hits, current_army, our_hits, false, 0), our_hits) && enemy_hits ) { if ( !can_shoot && (current_army.sMonInfo.attributes & CF_TWO_ATTACKS) != 0 ) simulate_single_attack(current_army, our_hits, enemy, enemy_hits, can_shoot, 0); } } } long type_AI_combat_parameters::get_simple_attack_effect( army const & current_army, long our_total, army const & enemy, long enemy_total, bool ranged, long distance) const { long dmg = 0; long enemy_dmg = 0; long our_hits = 0; long enemy_hits = 0; bool can_shoot = ranged; if ( ranged ) can_shoot = current_army.can_shoot(); if ( simulated ) { dmg = our_total - current_army.get_AI_expected_damage(); if ( dmg <= 0 ) return 0; enemy_dmg = enemy_total - enemy.get_AI_expected_damage(); if ( enemy_dmg <= 0 ) return 0; } else { enemy_dmg = enemy_total; dmg = our_total; } our_hits = dmg; enemy_hits = enemy_dmg; simulate_attack(current_army, our_hits, enemy, enemy_hits, ranged, distance); long result = enemy.get_loss_combat_value( this->lowest_attack, this->lowest_defense, ranged, enemy_dmg - enemy_hits, this->kills_only ); if ( dmg > our_hits ) result -= current_army.get_loss_combat_value( this->lowest_attack, this->lowest_defense, ranged, dmg - our_hits, false ); return result; } long type_AI_combat_parameters::get_simple_attack_effect( army const & current_army, army const & enemy, bool ranged, long distance ) const { long ourHP = current_army.get_total_hit_points(false); long enemyHP = enemy.get_total_hit_points(false); return get_simple_attack_effect(current_army, ourHP, enemy, enemyHP, ranged, distance); } long type_AI_combat_parameters::get_ranged_attack_value( army const & current_army, army const & enemy ) const { long result = get_simple_attack_effect(current_army, enemy, true, 0); if ( !gpGame->sSetup.difficulty && !gpCombatManager->IsHuman[get_group()] ) return result; if ( enemy.IsIncapacitated() ) return result / 10; if ( enemy.cannot_attack()) return result / 5; if ( !enemy.get_AI_target() ) return result / 5; if ( enemy.get_AI_target_time() > 5 ) return result / 5; return result / enemy.get_AI_target_time(); } long type_AI_combat_parameters::get_exchange_effect( army const & current_army, army const & enemy, long distance ) const { bool curr_can_shoot = current_army.can_shoot(); long curr_HP = current_army.get_total_hit_points(this->simulated); long enemy_HP = enemy.get_total_hit_points(this->simulated); our_hits = curr_HP; enemy_hits = enemy_HP; simulate_attack(current_army, our_hits, enemy, enemy_hits, curr_can_shoot, distance); if ( enemy_hits > 0 ) simulate_attack(enemy, enemy_hits, current_army, our_hits, curr_can_shoot, 0); long result = enemy.get_loss_combat_value( this->lowest_attack, this->lowest_defense, curr_can_shoot, enemy_HP - enemy_hits, kills_only && !simulated ); if ( curr_HP > our_hits ) result -= current_army.get_loss_combat_value( this->lowest_attack, this->lowest_defense, curr_can_shoot, curr_HP - our_hits, kills_only && !simulated ); return result; } type_AI_combat_parameters::type_AI_combat_parameters(combatManager const * combat, long side) : our_group(side), enemy_group(1 - side), lowest_attack(0), lowest_defense(0), kills_only(false), simulated(false) { static const double time_ratios[] = { 2.6, 1.9, 1.5, 1.31, 1.2, 1.13 }; long attack_modifier = 0; long defense_modifier = 0; for ( int g = 1; g >= 0 ; --g ) { for ( long i = 0; i < combat->numArmies[g]; ++i ) { if ( combat->Armies[g][i].numTroops > 0 && combat->Armies[g][i].armyType != CREATURE_AMMO_CART ) { attack_modifier = combat->Armies[g][i].get_attack_modifier(nullptr, combat->Armies[g][i].can_shoot()); defense_modifier = combat->Armies[g][i].get_defense_modifier(); if ( g || this->lowest_attack > attack_modifier ) this->lowest_attack = attack_modifier; if ( g || this->lowest_defense > defense_modifier ) this->lowest_defense = defense_modifier; } } } this->friendly_combat_value = combat->get_total_combat_value( this->our_group, this->lowest_attack, this->lowest_defense, true ); this->awake_combat_value = combat->get_total_combat_value( this->our_group, this->lowest_attack, this->lowest_defense, true ); this->enemy_combat_value = combat->get_total_combat_value( this->enemy_group, this->lowest_attack, this->lowest_defense, true ); this->awake_enemy_value = combat->get_total_combat_value( this->enemy_group, this->lowest_attack, this->lowest_defense, false ); if ( 2 * this->friendly_combat_value < this->enemy_combat_value && (gpGame->sSetup.difficulty > 0 || gpCombatManager->IsHuman[get_group()]) ) { this->kills_only = true; } long friendly_value = this->awake_friendly_value; long enemy_value = this->awake_enemy_value; if ( friendly_value < enemy_value ) { friendly_value = this->awake_enemy_value; enemy_value = this->awake_friendly_value; } if ( 5 * enemy_value > friendly_value && friendly_value ) { for ( this->rounds_left = 0; ; ++this->rounds_left ) { if ( rounds_left >= 6 || static_cast(friendly_value) / static_cast(enemy_value) >= time_ratios[rounds_left] ) break; } ++this->rounds_left; } else { this->rounds_left = 1; } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- type_AI_attack_hex_chooser::type_AI_attack_hex_chooser( army const * attack_army, army const * enemy_army, long const * enemy_attack_array, searchArray * search_data, type_AI_combat_parameters const & data) : estimate(data), attacker(enemy_army), speed(attack_army->GetSpeed()), enemy(enemy_army), best_value(0), best_attack_time(0), enemy_attacks(enemy_attack_array), search_array(search_data), best_hex(-1), { int total_HP;//local var if ( 0 > enemy_army->get_total_hit_points( data->simulated ) - AI_get_attack_damage( attack_army, attack_army->get_total_hit_points( data->simulated ), enemy_army, 0, 0) ) total_HP = enemy_army->sMonInfo.hitPoints; else total_HP = enemy_army->sMonInfo.hitPoints + enemy_army->get_total_hit_points( data->simulated ) - AI_get_attack_damage(attack_army,attack_army->get_total_hit_points( data->simulated ),enemy_army, 0, 0 ); retaliation_strength = (total_HP - 1) / enemy_army->sMonInfo.hitPoints; our_strength = (attack_army->get_total_hit_points( data->simulated ) - 1) / (attack_arm->sMonInfo.hitPoints); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_attack_hex_chooser::get_hex_attack_value( long const hex, long & checked ) { if ( !gpGame->sSetup.difficulty && !gpCombatManager->IsHuman[estimate->our_group] ) return 0; army * adj_army = nullptr; army * adj = nullptr; long delta = 0; long totalHP = 0; long mask = 0; long result = 0; for (int i = 6 ; i != 1 ; --i ) { if ( combatManager::ValidHex( gpCombatManager->AdjacentIndex[hex].adjacent[i] ) ) { adj_army = gpCombatManager->cell[gpCombatManager->AdjacentIndex[hex].adjacent[i]].get_army(); adj = adj_army; if ( adj_army ) { if ( adj_army->group != gpCombatManager->currControl && adj_army != this->attacker ) { mask = 1 << adj_army->index; if ( (checked & mask) == 0 ) { checked |= mask; totalHP = adj->get_total_hit_points(estimate->simulated); if ( totalHP ) { // (value1 - value2) delta = ((adj->get_unit_combat_value(this->estimate->lowest_attack, this->estimate->lowest_defense, true, this->attacker) - adj->get_unit_combat_value(this->estimate->lowest_attack, this->estimate->lowest_defense, false, nullptr)) * static_cast(totalHP) / static_cast(adj->sMonInfo.hitPoints)); if ( delta < 1 ) delta = 1; result += delta; } } } } } } return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_attack_hex_chooser::get_attack_time( pathCell const * cell )const { long result; if ( speed ) { result = (speed + cell->cost - 1) / speed; if ( search_array->visited_points[cell->getX()] ) ++result; if ( result < 1 ) return 1; } else if ( cell->cost ) { return 100; } else { return 1; } return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_attack_hex_chooser::check_adjacent_hexes( long enemy_hex, long start_direction, long stop_direction ) { long adj_enemy_hex = 0; long enemy_value = 0; long checked = 0; long attack_time = 0; long value = 0; long enemy_attack = 0; long direction = 0; const pathCell* cell = nullptr; for ( long i = start_direction; i < stop_direction; ++i ) { adj_enemy_hex = gpCombatManager->AdjacentIndex[enemy_hex].adjacent[i]; if ( combatManager::ValidHex(adj_enemy_hex) ) { cell = search_array->get_hex(adj_enemy_hex); if ( cell->combat_visited && (cell->combat_flight_cost == 0)) { attack_time = get_attack_time(cell); if ( this->best_hex < 0 || this->best_attack_time >= attack_time ) { checked = 0; value = get_hex_attack_value(adj_enemy_hex, checked); if ( gpGame->sSetup.difficulty > 0 || gpCombatManager->IsHuman[this->estimate->our_group] ) { if ( (this->attacker->sMonInfo.attributes & CF_MULTI_HEADED) != 0 ) value += get_multi_head_bonus( gpCombatManager->currControl, this->attacker, adj_enemy_hex, this->our_strength, this->enemy, this->enemy->gridIndex, this->estimate ); if ( (this->attacker->sMonInfo.attributes & CF_HAS_EXTENDED_ATTACK) != 0 ) value += get_breath_bonus( gpCombatManager->currControl, this->attacker, adj_enemy_hex, this->our_strength, this->enemy, this->enemy->gridIndex, this->estimate ); if ( this->enemy->can_retaliate(this->attacker) ) { if ( (this->enemy->sMonInfo.attributes & CF_MULTI_HEADED) != 0 ) value -= get_multi_head_bonus( this->enemy->group, this->enemy, this->enemy->gridIndex, this->retaliation_strength, this->attacker, adj_enemy_hex, this->estimate); if ( (this->enemy->sMonInfo.attributes & CF_HAS_EXTENDED_ATTACK) != 0 ) value -= get_multi_head_bonus( this->enemy->group, this->enemy, this->enemy->gridIndex, this->retaliation_strength, this->attacker, adj_enemy_hex, this->estimate); } } } enemy_attack = this->enemy_attacks[adj_enemy_hex]; if ( (this->attacker->sMonInfo.attributes & CF_DOUBLE_WIDE) != 0 ) { direction = attacker->OffsetToFront(-1); value += get_hex_attack_value(adj_enemy_hex + direction, checked); enemy_attack = std::min(enemy_attack, this->enemy_attacks[adj_enemy_hex + direction]); } enemy_value = value + enemy_attack; if ( this->best_hex >= 0 && attack_time == this->best_attack_time ) { if ( enemy_value < this->best_value ) continue; if ( enemy_value == this->best_value ) { long dcost = cell->cost - search_array->get_hex(adj_enemy_hex)->cost; if ( (this->attacker->armyType == CAVALIER || this->attacker->armyType == CHAMPION) && (gpGame->sSetup.difficulty > 0 || gpCombatManager->IsHuman[this->estimate->our_group]) ) { if ( dcost <= 0 ) continue; } else if ( dcost >= 0 ) { continue; } } } this->best_value = enemy_value; this->best_hex = adj_enemy_hex; this->best_attack_time = attack_time; } } } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- bool type_AI_attack_hex_chooser::find_attack_hex( ) { short adj = 0; this->best_value = 0; this->best_hex = -1; check_adjacent_hexes( enemy->gridIndex, 0, 6 ); if ( enemy->Is(CF_DOUBLE_WIDE) ) { check_adjacent_hexes( enemy->get_second_grid_index(), 0, 6 ); } if ( attacker->Is(CF_DOUBLE_WIDE) ) { long index = enemy->gridIndex; if ( enemy->Is(CF_DOUBLE_WIDE) && (attacker->OffsetToFront(-1) == enemy->OffsetToFront(-1)) ) index = enemy->get_second_grid_index(); if ( attacker->OffsetToFront(-1) >= 0 ) { adj = gpCombatManager->AdjacentIndex[gridIndex].adjacent[1]; if ( combatManager::ValidHex(adj) ) check_adjacent_hexes( adj, 0, 3 ); //change best_hex } else { adj = gpCombatManager->AdjacentIndex[gridIndex].adjacent[4]; if ( combatManager::ValidHex(adj) ) check_adjacent_hexes( adj, 3, 6 ); //change best_hex } } return combatManager::ValidHex(best_hex); } // AI spell cast during a battle type_enchant_data::type_enchant_data( SpellID new_spell, TSkillMastery new_mastery, int new_power, int new_duration ) { this->spell = new_spell; this->mastery = new_mastery; this->power = new_power; this->duration = new_duration; this->check_resistance = true; return this; } int type_enchant_data::get_mastery_value() { return akSpellTraits[this->spell].m_mastery_bonus[this->mastery]; } type_spell_choice::type_spell_choice() : type_enchant_data(SPL_NONE, eMasteryNone, 0, 0), value(0), target_hex(-1), second_target_hex(-1), cast_now(false) { } type_spell_choice::type_spell_choice( SpellID new_spell, TSkillMastery new_mastery, long new_power, long new_duration ) : type_enchant_data(new_spell, new_mastery, new_power, new_duration), value(0), target_hex(-1), second_target_hex(-1), cast_now(false) { } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::initialize( combatManager* combat, int side ) { this->our_group = side; this->enemy_group = 1 - side; this->current_hero = combat->Heroes[side]; this->enemy_hero = combat->Heroes[this->enemy_group]; this->win_likely = false; } type_AI_spellcaster::type_AI_spellcaster( combatManager* combat, int side, bool creature_spell ) : estimate(combat, side) { this->is_creature_spell = creature_spell; initialize(combat, side); combat->find_move_order(combat, nullptr); combat->simulate_combat(side, 0); check_simulation(); for ( i = 0; i < 2; ++i ) gpCombatManager->find_AI_targets(i, 0, false, &this->estimate, 0); find_enemy_attacks(); this->enemy_caster = new type_AI_spellcaster(this, combat, 1 - side, creature_spell); this->owns_enemy_caster = true; } //--------------------------------------------------------------------------- // used to initialize 'enemy_caster' member //--------------------------------------------------------------------------- type_AI_spellcaster::type_AI_spellcaster( type_AI_spellcaster * parent, combatManager *combat, long side, bool creature_spell ) : estimate(combat, side) { this->is_creature_spell = creature_spell; initialize(combat, side); this->enemy_caster = parent; this->owns_enemy_caster = false; check_simulation(); find_enemy_attacks(); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- bool type_AI_spellcaster::is_last_action() const { if ( gpCombatManager->numArmies[our_group] <= 0 ) return true; army * i_army = nullptr; for ( int i = 0; i < gpCombatManager->numArmies[our_group]; ++i ) { i_army = &gpCombatManager->Armies[our_group][i]; if ( !i_army->Is(CF_IMMOBILIZED|CF_SIEGE_WEAPON) ) { if ( !i_army->IsIncapacitated() && !i_army->Is(CF_DONE) && i_army != gpCombatManager->get_current_army() ) break; } } return false; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- bool type_AI_spellcaster::should_attack_now(army const * enemy) const { if ( this->estimate.kills_only ) return true; if ( is_last_action() ) return true; const army * curr = gpCombatManager->get_current_army(); if ( curr->group == this->our_group && curr->AI_target == enemy ) { if ( curr->get_AI_target_time() == 1 && !curr->can_shoot() && !curr->Is(CF_FREE_ATTACK) ) { return true; } } if ( (this->enemy_can_attack & (1 << enemy->index)) == 0 ) return false; army * i_army = nullptr; if ( gpCombatManager->numArmies[our_group] <= 0 ) return true; for ( int i = 0; i < gpCombatManager->numArmies[our_group]; ++i ) { i_army = &gpCombatManager->Armies[our_group][i]; if ( !i_army->Is(CF_IMMOBILIZED) ) { if ( !i_army->IsIncapacitated() && (i_army->armyType != CREATURE_AMMO_CART) && i_army != curr && i_army->expected_move_order > enemy->expected_move_order ) break; } } return false; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_damage_value( SpellID spell, long base_damage, hero const * target_hero, army const * target ) const { if ( target->Is(CF_IMMOBILIZED) || target->armyType == CREATURE_ARROW_TOWER ) return 0; long spell_dmg = gpCombatManager->ModifySpellDamage( base_damage, spell, this->current_hero, target_hero, target, false ); double chance = gpCombatManager->SpellCastWorkChance( spell, this->our_group, target, false, true, this->is_creature_spell ); if ( spell_dmg * chance <= 0 ) return 0; spell_dmg = target->get_loss_combat_value( this->estimate.lowest_attack, this->estimate.lowest_defense, target->can_shoot(), std::min(spell_dmg, target->get_total_hit_points()), this->estimate.kills_only ); if ( !target->cannot_attack() ) { long combat_value = target->get_total_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense); if ( combat_value > spell_dmg ) spell_dmg = 2 * spell_dmg - combat_value; } return spell_dmg; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_damage_spell_value( army const * enemy, const type_enchant_data & caster ) const { return get_damage_value( this, caster.spell, caster.power * akSpellTraits[caster.spell].m_power_factor + caster.get_mastery_value(), this->enemy_hero, enemy ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_group_damage_value( SpellID spell, long base_damage, long group, hero * target_hero ) const { long value = 0; int n = gpCombatManager->numArmies[group]; while(true) { --n; if ( !n ) break; value += get_damage_value( spell, base_damage, target_hero, &gpCombatManager->Armies[group][n] ); } return value; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_mass_damage_effect( long enemy_damage, long friendly_damage ) const { if ( enemy_damage <= 0 ) return 0; if ( friendly_damage < 0 ) friendly_damage = 0; if ( enemy_damage <= friendly_damage ) return 0; if ( static_cast(friendly_damage) / static_cast(this->estimate.friendly_combat_value) >= static_cast(enemy_damage) / static_cast(this->estimate.enemy_combat_value) ) return 0; if ( friendly_damage < this->estimate.friendly_combat_value ) return enemy_damage - friendly_damage; return 0; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_area_effect_value( SpellID spell, long base_damage, TSkillMastery mastery, long hex ) const { long friendly_damage = 0; long enemy_damage = 0; std::vector targets{}; gpCombatManager->mark_area_effect(spell, hex, mastery, targets); for ( auto it = targets.crbegin(); it != targets.crend(); ++it ) { if ( it->group == this->our_group ) friendly_damage += get_damage_value(spell, base_damage, this->current_hero, it); else enemy_damage += get_damage_value(spell, base_damage, this->enemy_hero, it); } return get_mass_damage_effect(enemy_damage, friendly_damage); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_area_effect( type_spell_choice & choice ) const { const long spell_value = choice.power * akSpellTraits[choice.spell].m_power_factor + choice.get_mastery_value(); long area_effect_value = 0; for ( int hex = 0; hex < 187; ++hex ) { if ( !combatManager::InInvisibleColumn(hex) ) { area_effect_value = get_area_effect_value( choice.spell, spell_value, choice.mastery, hex ); if ( area_effect_value > choice.value ) { choice.target_hex = hex; choice.value = area_effect_value; choice.cast_now = true; } } } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_chain_lightning_value( long power, TSkillMastery mastery, army* target ) const { static long max_affected[kNumMasteries] = {4, 4, 5, 5}; long friendly_damage = 0; long enemy_damage = 0; long affected = max_affected[mastery]; for ( int i = akSpellTraits[SPL_CHAIN_LIGHTNING].m_mastery_bonus[mastery] + power * akSpellTraits[SPL_CHAIN_LIGHTNING].m_power_factor; affected-- != 0; i /= 2 ) { if ( target->group == this->our_group ) friendly_value += get_damage_value(SPL_CHAIN_LIGHTNING, i, this->current_hero, target); else enemy_value += get_damage_value(SPL_CHAIN_LIGHTNING, i, this->enemy_hero, target); gpCombatManager->ArmyEffected[target->group][target->index] = true; long next_hex = gpCombatManager->GetNextChainLightningTarget(target, nullptr); if ( !combatManager::ValidHex(next_hex) ) break; target = gpCombatManager->cell[hex].get_army(); } return get_mass_damage_effect( enemy_value, friendly_value ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_chain_lightning(type_spell_choice & choice) const { army const * curr = nullptr; long chain_value = 0; for ( int i = 0; i < gpCombatManager->numArmies[1 - this->our_group]; ++i ) { curr = &gpCombatManager->Armies[1 - this->our_group][i]; if ( !curr->Is(CF_IMMOBILIZED) && gpCombatManager->ValidSpellTargetArmy( SPL_CHAIN_LIGHTNING, this->our_group, &gpCombatManager->Armies[1 - this->our_group][i], true, this->is_creature_spell ) ) { chain_value = get_chain_lightning_value(choice.power, choice.mastery, curr); if ( chain_value > choice.value ) { choice.value = chain_value; choice.target_hex = curr.gridIndex; choice.cast_now = true; } } } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_mass_damage(type_spell_choice & choice) const { long value = choice.power * akSpellTraits[choice.spell].m_power_factor + choice.get_mastery_value(); long enemy_value = get_group_damage_value(choice.spell, value, this->enemy_group, this->enemy_hero); long friendly_value = get_group_damage_value(choice.spell, value, this->our_group, this->current_hero); choice.value = get_mass_damage_effect( enemy_value, friendly_value ); choice.cast_now = true; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_age_value( army const * enemy, const type_enchant_data & caster ) const { if ( this->win_likely ) return 0; else return enemy->get_total_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense) / 3; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_attack_boost_value( army const * our_army, army const * enemy, long old_damage, long duration, double increase ) const { double factor = increase; long new_damage = old_damage * increase; long enemy_HP = enemy->get_total_hit_points(false); if ( new_damage > enemy_HP ) { new_damage = enemy_HP; factor = static_cast(enemy_HP) / static_cast(old_damage); } if ( new_damage <= old_damage ) return 0; return static_cast( get_duration(duration, our_army->Is(CF_DONE)) * sqrt(factor) - 1.0 * static_cast(our_army->get_total_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense)) ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_attack_boost_value( army const * our_army, army const * enemy, long duration, double increase ) const { return get_attack_boost_value( our_army, enemy, our_army->get_average_damage(enemy, our_army->can_shoot(), our_army->numTroops, true, 0), duration, increase ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_bless_value( army const * our_army, const type_enchant_data & caster ) const { if ( !our_army->AI_target ) return 0; if ( our_army->get_AI_target_time(our_army->GetSpeed()) > 1 ) return 0; get_attack_boost_value(our_army, our_army->AI_target, caster.duration, static_cast(our_army->sMonInfo.damageHighBound + akSpellTraits[SPL_BLESS].m_mastery_bonus[mastery]) / our_army->get_average_damage()); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_frenzy_value( army const * our_army, const type_enchant_data & caster ) const { if ( !our_army->AI_target ) return 0; if ( our_army->get_AI_target_time() > 1 ) return 0; long our_HP = our_army->get_total_hit_points(); long target_HP = our_army->AI_target->get_total_hit_points(); long damage1 = AI_get_attack_damage(our_army, our_HP, our_army->AI_target, our_army->can_shoot(), 0); estimate.simulate_attack( our_army, our_HP, our_army->AI_target, target_HP, our_army->can_shoot(), 0 ); if ( !our_HP ) return 0; long damage2 = AI_get_attack_damage(our_army, our_HP, our_army->AI_target, our_army->can_shoot(), 0); if ( our_army->can_shoot ) { if ( our_army->Is(CF_TWO_ATTACKS) ) damage2 /= 2; } get_attack_boost_value( our_army, our_army->AI_target, caster.duration, static_cast(damage2 + damage1) / static_cast(damage1) ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_attack_skill_value( army const * our_army, army const * enemy, long duration, long bonus ) const { if ( win_likely ) return 0; army temp(*our_army); // deep copy temp.sMonInfo.attackSkill += bonus; double our_incr = static_cast(our_army->SoD_ComputeDamageModifier(enemy, 100, our_army->can_shoot(), 0)); double temp_incr = static_cast(temp.SoD_ComputeDamageModifier(enemy, 100, our_army->can_shoot(), 0)); return get_attack_boost_value(our_army, enemy, duration, temp_incr / our_incr); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_blood_lust_value( army const * our_army, const type_enchant_data & caster ) const { if ( our_army->can_shoot() ) return 0; if ( our_army->get_AI_target() && our_army->get_AI_target_time() <= 1 ) return get_attack_skill_value( our_army, our_army->AI_target, caster.duration, akSpellTraits[SPL_BLOODLUST].m_mastery_bonus[caster.mastery] ); else return 0; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_mirth_value( army const * our_army, const type_enchant_data & caster ) const { if ( our_army->Is(CF_NO_MORALE) ) return 0; double morale_value = AI_value_of_morale(std::clamp(our_army->iMorale, -3, 3), akSpellTraits[SPL_MIRTH].m_mastery_bonus[caster.mastery]); if ( morale_value == 0.0 ) return 0; return morale_value * get_duration(caster.duration, our_army->Is(CF_DONE) ) * our_army->get_total_combat_value( this->estimate.lowest_attack, this->estimate.lowest_defense ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_sorrow_value( army const * enemy, const type_enchant_data & caster ) const { if ( enemy->Is(CF_NO_MORALE) ) return 0; if ( (enemy_can_attack & (1 << enemy->index)) == 0 ) return 0; if ( estimate.kills_only ) return 0; if ( win_likely ) return 0; double morale_value = -AI_value_of_morale(std::clamp(enemy->iMorale, -3, 3), -akSpellTraits[SPL_SORROW].m_mastery_bonus[caster.mastery]); if ( morale_value == 0.0 ) return 0; if ( caster.check_resistance ) morale_value *= gpCombatManager->SpellCastWorkChance( 50, this->our_group, enemy, false, true, this->is_creature_spell ); return morale_value * get_duration(caster.duration, enemy->Is(CF_DONE) ) * enemy->get_total_combat_value( this->estimate.lowest_attack, this->estimate.lowest_defense ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_fortune_value( army const * our_army, const type_enchant_data & caster ) const { if ( !our_army->AI_target ) return 0; if ( our_army->get_AI_target_time() > 1 ) return 0; long luck = our_army->iLuck; long bonus = akSpellTraits[SPL_FORTUNE].m_mastery_bonus[caster.mastery]; long avg_damage = our_army->get_average_damage( our_army->AI_target, our_army->can_shoot(), our_army->numTroops, false, 0 ); long result = 0; if ( luck >= 3 ) return 0; if ( luck + bonus <= -3 ) return 0; if ( luck + bonus > 3 ) bonus = 3 - luck; if ( luck < 0 ) { result = ( ( luck + bonus <= 0 ) ? bonus : -luck ) * get_attack_boost_value( our_army, our_army->AI_target, avg_damage / 2, caster.duration, 2.0 ) / 24; } if ( luck + bonus > 0 ) { result += ( ( luck >= 0 ) ? bonus : luck + bonus ) * get_attack_boost_value( our_army, our_army->AI_target, avg_damage, caster.duration, 2.0 ) / 24; } return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_defense_boost_value( army const* our_army, army const* enemy, long duration, double increase ) const { long dmg = enemy->get_average_damage(our_army, enemy->can_shoot(), enemy->numTroops, false, 0); long ratio = static_cast(static_cast(dmg) / increase); long our_HP = our_army->get_total_hit_points(); if ( dmg > our_HP ) { dmg = our_HP; increase = static_cast(our_HP) / static_cast(ratio); } if ( ratio >= dmg ) return 0; if ( win_likely && (our_army->residualDamage + our_army->AI_expected_damage) < our_army->sMonInfo.hitPoints ) { return 0; } if ( our_army->residualDamage + this->estimate.rounds_left * (this->melee_enemies[our_army->index].total_damage + this->ranged_enemies[our_army->index].total_damage) < our_army->sMonInfo.hitPoints ) return 0; return (std::sqrt(increase) - 1.0) * get_duration(duration, 0) * our_army->get_total_combat_value( this->estimate.lowest_attack, this->estimate.lowest_defense ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_defense_skill_value( army const* our_army, long duration, long bonus ) const { if ( !this->worst_enemies[our_army->index].enemy ) return 0; army temp(*our_army); // deep copy temp.sMonInfo.defenseSkill += bonus; double our_incr = static_cast(our_army->SoD_ComputeDamageModifier(enemy, 100, our_army->can_shoot(), 0)); double temp_incr = static_cast(temp.SoD_ComputeDamageModifier(enemy, 100, our_army->can_shoot(), 0)); return get_defense_boost_value(our_army, our_army->can_shoot(), duration, temp_incr / our_incr); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_disease_value( army const * enemy, const type_enchant_data & caster ) const { if ( (this->enemy_can_attack & (1 << enemy->index)) == 0 ) return 0; if ( this->estimate.kills_only ) return 0; long combat_value = enemy->get_total_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense); long result = caster.duration * 0.1 * combat_value; if ( caster.check_resistance ) { result *= gpCombatManager->SpellCastWorkChance( SPL_WEAKNESS, this->our_group, enemy, false, true, this->is_creature_spell ) ; } return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_prayer_value( army const * our_army, const type_enchant_data & caster ) const { long result = get_defense_skill_value(our_army, caster.duration, akSpellTraits[SPL_PRAYER].m_mastery_bonus[caster.mastery]) + get_speed_value(our_army, akSpellTraits[SPL_PRAYER].m_mastery_bonus[caster.mastery], caster.duration); if ( our_army->AI_target ) { if ( our_army->get_AI_target_time() == 1 ) result += get_attack_skill_value(our_army, our_army->AI_target, caster.duration, akSpellTraits[SPL_PRAYER].m_mastery_bonus[caster.mastery]); } return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_precision_value( army const * our_army, const type_enchant_data & caster ) const { if ( !our_army->can_shoot() ) return 0; if ( our_army->AI_target && our_army->get_AI_target_time() <= 1 ) return get_attack_skill_value( our_army, our_army->AI_target, caster.duration, akSpellTraits[SPL_PRECISION].m_mastery_bonus[caster.mastery] ); else return 0; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_air_shield_value( army const * our_army, const type_enchant_data & caster ) const { if ( this->ranged_enemies[our_army->index].enemy ) return get_defense_boost_value( our_army, this->ranged_enemies[our_army->index].enemy, caster.duration, static_cast(this->ranged_enemies[our_army->index].total_damage + this->melee_enemies[our_army->index].total_damage) / static_cast(this->melee_enemies[our_army->index].total_damage + this->ranged_enemies[our_army->index].total_damage * akSpellTraits[SPL_AIR_SHIELD].m_mastery_bonus[caster.mastery] / 100)); else return 0; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_shield_value( army const * our_army, const type_enchant_data & caster ) const { if ( this->ranged_enemies[our_army->index].enemy ) return get_defense_boost_value( our_army, this->ranged_enemies[our_army->index].enemy, caster.duration, static_cast(this->melee_enemies[index].total_damage + this->ranged_enemies[index].total_damage) / static_cast(this->ranged_enemies[index].total_damage + this->melee_enemies[index].total_damage * akSpellTraits[SPL_SHIELD].m_mastery_bonus[caster.mastery] / 100)); else return 0; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_slayer_value( army const * our_army, const type_enchant_data & caster ) const { if ( !our_army->AI_target ) return 0; if ( our_army->get_AI_target_time() > 1 ) return 0; if ( our_army->AI_target->Is(CF_KING_1) || our_army->AI_target->Is(CF_KING_2) && caster.mastery >= eMasteryAdvanced || our_army->AI_target->Is(CF_KING_3) && caster.mastery >= eMasteryExpert ) { return get_attack_skill_value( our_army, our_army->AI_target, caster.duration, akSpellTraits[SPL_SLAYER].m_mastery_bonus[caster.mastery] ); } else { return 0; } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_tough_skin_value( army const * our_army, const type_enchant_data & caster ) const { return get_defense_skill_value( our_army, caster.duration, akSpellTraits[SPL_STONE_SKIN].m_mastery_bonus[caster.mastery] ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_disruptive_ray_value( army const * enemy, const type_enchant_data & caster ) const { if ( this->estimate.kills_only ) return 0; army const * curr = &gpCombatManager->Armies[this->our_group][0]; long result = 0; long num = 0; long n = gpCombatManager->numArmies[this->our_group]; if ( n > 0 ) { do { if ( curr[num].AI_target == enemy ) break; ++num; ++curr; --n; } while ( n ); } if ( num == gpCombatManager->numArmies[this->our_group] ) return 0; result = (1.0 - sqrt(1.0 - akSpellTraits[SPL_DISRUPTING_RAY].m_mastery_bonus[caster.mastery] * 0.05)) * enemy->get_total_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense); if ( caster.check_resistance ) return result * gpCombatManager->SpellCastWorkChance( SPL_DISRUPTING_RAY, this->our_group, enemy, false, true, this->is_creature_spell ); else return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_weakness_value( army const * enemy, const type_enchant_data & caster ) const { if ( (this->enemy_can_attack & (1 << enemy->index)) == 0 ) return 0; if ( this->estimate.kills_only ) return 0; if ( !enemy->AI_target ) return 0; if ( enemy->get_AI_target_time() > 1 ) return 0; return get_attack_skill_value( enemy, enemy->AI_target, caster.duration, std::min(enemy->sMonInfo.attackSkill, akSpellTraits[SPL_WEAKNESS].m_mastery_bonus[caster.mastery]) ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_misfortune_value( army const * enemy, const type_enchant_data & caster ) const { if ( (this->enemy_can_attack & (1 << enemy->index)) == 0 ) return 0; if ( this->estimate.kills_only ) return 0; double luck_value = -AI_value_of_luck( std::clamp(enemy->iLuck, -3, 3), -akSpellTraits[SPL_MISFORTUNE].m_mastery_bonus[caster_mastery] ); if ( luck_value == 0.0 ) return 0; if ( caster.check_resistance ) luck_value *= gpCombatManager->SpellCastWorkChance( SPL_MISFORTUNE, this->our_group, enemy, false, true, this->is_creature_spell ); return luck_value * get_duration(caster.duration, enemy->Is(CF_DONE)) * enemy->get_total_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_blind_value( army const * enemy, const type_enchant_data & caster ) const { if ( (this->enemy_can_attack & (1 << enemy->index)) == 0 ) return 0; if ( this->win_likely ) return 0; long result = 0; long combat_value = enemy->get_total_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense); if ( combat_value < this->estimate.awake_enemy_value ) result = combat_value * get_duration(caster.duration, enemy->Is(CF_DONE)); else result = combat_value * (0.5 - std::sqrt(akSpellTraits[SPL_BLIND].m_mastery_bonus[caster.mastery] * 0.0025)); if ( caster.check_resistance ) result *= gpCombatManager->SpellCastWorkChance( SPL_BLIND, this->our_group, enemy, false, true, this->is_creature_spell ); return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_move_order_change_value( army const * our_army ) const { if ( !our_army->AI_target ) return 0; return estimate.get_exchange_effect(our_army, our_army->AI_target, 0) + estimate.get_exchange_effect(our_army->AI_target, our_army, 0); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_slow_value( army const * enemy, const type_enchant_data & caster ) const { if ( !enemy->AI_target ) return 0; if ( this->win_likely ) return 0; long attack_time = enemy->get_AI_target_time(); if ( attack_time > this->estimate.rounds_left ) return 0; if ( !(caster.duration - 1*(enemy->sMonInfo.attributes & CF_DONE) ) ) return 0; long current_speed = enemy->GetSpeed(); long speed_bonus = current_speed * akSpellTraits[SPL_SLOW].m_mastery_bonus[caster.mastery] / 100; if ( speed_bonus < 1 ) speed_bonus = 1; army const * curr = &gpCombatManager->Armies[this->our_group][0]; if ( attack_time == 1 ) { for ( int n = 0; n < gpCombatManager->numArmies[this->our_group]; ++n ) { if ( !curr->cannot_attack() && curr->GetSpeed() <= current_speed && curr->GetSpeed() > speed_bonus ) { long order_change_value = get_move_order_change_value(curr); if ( order_change_value > result ) result = order_change_value; } } } if ( !enemy->can_shoot() ) { current_speed = enemy->get_AI_target_time(speed_bonus) - attack_time; if ( current_speed > (caster.duration - 1*(enemy->sMonInfo.attributes & CF_DONE)) ) current_speed = caster.duration - 1*(enemy->sMonInfo.attributes & CF_DONE); if ( current_speed > 0 ) { long combat_value = enemy->get_total_combat_value( this->estimate.lowest_attack, this->estimate.lowest_defense ); result += combat_value * (this->estimate.rounds_left - attack_time + 1) / this->estimate.rounds_left - combat_value * (this->estimate.rounds_left - ( (attack_time + current_speed > this->estimate.rounds_left + 1) ? this->estimate.rounds_left + 1 : attack_time + current_speed) + 1) / this->estimate.rounds_left; } } if ( caster.check_resistance ) result *= gpCombatManager->SpellCastWorkChance( SPL_SLOW, this->our_group, enemy, false, true, this->is_creature_spell ); return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_poison_value( army const * enemy, const type_enchant_data & caster ) const { if ( this->win_likely ) return 0; else return enemy->get_loss_combat_value( this->estimate.lowest_attack, this->estimate.lowest_defense, enemy->can_shoot(), (enemy->sMonInfo.hitPoints - enemy->residualDamage) / 2, this->estimate.kills_only ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_speed_value( army const * our_army, long increase, long duration ) const { if ( !our_army->AI_target ) return 0; if ( this->win_likely ) return 0; if ( !(duration - 1*(enemy->sMonInfo.attributes & CF_DONE)) ) return 0; long result = 0; long current_speed = our_army->GetSpeed(); long new_speed = our_army->origSpeed + increase; long oldt = our_army->get_AI_target_time(current_speed); long newt = our_army->get_AI_target_time(new_speed); if ( newt > this->estimate.rounds_left ) return 0; if ( newt == 1 ) { if ( our_army->AI_target->GetSpeed() >= current_speed && our_army->AI_target->GetSpeed() < new_speed ) { result = get_move_order_change_value(our_army); if ( result < 0 ) result = 0; } } if ( newt < oldt ) { if ( oldt > this->estimate.rounds_left + 1 ) oldt = this->estimate.rounds_left + 1; long combat_value = our_army->get_total_combat_value( this->estimate.lowest_attack, this->estimate.lowest_defense ); result += combat_value * (this->estimate.rounds_left - newt + 1) / this->estimate.rounds_left - combat_value * (this->estimate.rounds_left - oldt + 1) / this->estimate.rounds_left; } return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_haste_value( army const * our_army, const type_enchant_data & caster ) const { return get_speed_value( our_army, caster.get_mastery_value(), caster.duration ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- inline long type_AI_spellcaster::get_protection_value( army const * our_army, TSpellSchool school, long level, long duration, long amount ) const { if ( !gpCombatManager->can_cast_spells(this->enemy_group, true) ) return 0; if ( this->win_likely ) return 0; if ( our_army->Is(CF_CLONE) ) return 0; long value = 0; for ( SpellID spell = K_FIRST_COMBAT_SPELL; spell <= K_LAST_COMBAT_SPELL; ++spell ) { const auto& traits = akSpellTraits[spell]; if ( (traits.m_school & school) != 0 ) { if ( (traits.m_flags & SF_DAMAGE_SPELL) != 0 && (traits.m_flags & SF_BATTLE_SPELL) != 0 && traits.m_level <= level && this->enemy_hero->available_spells[spell] && gpCombatManager->ValidSpellTargetArmy(spell, this->enemy_group, our_army, true, false) ) { long spell_level = this->enemy_hero->get_spell_level(spell, gpCombatManager->ground_type); if ( this->enemy_hero->GetManaCost(spell, gpCombatManager->ArmyGroups[this->enemy_group], gpCombatManager->ground_type) <= this->enemy_hero->mana ) { long dmg = gpCombatManager->ModifySpellDamage( akSpellTraits[spell].m_mastery_bonus[spell_level] + gpCombatManager->iSideSpellPower[this->enemy_group] * akSpellTraits[spell].m_power_factor, spell, this->current_hero, this->enemy_hero, our_army, false ); long HP = dmg; if ( dmg ) { if ( dmg > our_HP ) HP = our_HP; if ( dmg * amount / 100 < HP ) { long total_dmg = (HP - (dmg * amount / 100)) * gpCombatManager->SpellCastWorkChance( spell, this->enemy_group, our_army, false, true, false ); long loss_value = our_army->get_loss_combat_value( this->estimate.lowest_attack, this->estimate.lowest_defense, our_army->can_shoot, total_dmg, false); if ( loss_value > value ) value = loss_value; } } // if ( dmg ) } // if ( this->enemy_hero->GetManaCost(spell, gpCombatManager->ArmyGroups[this->enemy_group], gpCombatManager->ground_type) <= this->enemy_hero->mana ) } // if ( ... ) } // if ( (traits->m_school & school) != 0 ) } // ( SpellID spell = K_FIRST_COMBAT_SPELL; spell < K_LAST_COMBAT_SPELL; ++spell ) return value * get_duration(duration, our_army->Is(CF_DONE) ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_air_protection_value( army const * our_army, const type_enchant_data & caster ) const { return get_protection_value( our_army, eSchoolAir, 5, caster.duration, akSpellTraits[SPL_PROTECTION_FROM_AIR].m_mastery_bonus[caster.mastery] ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_fire_protection_value( army const * our_army, const type_enchant_data & caster ) const { return get_protection_value( our_army, eSchoolFire, 5, caster.duration, akSpellTraits[SPL_PROTECTION_FROM_FIRE].m_mastery_bonus[caster.mastery] ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_earth_protection_value( army const * our_army, const type_enchant_data & caster ) const { return get_protection_value( our_army, eSchoolEarth, 5, caster.duration, caster.get_mastery_value() ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_water_protection_value( army const * our_army, const type_enchant_data & caster ) const { return get_protection_value( our_army, eSchoolWater, 5, caster.duration, caster.get_mastery_value() ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- double type_AI_spellcaster::get_duration(long turns, bool moved_this_turn) const { double result = 0.0; double ratio = 1.0; if ( turns < this->estimate.rounds_left ) ratio = static_cast(turns) / static_cast(this->estimate.rounds_left); if ( !moved_this_turn || (ratio = ratio - 1.0 / static_cast(this->estimate.rounds_left), ratio >= 0.0) ) result = ratio; return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_cancel_value( army & current_army, bool bad_spells_only ) const { long result = 0; for ( SpellID spell = K_FIRST_COMBAT_SPELL; spell < SPL_COUNT; ++spl ) { int spell_time = current_army.get_spell_time(spell); if ( spell_time && (!bad_spells_only || akSpellTraits[spell].m_karma < 0) && (bad_spells_only || spell != SPL_POISON) ) { type_enchant_data caster(spell, current_army.spell_level[spell], spell_time, spell_time); current_army.CancelIndividualSpell(spell); if ( (((~akSpellTraits[spell].m_karma < 0) + 1) & 1) == (this->our_group == current_army.group) ) result += (enemy_caster->get_enchantment_function(spell))(target, caster); else result -= (this->get_enchantment_function(spell))(target, caster); } } return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_dispel_value( army const * our_army, const type_enchant_data & caster ) const { army current_army = *our_army; // deep copy return get_cancel_value(current_army, false); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_cure_value( army const * our_army, const type_enchant_data & caster ) const { army current_army = *our_army; // deep copy long cancel_value = get_cancel_value(current_army, true); long dmg = std::min(cancel_value, akSpellTraits[SPL_CURE].m_power_factor * caster.power + caster.get_mastery_value()); if ( this->win_likely && (our_army->residualDamage + our_army->AI_expected_damage) < our_army->sMonInfo.hitPoints ) { dmg = 0; } else { long combat_value = our_army->get_unit_combat_value( this->estimate.lowest_attack, this->estimate.lowest_defense, our_army->can_shoot(), nullptr ); return cancel_value + dmg * combat_value / our_army->sMonInfo.hitPoints } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_antimagic_value( army const * enemy, const type_enchant_data & caster ) const { army current_army = *our_army; // deep copy return get_cancel_value(current_army, false) + get_protection_value(&our_army, eSchoolAll, akSpellTraits[SPL_ANTI_MAGIC].m_mastery_bonus[caster_mastery], caster_duration, 0); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_backlash_value( army const * enemy, const type_enchant_data & caster ) const { army current_army = *our_army; // deep copy return get_protection_value(our_army, eSchoolAll, 5, caster.duration, 100 - 2 * caster.get_mastery_value()); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_counterstrike_value( army const * our_army, const type_enchant_data & caster ) const { long retaliations = 1; if ( our_army->armyType == GRIFFIN ) retaliations = 2; if ( our_army->armyType == ROYAL_GRIFFIN ) // Royal griffin has 5000 retaliations so this spell is useless return 0; if ( win_likely ) return 0; long bonus = akSpellTraits[SPL_COUNTERSTRIKE].m_mastery_bonus[caster.mastery]; bonus = std::min(bonus, this->melee_enemies[our_army->index].count - retaliations); if ( bonus <= 0 ) return 0; if ( !this->melee_enemies[our_army->index].enemy ) return 0; long delta = our_army->get_total_hit_points(false) - get_average_damage(this->melee_enemies[our_army->index].enemy, our_army, false, this->melee_enemies[our_army->index].enemy->numTroops, true, 0); if ( delta <= 0 ) return 0; long avg_dmg = get_average_damage( our_army, this->melee_enemies[our_army->index].enemy, false, our_army->numTroops, true, 0 ); long attack_dmg = AI_get_attack_damage( our_army, delta, this->melee_enemies[our_army->index].enemy, false, 0 ); if ( attack_dmg > this->melee_enemies[our_army->index].enemy->get_total_hit_points(false) ) attack_dmg = this->melee_enemies[our_army->index].enemy->get_total_hit_points(false); long dmg = avg_dmg + attack_dmg * retaliations; if ( our_army->Is(CF_TWO_ATTACKS) && !our_army->Is(CF_SHOOTING_ARMY) ) dmg += attack_dmg; return get_attack_boost_value( our_army, this->melee_enemies[our_army->index].enemy, caster.duration, static_cast(dmg + bonus * attack_damage) / static_cast(dmg)); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_fire_shield_value( army const * our_army, const type_enchant_data & caster ) const { if ( this->win_likely ) return 0; long coeff = caster.get_mastery_value(); if ( our_army->armyType == EFREET_SULTAN ) coeff -= 20; if ( coeff <= 0 ) return 0; if ( !this->melee_enemies[our_army->index].enemy || this->melee_enemies[our_army->index].enemy->Is(CF_IMMUNE_TO_FIRE_SPELLS) ) return 0; long dmg = this->melee_enemies[our_army->index].enemy->get_average_damage( our_army, false, this->melee_enemies[our_army->index].enemy->numTroops, true, 0 ) * coeff / 100; dmg = std::min(dmg, our_army->get_total_hit_points(false)); long avg_dmg = our_army->get_average_damage(this->melee_enemies[our_army->index].enemy, false, our_army->numTroops, true, 0); return get_attack_boost_value( our_army, this->melee_enemies[our_army->index].enemy, caster.duration, static_cast(avg_dmg + this->melee_enemies[our_army->index].count * dmg) / static_cast(avg_dmg)); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_traitor_value(army const * enemy, army const * target) const { if ( target->group == this->our_group ) return 0; long enemy_hits = enemy->get_total_hit_points(false); long enemy_HP = enemy_hits; long target_hits = target->get_total_hit_points(false); long target_HP = target_hits; this->estimate.simulate_attack(enemy, &enemy_hits, target, &target_hits, enemy->can_shoot(), 0); return enemy->get_loss_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense, enemy->can_shoot(), enemy_HP - enemy_hits, this->estimate.kills_only) + target->get_loss_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense, enemy->can_shoot(), target_HP - target_hits, this->estimate.kills_only); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_berserk_value( army const * enemy, const type_enchant_data & caster ) const { std::vector list; if ( this->win_likely || (enemy->get_berserk_targets(list), !list.size()) ) { return 0; } else { long value = 0; for ( size_t i = 0; i < list.size(); ++i ) value += get_traitor_value(enemy, list[i]); return value / list.size(); } return 0; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_hypnotize_value( army const * enemy, const type_enchant_data & caster ) const { if ( this->win_likely ) return 0; if ( enemy->cannot_attack() ) return 0; static const int durations[kNumMasteries] = {1,1,2,3}; int duration = durations[caster.mastery]; duration = std::min(duration, this->estimate.rounds_left); if ( enemy->Is(CF_DONE) ) --duration; if ( !duration ) return 0; gpSearchArray->SeedCombatPosition(enemy, this->our_group, enemy->sMonInfo.speed * duration, false, -1); long result = 0; for ( size_t i = 0; i < gpCombatManager->numArmies[this->enemy_group]; ++i ) { if ( &gpCombatManager->Armies[this->enemy_group][i] != enemy && !(gpCombatManager->Armies[this->enemy_group][i]->Is(CF_IMMOBILIZED)) && gpCombatManager->cell[gpCombatManager->Armies[this->enemy_group][i]->gridIndex].bValidMove ) { result = std::max(result, get_traitor_value(enemy, &gpCombatManager->Armies[this->enemy_group][i])); } } return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_single_enchantment(type_spell_choice & choice, long group) const { const auto ench_func = this->get_enchantment_function(spell); army const * curr = nullptr; army const * target = nullptr; long value = 0; for ( size_t i = 0; i < gpCombatManager->numArmies[group]; ++i ) { target = &gpCombatManager->Armies[group][i]; if ( !target->cannot_attack() && gpCombatManager->ValidSpellTargetArmy( choice.spell, this->our_group, &gpCombatManager->Armies[group][i], true, this->is_creature_spell ) ) { value = ench_func(target, choice); if ( target->spellInfluence[SPL_MAGIC_MIRROR] && group != this->our_group && value > 0 && choice.spell != SPL_DISPEL ) { value *= (100 - 2 * target->SoD_get_mirror_effect()) / 100; } if ( value > choice.value ) { choice.value = value; curr = target; choice.target_hex = target->gridIndex; } } } if ( curr ) { if ( group == this->our_group ) { if ( !(curr == &gpCombatManager->Armies[gpCombatManager->currArmyGroup][gpCombatManager->currArmyIndex] || is_last_action()) && (akSpellTraits[choice.spell].m_flags & SF_DEFENSIVE) == 0 ) choice.cast_now = false; else choice.cast_now = true; } else { choice.cast_now = should_attack_now(curr); } if ( choice.spell == SPL_HASTE ) choice.cast_now = true; } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_enchantment(type_spell_choice & choice, long group) const { army const * curr = nullptr; long value = 0; const auto ench_func = this->get_enchantment_function(spell); if ( SpellTargetsASingleArmy(choice.spell, choice.mastery) ) { consider_single_enchantment(choice, group); } else { for ( size_t i = 0; i < gpCombatManager->numArmies[group]; ++i ) { curr = &gpCombatManager->Armies[group][i]; if ( !curr->cannot_attack() && !curr->spellInfluence[choice.spell] && gpCombatManager->ValidSpellTargetArmy( choice.spell, this->our_group, &gpCombatManager->Armies[group][i], true, this->is_creature_spell ) ) { value += ench_func(target, choice); } } choice.cast_now = true; choice.value = value; } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_teleport( type_spell_choice & choice ) const { army const * curr = nullptr; bool b = false; long value = 0; for ( size_t i = 0; i < gpCombatManager->numArmies[group]; ++i ) { curr = &gpCombatManager->Armies[group][i]; if ( !curr->Is(CF_IMMOBILIZED) && !curr->Is(CF_DONE) && gpCombatManager->ValidSpellTargetArmy( SPL_TELEPORT, this->our_group, curr, true, this->is_creature_spell ) && !curr->can_shoot() ) { b = true; auto nonteleport_action = gpCombatManager->choose_melee_action(curr, false, false, this->our_group) value = gpCombatManager->choose_melee_action(curr, true, false, this->our_group) - nonteleport_action; if ( gpCombatManager->iNextAction == 6 && gpCombatManager->iNextActionGridIndex != curr->gridIndex && value > choice.value ) { choice.value = value; choice.target_hex = curr->gridIndex; choice.second_target_hex = gpCombatManager->iNextActionExtra; if ( curr != &gpCombatManager->Armies[gpCombatManager->currArmyGroup][gpCombatManager->currArmyIndex] ) { if ( !curr->IsIncapacitated() && !is_last_action() ) choice.cast_now = false; else choice.cast_now = true; } } } } if ( b ) { for ( size_t i = 0; i < 2; ++i ) gpCombatManager->find_AI_targets(i, nullptr, false, this->estimate, nullptr); } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_resurrect(type_spell_choice & choice) const { army const * curr = nullptr; army const * target = nullptr; long hex = 0; long value = 0; for ( size_t i = 0; i < gpCombatManager->numArmies[this->our_group]; ++i ) { curr = &gpCombatManager->Armies[this->our_group][i]; if ( gpCombatManager->ValidSpellTargetArmy( choice.spell, this->our_group, curr, true, this->is_creature_spell ) ) { hex = curr->gridIndex; target = choice.spell == SPL_ANIMATE_DEAD ? gpCombatManager->find_animate_dead_target(this->our_group, curr->gridIndex) : gpCombatManager->find_resurrection_target(this->our_group, curr->gridIndex, false); if ( target == curr || curr->Is(CF_DOUBLE_WIDE) && ((hex = curr->get_second_grid_index(), choice.spell != SPL_ANIMATE_DEAD) ? (target = gpCombatManager->find_resurrection_target(this->our_group, hex, false)) : (target = gpCombatManager->find_animate_dead_target(this->our_group, hex)), target == curr) ) { long num = (akSpellTraits[choice.spell].m_mastery_bonus[choice.mastery] + choice.power * akSpellTraits[choice.spell].m_power_factor) / curr->sMonInfo.hitPoints; long dNumTroops = curr->origNumTroops - curr->numTroops; if ( num <= dNumTroops || dNumTroops >= 3 * curr->origNumTroops / 4 || this->win_likely ) { num = std::min(num, dNumTroops); if ( num >= 1 && (choice.spell != SPL_RESURRECTION || choice.mastery >= eMasteryAdvanced || this->win_likely == false) ) { value = num * static_cast(curr->get_unit_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense, curr->can_shoot(), nullptr)); if ( this->estimate.awake_friendly_value > this->estimate.awake_enemy_value && this->estimate.rounds_left <= 1 ) { value *= 2; } if ( value > choice.value ) { choice.value = value; choice.target_hex = hex; if ( !(curr != &gpCombatManager->Armies[gpCombatManager->currArmyGroup][gpCombatManager->currArmyIndex] && this->win_likely == false) && !is_last_action() ) choice.cast_now = false; else choice.cast_now = true; } } } } } } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_sacrifice( type_spell_choice & choice, army const * healed_army, long target_hex ) const { army const * curr = nullptr; long value = 0; for ( size_t i = 0; i < gpCombatManager->numArmies[this->our_group]; ++i ) { curr = &gpCombatManager->Armies[this->our_group][i]; if ( !curr->Is(CF_IMMOBILIZED) && curr != healed_army && gpCombatManager->ValidSpellTargetArmy( choice->spell, this->our_group, curr, false, this->is_creature_spell) ) { long num = curr->numTroops * (choice.power + choice.get_mastery_value() + akCreatureTypeTraits[curr->armyType].hitPoints) / healed_army->sMonInfo.hitPoints; long dNumTroops = healed_army->origNumTroops - healed_army->numTroops; if ( num <= dNumTroops || dNumTroops >= 3 * healed_army->origNumTroops / 4 || this->win_likely ) { num = std::min(num, dNumTroops); if ( num >= 1 ) { value = num * static_cast(healed_army->get_unit_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense, curr->can_shoot(), false)); value -= curr->get_total_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense); if ( value > 0 ) { if ( this->estimate.awake_friendly_value > this->estimate.awake_enemy_value && this->estimate.rounds_left <= 1 ) { value *= 2; } if ( value > choice.value ) { choice.value = value; choice.target_hex = target_hex; choice.second_target_hex = curr->gridIndex; if ( !(healed_army != &gpCombatManager->Armies[gpCombatManager->currArmyGroup][gpCombatManager->currArmyIndex] && this->win_likely == false) && !is_last_action() ) choice.cast_now = false; else choice.cast_now = true; } } } } } } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_sacrifice(type_spell_choice & choice) const { army const * curr = nullptr; long hex = 0; for ( size_t i = 0; i < gpCombatManager->numArmies[this->our_group]; ++i ) { curr = &gpCombatManager->Armies[this->our_group][i]; if ( gpCombatManager->ValidSpellTargetArmy( choice->spell, this->our_group, curr, true, this->is_creature_spell) ) { hex = curr->gridIndex; if ( target == curr || curr->Is(CF_DOUBLE_WIDE) && ((hex = curr->get_second_grid_index(), choice.spell != SPL_ANIMATE_DEAD) ? (target = gpCombatManager->find_resurrection_target(this->our_group, hex, false)) : (target = gpCombatManager->find_animate_dead_target(this->our_group, hex)), target == curr) ) { consider_sacrifice(choice, curr, hex); } } } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_clone_value( army const * our_army, const type_enchant_data & caster ) const { if ( this->win_likely || our_army->Is(CF_DONE) ) return 0; if ( !our_army->AI_target ) return 0; if ( our_army->get_AI_target_time() > 1 ) return 0; return our_army->AI_target->get_loss_combat_value( this->estimate.lowest_attack, this->estimate.lowest_defense, our_army->AI_target->can_shoot(), our_army->get_average_damage(our_army->AI_target, our_army->can_shoot(), our_army->numTroops, true, 0), this->estimate.kills_only ); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_curse_value( army const * enemy, const type_enchant_data & caster ) const { if ( (this->enemy_can_attack & (1 << enemy->index)) == 0 ) return 0; if ( this->estimate.kills_only || this->win_likely ) return 0; long combat_value = enemy->get_total_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense); double damage = (enemy->sMonInfo.damageLowBound - akSpellTraits[SPL_CURSE].m_mastery_bonus[caster.mastery]); if ( damage < 1.0 ) damage = 1.0; long result = (static_cast(combat_value) - combat_value * std::sqrt(damage / enemy->get_average_damage())) * get_duration(this, caster.duration, enemy->Is(CF_DONE)); if ( caster.check_resistance ) { result *= gpCombatManager->SpellCastWorkChance( SPL_CURSE, this->our_group, enemy, false, true, this->is_creature_spell ); } else { return result; } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_forgetfulness_value( army const * enemy, const type_enchant_data & caster ) const { if ( !enemy->can_shoot() || enemy->Is(CF_CLONE) ) return 0; if ( this->estimate.kills_only || this->win_likely ) return 0; double ranged_value = enemy->get_unit_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense, true, nullptr); // delta = ranged_value - not_ranged_value double delta = ranged_value - enemy->get_unit_combat_value(this->estimate.lowest_attack, this->estimate.lowest_defense, false, nullptr); long result = (delta * static_cast(enemy->get_total_hit_points(false)) / static_cast(enemy->sMonInfo.hitPoints)) * get_duration(caster.duration, enemy->Is(CF_DONE)); if ( caster.check_resistance ) { result *= gpCombatManager->SpellCastWorkChance( SPL_FORGETFULNESS, this->our_group, enemy, false, true, this->is_creature_spell ); } return result; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::unimplemented( army const * target, const type_enchant_data & caster ) const { return 0; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- type_enchant_func type_AI_spellcaster::get_enchantment_function(SpellID spell) const { switch ( spell ) { case SPL_MAGIC_ARROW: case SPL_ICE_BOLT: case SPL_LIGHTNING_BOLT: case SPL_IMPLOSION: case SPL_TITANS_LIGHTNING_BOLT: return &get_damage_spell_value; break; case SPL_SHIELD: return &get_shield_value; break; case SPL_AIR_SHIELD: return &get_air_shield_value; break; case SPL_FIRE_SHIELD: return &get_fire_shield_value; break; case SPL_PROTECTION_FROM_AIR: return &get_air_protection_value; break; case SPL_PROTECTION_FROM_FIRE: return &get_fire_protection_value; break; case SPL_PROTECTION_FROM_WATER: return &get_water_protection_value; break; case SPL_PROTECTION_FROM_EARTH: return &get_earth_protection_value; break; case SPL_ANTI_MAGIC: return &get_antimagic_value; break; case SPL_DISPEL: return &get_dispel_value; break; case SPL_MAGIC_MIRROR: return &get_backlash_value; break; case SPL_CURE: return &get_cure_value; break; case SPL_BLESS: return &get_bless_value; break; case SPL_CURSE: return &get_curse_value; break; case SPL_BLOODLUST: return &get_blood_lust_value; break; case SPL_PRECISION: return &get_precision_value; break; case SPL_WEAKNESS: return &get_weakness_value; break; case SPL_STONE_SKIN: return &get_stone_skin_value; break; case SPL_DISRUPTING_RAY: return &get_disruptive_ray_value; break; case SPL_PRAYER: return &get_prayer_value; break; case SPL_MIRTH: return &get_mirth_value; break; case SPL_SORROW: return &get_sorrow_value; break; case SPL_FORTUNE: return &get_fortune_value; break; case SPL_MISFORTUNE: return &get_misfortune_value; break; case SPL_HASTE: return &get_haste_value; break; case SPL_SLOW: return &get_slow_value; break; case SPL_SLAYER: return &get_slayer_value; break; case SPL_FRENZY: return &get_frenzy_value; break; case SPL_COUNTERSTRIKE: return &get_counterstrike_value; break; case SPL_BERSERK: return &get_berserk_value; break; case SPL_HYPNOTIZE: return &get_hypnotize_value; break; case SPL_FORGETFULNESS: return &get_forgetfulness_value; break; case SPL_BLIND: case SPL_PARALYZE: return &get_blind_or_paralyze_value; break; case SPL_CLONE: return &get_clone_value; break; case SPL_POISON: return &get_poison_value; break; case SPL_DESEASE: return &get_disease_value; break; case SPL_AGE: return &get_age_value; break; default: return &unimplemented; break; } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_earthquake(type_spell_choice & choice) const { } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_summon(type_spell_choice & choice) const { if ( !win_likely && gpCombatManager->AbleToSummonElemental(choice.spell, this->our_group) ) { if ( this->estimate.kills_only ) choice.value = 1000 * choice.get_mastery_value(); else choice.value = choice.get_mastery_value() * choice.power * akCreatureTypeTraits[get_elemental_type(choice->spell)].AI_value; choice.cast_now = true; } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::consider_spell(type_spell_choice & choice) const { if ( choice.spell == SPL_DISPEL ) { consider_enchantment(choice, this->our_group); if ( choice.mastery == eMasteryAdvanced ) consider_enchantment(choice, this->our_group); if ( choice.mastery == eMasteryExpert ) { type_spell_choice enemy_effect = choice; consider_enchantment(enemy_effect, this->enemy_group); choice.value += enemy_effect.value; } } else if ( choice.spell >= SPL_DISPEL ) { if ( choice.spell == SPL_TELEPORT ) { consider_teleport(choice); return; } if ( choice.spell >= SPL_TELEPORT ) { if ( choice.spell < SPL_STONE_GAZE && choice.spell >= SPL_SUMMON_FIRE_ELEMENTAL ) { consider_summon(choice); return; } } else { if ( choice.spell == SPL_SACRIFICE ) { consider_sacrifice(choice); return; } if ( choice.spell < SPL_SACRIFICE && choice.spell >= SPL_RESURRECTION ) { consider_resurrect(choice); return; } } } else { if ( choice.spell == SPL_CHAIN_LIGHTNING ) { consider_chain_lightning(choice); return; } if ( choice.spell >= SPL_CHAIN_LIGHTNING ) { if ( choice.spell < SPL_SHIELD ) { if ( choice.spell >= SPL_DEATH_RIPPLE ) consider_mass_damage(choice); else consider_area_effect(choice); return; } } else if ( choice.spell == SPL_EARTHQUAKE ) { consider_earthquake(choice); return; } } if ( (akSpellTraits[choice.spell].m_flags & (CF_SIEGE_WEAPON|CF_CATAPULT|CF_ALIVE)) != 0 ) { if ( akSpellTraits[choice.spell].m_karma >= 0 ) consider_enchantment(choice, this->our_group); if ( akSpellTraits[choice.spell].m_karma <= 0 ) consider_enchantment(choice, this->enemy_group); } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::set_melee_enemies() { army const * curr = &gpCombatManager->Armies[this->our_group][0]; std::memset(this->melee_enemies, 0, sizeof(this->melee_enemies)); for ( int i = 0; i < gpCombatManager->numArmies[this->our_group]; ++i ) { if ( !curr[i].cannot_attack() ) { if ( curr[i].AI_target ) { if ( !curr[i].cannot_shoot() ) { if ( curr[i].get_AI_target_time() <= 1 ) { melee_enemies[i].enemy = curr[i].AI_target; melee_enemies[i].damage = curr[i].AI_target->get_average_damage( curr[i], false, curr[i].AI_target->numTroops, false, 0 ); melee_enemies[i].total_damage = melee_enemies[i].damage; melee_enemies[i].count = 1; } } } } } set_worst_enemies(); } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::set_worst_enemies() { for ( int i = 0; i < gpCombatManager->numArmies[this->our_group]; ++i ) { if ( ranged_enemies[i].damage > melee_enemies[i].damage ) { worst_enemies[i].enemy = ranged_enemies[i].enemy; worst_enemies[i].damage = ranged_enemies[i].damage; worst_enemies[i].count = ranged_enemies[i].count; worst_enemies[i].total_damage = ranged_enemies[i].total_damage; } else { worst_enemies[i].enemy = melee_enemies[i].enemy; worst_enemies[i].damage = melee_enemies[i].damage; worst_enemies[i].count = melee_enemies[i].count; worst_enemies[i].total_damage = melee_enemies[i].total_damage; } } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::add_enemy( type_AI_enemy_data & sum, army const * our_army, army const * enemy, bool ranged ) { long dmg = enemy->get_average_damage(our_army, ranged, enemy->numTroops, false, 0); ++sum->count; sum->total_damage += dmg; if ( dmg > sum->damage ) { sum->damage = dmg; sum->enemy = enemy; } this->enemy_can_attack |= 1 << enemy->index; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::find_enemy_attacks() { this->can_be_attacked = 0; this->enemy_can_attack = 0; std::memset(this->worst_enemies, 0, sizeof(this->worst_enemies)); std::memset(this->ranged_enemies, 0, sizeof(this->worst_enemies)); set_melee_enemies(); army const * curr_i = &gpCombatManager->Armies[this->our_group][0]; army const * curr_j = nullptr; for ( int i = 0; i < gpCombatManager->numArmies[this->our_group]; ++i ) { if ( !curr_i[i].Is(CF_IMMOBILIZED) && curr_i[i].armyType != CREATURE_ARROW_TOWER ) { curr_j = &gpCombatManager->Armies[this->our_group][0]; for ( int j = 0; j < gpCombatManager->numArmies[this->our_group]; ++j ) { if ( !curr_j[j].cannot_attack() && !curr_j[j].get_spell_time(SPL_HYPNOTIZE) && curr_j[j].armyType != CREATURE_ARROW_TOWER && curr_j[j].get_total_hit_points(true) ) { if ( curr_j[j].can_shoot() ) { add_enemy(this, this->ranged_enemies, curr_i[i], curr_j[j], true); } else if (curr_j[j] != this->melee_enemies[i].enemy && ((1 << i) & curr_j[j]->AI_possible_targets) != 0 ) { this->can_be_attacked |= 1 << j; add_enemy(this, this->melee_enemies, curr_i[i], curr_j[j], false); } } } } } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_ogre_mage_value( army const * target ) const { if ( target->spellInfluence[SPL_BLOODLUST] ) return 0; TSkillMastery mastery = eMasteryAdvanced; if ( SoD_SpellExpert(SPL_BLOODLUST, gpCombatManager->ground_type) ) mastery = eMasteryExpert; type_spell_choice choice(SPL_BLOODLUST, mastery, 6, 6); if ( SpellTargetsASingleArmy(SPL_BLOODLUST, mastery) ) return get_blood_lust_value(target, choice); consider_enchantment(choice, this->our_group); return choice.value; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- long type_AI_spellcaster::get_caliph_value( army const * target ) const { TSkillMastery mastery {}; // current mastery int n = 0; // number of valid spells int value = 0; for ( SpellID spell = K_FIRST_COMBAT_SPELL; spell <= K_LAST_COMBAT_SPELL; ++spell ) { if ( is_valid_caliph_spell(spell, target) ) { // same as "if SoD_SpellExpert() eMasteryExpert else eMasteryAdvanced" mastery = eMasteryAdvanced + SoD_SpellExpert(spell, gpCombatManager->ground_type); ++n; type_spell_choice caster(spell, mastery, 6, 6); if ( caster.valid() ) { if ( SpellTargetsASingleArmy(spell, mastery) ) { value += ( get_enchantment_function(spell) )(target, static_cast(caster)); } else { consider_enchantment(choice, this->our_group); value += choice.value; } } } } if ( n ) result = value / n; else result = 0; return result; } //--------------------------------------------------------------------------- // Added in AB //--------------------------------------------------------------------------- long type_AI_spellcaster::SoD_get_faerie_dragon_spell_value(long hex, long power, SpellID spell) { // same as "if SoD_SpellExpert() eMasteryExpert else eMasteryAdvanced" type_spell_choice caster(spell, eMasteryAdvanced + SoD_SpellExpert(spell, gpCombatManager->ground_type), power, power); army const * target = gpCombatManager->cell[hex].get_army(); long result = 0; if ( spell == SPL_CHAIN_LIGHTNING ) { if ( target ) { if ( !target->Is(CF_IMMOBILIZED) && gpCombatManager->ValidSpellTargetArmy( SPL_CHAIN_LIGHTNING, this->our_group, target, true, this->is_creature_spell ) ) { result = get_chain_lightning_value(caster.power, caster.mastery, target); } else { result = 0; } } else { result = 0; } } else { if ( spell >= SPL_CHAIN_LIGHTNING ) { if ( spell < SPL_DEATH_RIPPLE ) { return get_area_effect_value( caster.spell, caster.power * akSpellTraits[caster.spell].m_power_factor + caster.get_mastery_value(), caster.mastery, hex ); } return 0; } if ( spell < SPL_MAGIC_ARROW ) return 0; if ( target ) result = get_damage_spell_value( target, caster.spell, caster.mastery, caster.power, caster.duration, caster.check_resistance ); else result = 0; } } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- void type_AI_spellcaster::check_simulation() { int n = gpCombatManager->numArmies[this->enemy_group]; for ( army const * it = &gpCombatManager->Armies[this->enemy_group]; n-- > 0; ++it ) { if ( !it->Is(CF_IMMOBILIZED) && it->get_total_hit_points(it, true) > 0 && !it->Is(CF_SIEGE_WEAPON)) { this->win_likely = false; return; } } this->win_likely = true; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- bool type_AI_spellcaster::spells_not_required() const { if ( !this->win_likely ) return 0; army const * curr = gpCombatManager->Armies[this->our_group]; long num = gpCombatManager->numArmies[this->our_group]; if ( num <= 0 ) return true; while( curr->Is(CF_IMMOBILIZED) || curr->armyType == CREATURE_ARROW_TOWER || (curr->residualDamage + curr->AI_expected_damage) < curr->sMonInfo.hitPoints ) { ++curr; if ( !--num ) return true; } return false; } //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- bool type_AI_spellcaster::cast_spell( bool retreating ) { type_spell_choice best_choice{}; long spell_power = gpCombatManager->iSideSpellPower[this->our_group]; long spell_level = 0; long spell_duration = spell_power; const armyGroup * enemy_army_group = gpCombatManager->ArmyGroups[this->enemy_group]; bool recanters_cloak_used = enemy_hero->IsWieldingArtifact(RECANTERS_CLOAK) || current_hero->IsWieldingArtifact(RECANTERS_CLOAK); bool resurrection_only = spells_not_required(); if ( this->current_hero ) spell_duration = spell_power + this->current_hero->GetSpellDurationBonus(); if ( retreating ) this->estimate.kills_only = true; for ( SpellID spell = K_FIRST_COMBAT_SPELL; spell <= K_LAST_COMBAT_SPELL; ++spell ) { if ( this->current_hero->available_spells[spell] ) { if ( ( akSpellTraits[spell].m_flags & SF_BATTLE_SPELL) != 0 && (!retreating || (akSpellTraits[spell].m_flags & SF_DAMAGE_SPELL) != 0) && (gpCombatManager->ground_type != 2 || akSpellTraits[spell].m_level <= 1) && (!recanters_cloak_used || akSpellTraits[spell].m_level <= 2) ) { spell_level = this->current_hero->get_spell_level(spell, gpCombatManager->ground_type); spell_cost = this->current_hero->GetManaCost(this->current_hero, spell, enemy_army_group, gpCombatManager->ground_type); if ( spell_cost <= this->current_hero->mana && (!resurrection_only || spell == SPL_RESURRECTION || spell == SPL_ANIMATE_DEAD) ) { type_spell_choice choice(spell, spell_level, spell_power, spell_duration); consider_spell(choice); if ( choice.value > 0 ) { if ( this->current_hero->mana < 7 * spell_cost ) { choice.value *= std::sqrt(this->current_hero->mana / spell_cost); } else { choice.value = 5 * choice.value / 2; } choice.value *= Random(75, 100) / 100; if ( choice.value > best_choice.value ) best_choice = choice; } } } } } if ( best_choice.spell == SPL_NONE || !best_choice.cast_now && !retreating ) return false; gpCombatManager->iNextAction = 1; gpCombatManager->iNextActionExtra = best_choice.spell; gpCombatManager->iNextActionGridIndex = best_choice.target_hex; gpCombatManager->iNextActionGridIndex2 = best_choice.second_target_hex; return true; }