1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-24 08:32:34 +02:00

Ballistics mechanics should now match H3

This commit is contained in:
Ivan Savenko 2023-01-12 23:53:29 +02:00
parent dd3adb7e16
commit b86704bece

View File

@ -4790,7 +4790,7 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
case EActionType::CATAPULT: case EActionType::CATAPULT:
{ {
//TODO: unify with spells::effects:Catapult //TODO: unify with spells::effects:Catapult
auto getCatapultHitChance = [&](EWallPart::EWallPart part, const CHeroHandler::SBallisticsLevelInfo & sbi) -> int auto getCatapultHitChance = [](EWallPart::EWallPart part, const CHeroHandler::SBallisticsLevelInfo & sbi) -> int
{ {
switch(part) switch(part)
{ {
@ -4811,115 +4811,105 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
} }
}; };
auto wrapper = wrapAction(ba); auto getBallisticsInfo = [this, &ba] (const CStack * actor)
if(target.size() < 1)
{ {
complain("Destination required for catapult action."); const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side);
ok = false;
break;
}
auto destination = target.at(0).hexValue;
const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side); if(actor->getCreature()->idNumber == CreatureID::CATAPULT)
return VLC->heroh->ballistics.at(attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::BALLISTICS));
CHeroHandler::SBallisticsLevelInfo stackBallisticsParameters; else
if(stack->getCreature()->idNumber == CreatureID::CATAPULT)
stackBallisticsParameters = VLC->heroh->ballistics.at(attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::BALLISTICS));
else
{
if(stack->hasBonusOfType(Bonus::CATAPULT_EXTRA_SHOTS)) //by design use advanced ballistics parameters with this bonus present, upg. cyclops use advanced ballistics, nonupg. use basic in OH3
{ {
stackBallisticsParameters = VLC->heroh->ballistics.at(2); //by design use advanced ballistics parameters with this bonus present, upg. cyclops use advanced ballistics, nonupg. use basic in OH3
stackBallisticsParameters.shots = 1; //skip default "2 shots" from adv. ballistics int ballisticsLevel = actor->hasBonusOfType(Bonus::CATAPULT_EXTRA_SHOTS) ? 2 : 1;
auto parameters = VLC->heroh->ballistics.at(ballisticsLevel);
parameters.shots = 1 + std::max(actor->valOfBonuses(Bonus::CATAPULT_EXTRA_SHOTS), 0);
return parameters;
}
};
auto isWallPartAttackable = [this] (EWallPart::EWallPart part)
{
return (gs->curB->si.wallState[part] == EWallState::INTACT || gs->curB->si.wallState[part] == EWallState::DAMAGED);
};
CHeroHandler::SBallisticsLevelInfo stackBallisticsParameters = getBallisticsInfo(stack);
auto wrapper = wrapAction(ba);
auto destination = target.empty() ? BattleHex(BattleHex::INVALID) : target.at(0).hexValue;
auto desiredTarget = gs->curB->battleHexToWallPart(destination);
for (int shotNumber=0; shotNumber<stackBallisticsParameters.shots; ++shotNumber)
{
auto actualTarget = EWallPart::INVALID;
if ( isWallPartAttackable(desiredTarget) &&
getRandomGenerator().nextInt(99) < getCatapultHitChance(desiredTarget, stackBallisticsParameters))
{
actualTarget = desiredTarget;
} }
else else
stackBallisticsParameters = VLC->heroh->ballistics.at(1);
stackBallisticsParameters.shots += std::max(stack->valOfBonuses(Bonus::CATAPULT_EXTRA_SHOTS), 0); //0 is allowed minimum to let modders force advanced ballistics for "oneshotting creatures"
}
auto wallPart = gs->curB->battleHexToWallPart(destination);
if (!gs->curB->isWallPartPotentiallyAttackable(wallPart))
{
complain("catapult tried to attack non-catapultable hex!");
break;
}
//in successive iterations damage is dealt but not yet subtracted from wall's HPs
auto &currentHP = gs->curB->si.wallState;
if (currentHP.at(wallPart) == EWallState::DESTROYED || currentHP.at(wallPart) == EWallState::NONE)
{
complain("catapult tried to attack already destroyed wall part!");
break;
}
for (int g=0; g<stackBallisticsParameters.shots; ++g)
{
bool hitSuccessfull = false;
auto attackedPart = wallPart;
do // catapult has chance to attack desired target. Otherwise - attacks randomly
{ {
if (currentHP.at(attackedPart) != EWallState::DESTROYED && // this part can be hit static const std::array<EWallPart::EWallPart, 4> walls = { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL };
currentHP.at(attackedPart) != EWallState::NONE && static const std::array<EWallPart::EWallPart, 3> towers= { EWallPart::BOTTOM_TOWER, EWallPart::KEEP, EWallPart::UPPER_TOWER };
getRandomGenerator().nextInt(99) < getCatapultHitChance(attackedPart, stackBallisticsParameters))//hit is successful static const EWallPart::EWallPart gates = EWallPart::GATE;
{
hitSuccessfull = true; // in H3, catapult under automatic control will attack objects in following order:
} // walls, gates, towers
else // select new target std::vector<EWallPart::EWallPart> potentialTargets;
{ for (auto & part : walls )
std::vector<EWallPart::EWallPart> allowedTargets; if (isWallPartAttackable(part))
for (size_t i=0; i< currentHP.size(); i++) potentialTargets.push_back(part);
{
if(currentHP.at(i) != EWallState::DESTROYED && if (potentialTargets.empty() && isWallPartAttackable(gates))
currentHP.at(i) != EWallState::NONE) potentialTargets.push_back(gates);
allowedTargets.push_back(EWallPart::EWallPart(i));
} if (potentialTargets.empty())
if (allowedTargets.empty()) for (auto & part : towers )
break; if (isWallPartAttackable(part))
attackedPart = *RandomGeneratorUtil::nextItem(allowedTargets, getRandomGenerator()); potentialTargets.push_back(part);
}
if (potentialTargets.empty())
break; // everything is gone, can't attack anymore
actualTarget = *RandomGeneratorUtil::nextItem(potentialTargets, getRandomGenerator());
} }
while (!hitSuccessfull); assert(actualTarget != EWallPart::INVALID);
if (!hitSuccessfull) // break triggered - no target to shoot at std::array<int, 3> damageChances = { stackBallisticsParameters.noDmg, stackBallisticsParameters.oneDmg, stackBallisticsParameters.twoDmg }; //dmgChance[i] - chance for doing i dmg when hit is successful
break; int totalChance = std::accumulate(damageChances.begin(), damageChances.end(), 0);
int damageRandom = getRandomGenerator().nextInt(totalChance - 1);
int dealtDamage = 0;
CatapultAttack ca; //package for clients
CatapultAttack::AttackInfo attack;
attack.attackedPart = attackedPart;
attack.destinationTile = destination;
attack.damageDealt = 0;
BattleUnitsChanged removeUnits;
int dmgChance[] = { stackBallisticsParameters.noDmg, stackBallisticsParameters.oneDmg, stackBallisticsParameters.twoDmg }; //dmgChance[i] - chance for doing i dmg when hit is successful
int dmgRand = getRandomGenerator().nextInt(99);
//accumulating dmgChance
dmgChance[1] += dmgChance[0];
dmgChance[2] += dmgChance[1];
//calculating dealt damage //calculating dealt damage
for (int damage = 0; damage < ARRAY_COUNT(dmgChance); ++damage) for (int damage = 0; damage < damageChances.size(); ++damage)
{ {
if (dmgRand <= dmgChance[damage]) if (damageRandom <= damageChances[damage])
{ {
attack.damageDealt = damage; dealtDamage = damage;
break; break;
} }
damageRandom -= damageChances[damage];
} }
// attacked tile may have changed - update destination
attack.destinationTile = gs->curB->wallPartToBattleHex(EWallPart::EWallPart(attack.attackedPart)); CatapultAttack::AttackInfo attack;
attack.attackedPart = actualTarget;
attack.destinationTile = gs->curB->wallPartToBattleHex(actualTarget);
attack.damageDealt = dealtDamage;
CatapultAttack ca; //package for clients
ca.attacker = ba.stackNumber;
ca.attackedParts.push_back(attack);
sendAndApply(&ca);
logGlobal->trace("Catapult attacks %d dealing %d damage", (int)attack.attackedPart, (int)attack.damageDealt); logGlobal->trace("Catapult attacks %d dealing %d damage", (int)attack.attackedPart, (int)attack.damageDealt);
//removing creatures in turrets / keep if one is destroyed //removing creatures in turrets / keep if one is destroyed
if (currentHP.at(attackedPart) - attack.damageDealt <= 0 && (attackedPart == EWallPart::KEEP || //HP enum subtraction not intuitive, consider using SiegeInfo::applyDamage if (gs->curB->si.wallState[actualTarget] <= 0 && (actualTarget == EWallPart::KEEP || actualTarget == EWallPart::BOTTOM_TOWER || actualTarget == EWallPart::UPPER_TOWER))
attackedPart == EWallPart::BOTTOM_TOWER || attackedPart == EWallPart::UPPER_TOWER))
{ {
int posRemove = -1; int posRemove = -1;
switch(attackedPart) switch(actualTarget)
{ {
case EWallPart::KEEP: case EWallPart::KEEP:
posRemove = BattleHex::CASTLE_CENTRAL_TOWER; posRemove = BattleHex::CASTLE_CENTRAL_TOWER;
@ -4936,18 +4926,13 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
{ {
if(elem->initialPosition == posRemove) if(elem->initialPosition == posRemove)
{ {
BattleUnitsChanged removeUnits;
removeUnits.changedStacks.emplace_back(elem->unitId(), UnitChanges::EOperation::REMOVE); removeUnits.changedStacks.emplace_back(elem->unitId(), UnitChanges::EOperation::REMOVE);
sendAndApply(&removeUnits);
break; break;
} }
} }
} }
ca.attacker = ba.stackNumber;
ca.attackedParts.push_back(attack);
sendAndApply(&ca);
if(!removeUnits.changedStacks.empty())
sendAndApply(&removeUnits);
} }
//finish by scope guard //finish by scope guard
break; break;
@ -6769,8 +6754,6 @@ void CGameHandler::runBattle()
if (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(Bonus::MANUAL_CONTROL, CreatureID::CATAPULT)) if (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(Bonus::MANUAL_CONTROL, CreatureID::CATAPULT))
{ {
BattleAction attack; BattleAction attack;
auto destination = *RandomGeneratorUtil::nextItem(attackableBattleHexes, getRandomGenerator());
attack.aimToHex(destination);
attack.actionType = EActionType::CATAPULT; attack.actionType = EActionType::CATAPULT;
attack.side = next->side; attack.side = next->side;
attack.stackNumber = next->ID; attack.stackNumber = next->ID;