diff --git a/src/asm/heroes2_imports.inc b/src/asm/heroes2_imports.inc index 97cc3b2c9..c207c6823 100644 --- a/src/asm/heroes2_imports.inc +++ b/src/asm/heroes2_imports.inc @@ -291,6 +291,10 @@ IMPORT_?CheckWin@combatManager@@QAEHPAUtag_message@@@Z = 1 IMPORT_?DoHydraAttack@army@@QAEXH@Z = 1 IMPORT_?ProcessNextAction@combatManager@@QAEHAAUtag_message@@@Z = 1 ?ProcessNextAction@combatManager@@QAEHAAUtag_message@@@Z_clone EQU ?ProcessNextAction_orig@combatManager@@QAEHAAUtag_message@@@Z +IMPORT_?RawEffectSpellInfluence@combatManager@@QAEHPAVarmy@@H@Z = 1 +?RawEffectSpellInfluence@combatManager@@QAEHPAVarmy@@H@Z_clone EQU ?RawEffectSpellInfluence_orig@combatManager@@QAEHPAVarmy@@H@Z +IMPORT_?DetermineEffectOfSpell@combatManager@@QAEXHPAH0@Z = 1 +?DetermineEffectOfSpell@combatManager@@QAEXHPAH0@Z_clone EQU ?DetermineEffectOfSpell_orig@combatManager@@QAEXHPAH0@Z ;;heroes IMPORT_?cHeroTypeInitial@@3PADA = 1 diff --git a/src/cpp/shared/combat/army.h b/src/cpp/shared/combat/army.h index 2a65bf913..653423ac6 100644 --- a/src/cpp/shared/combat/army.h +++ b/src/cpp/shared/combat/army.h @@ -140,6 +140,7 @@ class army { int GetPowBaseY(); int RightX(); int LeftX(); + signed int OtherArmyAdjacent(int side, int idx); private: void RevertChargingMoveAnimation(); void SetChargingMoveAnimation(CHARGING_DIRECTION dir); diff --git a/src/cpp/shared/combat/combat.h b/src/cpp/shared/combat/combat.h index 5929c4049..dce3b370e 100644 --- a/src/cpp/shared/combat/combat.h +++ b/src/cpp/shared/combat/combat.h @@ -57,6 +57,17 @@ enum AOE_SPELL_DRAW_FLIP_TYPE { AOE_SPELL_DRAW_FLIP }; +enum AI_SPELL_TYPES { + AI_BATTLEFIELD_AFFECTING_SPELL = 0x0, + AI_MASS_BUFF_SPELL = 0x1, + AI_MASS_DEBUFF_SPELL = 0x2, + AI_BALL_SPALL = 0x3, + AI_TARGET_BUFF_SPELL = 0x4, + AI_TARGET_DAMAGE_OR_DEBUFF_SPELL = 0x5, + AI_RESURRECT_SPELL = 0x6, + AI_DISPEL_SPELL = 0x7, +}; + #pragma pack(push, 1) class hexcell { @@ -314,6 +325,13 @@ class combatManager : public baseManager void PlasmaCone(int hexIdx); void FireBomb(int hexIdx); void ImplosionGrenade(int hexIdx); + void DetermineEffectOfSpell(int spell, int *value, int *target); + void DetermineEffectOfSpell_orig(int spell, int *value, int *target); + int RawEffectSpellInfluence(army *stack, int eff); + int RawEffectSpellInfluence_orig(army *stack, int eff); + signed int FirstArmy(int startHex, int side, int *resultHex); + signed int FirstResurrectable(int startHex, int *resultHex, int spell); + void NextPos(int *hexIdx); }; extern combatManager* gpCombatManager; @@ -328,12 +346,14 @@ extern int giNextAction; extern int giNextActionExtra; extern int gbProcessingCombatAction; extern int giSpellEffectShowType; +extern int giCurrSpellGroup; extern unsigned char gColorTableGray[]; extern unsigned char gColorTableRed[]; extern unsigned char gColorTableDarkBrown[]; extern unsigned char gColorTableGray[]; extern unsigned char gColorTableLighten[]; extern char *gCombatFxNames[]; +extern unsigned char giSpellInfluenceToSpell[]; void __fastcall ModifyFrameInfo(struct SMonFrameInfo *frm, int creature); signed int __fastcall GetAdjacentCellIndexNoArmy(int hexIdx, signed int neighborIdx); bool IsCastleWall(int hexIdx); diff --git a/src/cpp/shared/combat/combat_ai.cpp b/src/cpp/shared/combat/combat_ai.cpp new file mode 100644 index 000000000..8357db099 --- /dev/null +++ b/src/cpp/shared/combat/combat_ai.cpp @@ -0,0 +1,294 @@ +#include "combat/combat.h" + +float heuristicModifierForDuration[] = { + 0.0, + 0.5, + 0.65, + 0.78, + 0.85, + 0.95, + 1.03, + 1.08, + 1.12, + 1.15, + 1.18 +}; + +float durationBenefit[] = { + 0.0, + 0.33, + 0.55, + 0.72, + 0.85, + 0.95, + 1.03, + 1.08, + 1.12, + 1.15, + 1.18 +}; + +void combatManager::DetermineEffectOfSpell(int spell, int *value, int *target) { + *value = 0; + AI_SPELL_TYPES aiSpellType; + int targetSide = 0; + + switch(spell) { + case SPELL_MASS_SLOW: + case SPELL_MASS_CURSE: + aiSpellType = AI_MASS_DEBUFF_SPELL; + targetSide = 1 - this->currentActionSide; + break; + case SPELL_SLOW: + case SPELL_CURSE: + aiSpellType = AI_TARGET_DAMAGE_OR_DEBUFF_SPELL; + targetSide = 1 - this->currentActionSide; + default: + this->DetermineEffectOfSpell_orig(spell, value, target); + return; + } + + int targetHex = 0; + int hex = 1; + + if(aiSpellType == AI_TARGET_BUFF_SPELL + || aiSpellType == AI_TARGET_DAMAGE_OR_DEBUFF_SPELL + || aiSpellType == AI_MASS_DEBUFF_SPELL + || aiSpellType == AI_MASS_BUFF_SPELL + || aiSpellType == AI_DISPEL_SPELL) + targetHex = this->FirstArmy(1, targetSide, &hex); + + bool targSpentTurn = 1; + float durBenefit = 1.0; + while(true) { + if(!targetHex) { + bool incapacitated = false; + bool underMyControl = false; + army *potTarg = nullptr; + + if(this->combatGrid[hex].stackIdx < 0 || this->combatGrid[hex].unitOwner < 0) { + potTarg = 0; + } else { + potTarg = &this->creatures[this->combatGrid[hex].unitOwner][this->combatGrid[hex].stackIdx]; + giCurrSpellGroup = this->combatGrid[hex].unitOwner; + targSpentTurn = (potTarg->creature.creature_flags & MAYBE_NOT_LOST_TURN) != 0; + int duration = this->heroSpellpowers[this->currentActionSide]; + if(this->heroes[this->currentActionSide]->HasArtifact(ARTIFACT_ENCHANTED_HOURGLASS)) + duration += 2; + if(this->heroes[this->currentActionSide]->HasArtifact(ARTIFACT_WIZARDS_HAT)) + duration += 10; + int durationBenefitIdx = duration - targSpentTurn; + if(duration - targSpentTurn >= 10) + durationBenefitIdx = 10; + durBenefit = durationBenefit[durationBenefitIdx]; + if(potTarg->effectStrengths[EFFECT_HYPNOTIZE] || potTarg->effectStrengths[EFFECT_BERSERKER]) + underMyControl = true; + if(potTarg->effectStrengths[EFFECT_BLIND] || potTarg->effectStrengths[EFFECT_PARALYZE] || potTarg->effectStrengths[EFFECT_PETRIFY]) + incapacitated = true; + } + + int effectValue = 0; + switch(spell) { + case SPELL_SLOW: + case SPELL_MASS_SLOW: + if(!incapacitated) { + if(!underMyControl) { + if(!potTarg->effectStrengths[EFFECT_SLOW]) { + effectValue = -this->RawEffectSpellInfluence(potTarg, EFFECT_SLOW) * durBenefit; + if(potTarg->effectStrengths[EFFECT_HASTE]) { + int durModifierIdx = targSpentTurn + potTarg->effectStrengths[EFFECT_HASTE]; + if(durModifierIdx >= 10) + durModifierIdx = 10; + + effectValue += this->RawEffectSpellInfluence(potTarg, EFFECT_HASTE) * heuristicModifierForDuration[durModifierIdx]; + } + } + } + } + break; + case SPELL_CURSE: + case SPELL_MASS_CURSE: + if(!incapacitated) { + if(!underMyControl) { + if(!potTarg->effectStrengths[EFFECT_CURSE]) { + effectValue = -this->RawEffectSpellInfluence(potTarg, EFFECT_CURSE) * durBenefit; + if(potTarg->effectStrengths[EFFECT_BLESS]) { + int durModifierIdx = targSpentTurn + potTarg->effectStrengths[EFFECT_BLESS]; + if(durModifierIdx >= 10) + durModifierIdx = 10; + effectValue += this->RawEffectSpellInfluence(potTarg, EFFECT_BLESS) * heuristicModifierForDuration[durModifierIdx]; + } + } + } + } + break; + default: + *value = 0; + break; + } + + int finalEffectValue = 0; + switch(aiSpellType) { + case AI_MASS_BUFF_SPELL: + case AI_MASS_DEBUFF_SPELL: + finalEffectValue += effectValue; + break; + case AI_BATTLEFIELD_AFFECTING_SPELL: + case AI_BALL_SPALL: + case AI_TARGET_BUFF_SPELL: + case AI_TARGET_DAMAGE_OR_DEBUFF_SPELL: + case AI_RESURRECT_SPELL: + case AI_DISPEL_SPELL: + finalEffectValue = effectValue; + break; + default: + break; + } + + if(aiSpellType == AI_MASS_BUFF_SPELL || aiSpellType == AI_MASS_DEBUFF_SPELL || *value < finalEffectValue) { + *value = finalEffectValue; + *target = hex; + } + + switch(aiSpellType) { + case AI_BATTLEFIELD_AFFECTING_SPELL: + targetHex = 1; + break; + case AI_MASS_BUFF_SPELL: + case AI_MASS_DEBUFF_SPELL: + case AI_TARGET_BUFF_SPELL: + case AI_TARGET_DAMAGE_OR_DEBUFF_SPELL: + case AI_DISPEL_SPELL: + targetHex = this->FirstArmy(hex + 1, targetSide, &hex); + break; + case AI_RESURRECT_SPELL: + targetHex = this->FirstResurrectable(hex + 1, &hex, spell); + break; + case AI_BALL_SPALL: + this->NextPos(&hex); + if(hex > 43) + targetHex = 1; + break; + } + continue; + } + break; + } +} + +int combatManager::RawEffectSpellInfluence(army *stack, int eff) { + double chance = stack->SpellCastWorkChance(giSpellInfluenceToSpell[eff]); + if(chance < 0.0001) { + return 0; + } + + int preResult; + int fightValue = stack->quantity * stack->creature.fight_value; + int owningSide = stack->owningSide; + int otherSide = 1 - owningSide; + switch(eff) { + case EFFECT_SLOW: + case EFFECT_HASTE: + { + int speedWithEffect; + if(eff == EFFECT_SLOW) + speedWithEffect = (stack->creature.speed + 1) >> 1; + else + speedWithEffect = stack->creature.speed + 2; + + if(eff == EFFECT_HASTE && stack->creature.creature_flags & FLYER) + return 0; + + if(this->isCastleBattle && owningSide == 1) + return 0; + + if(stack->creature.creature_flags & SHOOTER) + return 0; + + if(stack->GetAttackMask(stack->occupiedHex, 1, -1) == 255) { + int hexXCoord = stack->occupiedHex % 13; + int distFromOptimalColumn; + if(!this->currentActionSide) + distFromOptimalColumn = 10 - hexXCoord; + else + distFromOptimalColumn = hexXCoord - 2; + if(distFromOptimalColumn < 0) + distFromOptimalColumn = 0; + + + int adjustedDistFromOtherSide = distFromOptimalColumn + 2; + if(this->isCastleBattle) + adjustedDistFromOtherSide += 3; + double currentTurnsToOtherSide; + if(stack->creature.creature_flags & FLYER) + currentTurnsToOtherSide = 1.0; + else + currentTurnsToOtherSide = adjustedDistFromOtherSide / stack->creature.speed; + + double turnsToOtherSideAfterCast = adjustedDistFromOtherSide / speedWithEffect; + if(turnsToOtherSideAfterCast > 7.0) + turnsToOtherSideAfterCast = 7.0; + if(currentTurnsToOtherSide > 7.0) + currentTurnsToOtherSide = 7.0; + preResult = (currentTurnsToOtherSide - turnsToOtherSideAfterCast) / 10.0 * fightValue; + } else + return 0; + break; + } + case EFFECT_BLESS: + case EFFECT_CURSE: { + double avgDamage = (stack->creature.min_damage + stack->creature.max_damage) * 0.5; + preResult = (avgDamage - stack->creature.min_damage) / stack->creature.min_damage * fightValue * 0.45; + if(eff == EFFECT_CURSE) + preResult = -preResult; + break; + } + case EFFECT_DRAGON_SLAYER: { + bool hasAdjacentDragons = false; + int numDragons = 0; + + for(int i = 0; this->numCreatures[otherSide] > i; ++i) { + army* cr = &this->creatures[otherSide][i]; + int creatureIdx = cr->creatureIdx; + if(creatureIdx == CREATURE_GREEN_DRAGON + || creatureIdx == CREATURE_RED_DRAGON + || creatureIdx == CREATURE_BLACK_DRAGON + || creatureIdx == CREATURE_BONE_DRAGON) { + numDragons++; + if(stack->OtherArmyAdjacent(otherSide, cr->stackIdx)) + hasAdjacentDragons = true; + } + } + double dragonsVal; + if(hasAdjacentDragons) + dragonsVal = 1.0; + else + dragonsVal = numDragons / this->numCreatures[otherSide]; + preResult = 0.28 * dragonsVal; + break; + } + case EFFECT_SHIELD: { + int numShooters = 0; + for(int j = 0; this->numCreatures[otherSide] > j; ++j) { + if(this->creatures[otherSide][j].creature.creature_flags & SHOOTER) + numShooters++; + } + double shooterVal = numShooters / this->numCreatures[otherSide]; + if(!owningSide && this->isCastleBattle) { + shooterVal += 0.3; + if(shooterVal > 1.0) + shooterVal = 1.0; + } + preResult = 0.45 * shooterVal; + break; + } + default: + return this->RawEffectSpellInfluence_orig(stack, eff); + } + + if(stack->effectStrengths[EFFECT_BERSERKER] || stack->effectStrengths[EFFECT_HYPNOTIZE]) { + if(eff != EFFECT_ANTI_MAGIC) + return 0; + } + return preResult * chance; +} \ No newline at end of file diff --git a/src/cpp/shared/combat/combat_spells.cpp b/src/cpp/shared/combat/combat_spells.cpp index 9ef0aa58e..dbd1c0d32 100644 --- a/src/cpp/shared/combat/combat_spells.cpp +++ b/src/cpp/shared/combat/combat_spells.cpp @@ -1827,4 +1827,5 @@ int combatManager::ViewSpells(int unused) { return 0; return 1; } -} \ No newline at end of file +} + diff --git a/src/vcxproj/ironfist/ironfist.vcxproj b/src/vcxproj/ironfist/ironfist.vcxproj index ea61815eb..1782741a9 100644 --- a/src/vcxproj/ironfist/ironfist.vcxproj +++ b/src/vcxproj/ironfist/ironfist.vcxproj @@ -115,6 +115,7 @@ + diff --git a/src/vcxproj/ironfist/ironfist.vcxproj.filters b/src/vcxproj/ironfist/ironfist.vcxproj.filters index 59373a7b8..8f1c85ebe 100644 --- a/src/vcxproj/ironfist/ironfist.vcxproj.filters +++ b/src/vcxproj/ironfist/ironfist.vcxproj.filters @@ -339,6 +339,9 @@ Source Files\adventure + + Source Files\combat +