|
|
@@ -4440,6 +4440,26 @@ static EndAction end_action;
|
|
|
|
|
|
void CGameHandler::updateGateState()
|
|
|
{
|
|
|
+ // GATE_BRIDGE - leftmost tile, located over moat
|
|
|
+ // GATE_OUTER - central tile, mostly covered by gate image
|
|
|
+ // GATE_INNER - rightmost tile, inside the walls
|
|
|
+
|
|
|
+ // GATE_OUTER or GATE_INNER:
|
|
|
+ // - if defender moves unit on these tiles, bridge will open
|
|
|
+ // - if there is a creature (dead or alive) on these tiles, bridge will always remain open
|
|
|
+ // - blocked to attacker if bridge is closed
|
|
|
+
|
|
|
+ // GATE_BRIDGE
|
|
|
+ // - if there is a unit or corpse here, bridge can't open (and can't close in fortress)
|
|
|
+ // - if Force Field is cast here, bridge can't open (but can close, in any town)
|
|
|
+ // - deals moat damage to attacker if bridge is closed (fortress only)
|
|
|
+
|
|
|
+ bool hasForceFieldOnBridge = !battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), true).empty();
|
|
|
+ bool hasStackAtGateInner = gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_INNER), false) != nullptr;
|
|
|
+ bool hasStackAtGateOuter = gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr;
|
|
|
+ bool hasStackAtGateBridge = gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr;
|
|
|
+ bool hasLongBridge = gs->curB->town->subID == ETownType::FORTRESS;
|
|
|
+
|
|
|
BattleUpdateGateState db;
|
|
|
db.state = gs->curB->si.gateState;
|
|
|
if (gs->curB->si.wallState[EWallPart::GATE] == EWallState::DESTROYED)
|
|
|
@@ -4448,24 +4468,23 @@ void CGameHandler::updateGateState()
|
|
|
}
|
|
|
else if (db.state == EGateState::OPENED)
|
|
|
{
|
|
|
- if (!gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_OUTER), false) &&
|
|
|
- !gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_INNER), false))
|
|
|
- {
|
|
|
- if (gs->curB->town->subID == ETownType::FORTRESS)
|
|
|
- {
|
|
|
- if (!gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false))
|
|
|
- db.state = EGateState::CLOSED;
|
|
|
- }
|
|
|
- else if (gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_BRIDGE)))
|
|
|
- db.state = EGateState::BLOCKED;
|
|
|
- else
|
|
|
- db.state = EGateState::CLOSED;
|
|
|
- }
|
|
|
+ bool hasStackOnLongBridge = hasStackAtGateBridge && hasLongBridge;
|
|
|
+ bool gateCanClose = !hasStackAtGateInner && !hasStackAtGateOuter && !hasStackOnLongBridge;
|
|
|
+
|
|
|
+ if (gateCanClose)
|
|
|
+ db.state = EGateState::CLOSED;
|
|
|
+ else
|
|
|
+ db.state = EGateState::OPENED;
|
|
|
+ }
|
|
|
+ else // CLOSED or BLOCKED
|
|
|
+ {
|
|
|
+ bool gateBlocked = hasForceFieldOnBridge || hasStackAtGateBridge;
|
|
|
+
|
|
|
+ if (gateBlocked)
|
|
|
+ db.state = EGateState::BLOCKED;
|
|
|
+ else
|
|
|
+ db.state = EGateState::CLOSED;
|
|
|
}
|
|
|
- else if (gs->curB->battleGetStackByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false))
|
|
|
- db.state = EGateState::BLOCKED;
|
|
|
- else
|
|
|
- db.state = EGateState::CLOSED;
|
|
|
|
|
|
if (db.state != gs->curB->si.gateState)
|
|
|
sendAndApply(&db);
|
|
|
@@ -4804,7 +4823,7 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
|
|
|
case EActionType::CATAPULT:
|
|
|
{
|
|
|
//TODO: unify with spells::effects:Catapult
|
|
|
- auto getCatapultHitChance = [&](EWallPart::EWallPart part, const CHeroHandler::SBallisticsLevelInfo & sbi) -> int
|
|
|
+ auto getCatapultHitChance = [](EWallPart part, const CHeroHandler::SBallisticsLevelInfo & sbi) -> int
|
|
|
{
|
|
|
switch(part)
|
|
|
{
|
|
|
@@ -4825,115 +4844,105 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- auto wrapper = wrapAction(ba);
|
|
|
-
|
|
|
- if(target.size() < 1)
|
|
|
+ auto getBallisticsInfo = [this, &ba] (const CStack * actor)
|
|
|
{
|
|
|
- complain("Destination required for catapult action.");
|
|
|
- ok = false;
|
|
|
- break;
|
|
|
- }
|
|
|
- auto destination = target.at(0).hexValue;
|
|
|
-
|
|
|
- const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side);
|
|
|
+ const CGHeroInstance * attackingHero = gs->curB->battleGetFightingHero(ba.side);
|
|
|
|
|
|
- CHeroHandler::SBallisticsLevelInfo stackBallisticsParameters;
|
|
|
- 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);
|
|
|
- stackBallisticsParameters.shots = 1; //skip default "2 shots" from adv. ballistics
|
|
|
- }
|
|
|
+ if(actor->getCreature()->idNumber == CreatureID::CATAPULT)
|
|
|
+ return VLC->heroh->ballistics.at(attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::BALLISTICS));
|
|
|
else
|
|
|
- stackBallisticsParameters = VLC->heroh->ballistics.at(1);
|
|
|
+ {
|
|
|
+ //by design use advanced ballistics parameters with this bonus present, upg. cyclops use advanced ballistics, nonupg. use basic in OH3
|
|
|
+ int ballisticsLevel = actor->hasBonusOfType(Bonus::CATAPULT_EXTRA_SHOTS) ? 2 : 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 parameters = VLC->heroh->ballistics.at(ballisticsLevel);
|
|
|
+ parameters.shots = 1 + std::max(actor->valOfBonuses(Bonus::CATAPULT_EXTRA_SHOTS), 0);
|
|
|
+
|
|
|
+ return parameters;
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- auto wallPart = gs->curB->battleHexToWallPart(destination);
|
|
|
- if (!gs->curB->isWallPartPotentiallyAttackable(wallPart))
|
|
|
+ auto isWallPartAttackable = [this] (EWallPart part)
|
|
|
{
|
|
|
- complain("catapult tried to attack non-catapultable hex!");
|
|
|
- break;
|
|
|
- }
|
|
|
+ return (gs->curB->si.wallState[part] == EWallState::REINFORCED || gs->curB->si.wallState[part] == EWallState::INTACT || gs->curB->si.wallState[part] == EWallState::DAMAGED);
|
|
|
+ };
|
|
|
|
|
|
- //in successive iterations damage is dealt but not yet subtracted from wall's HPs
|
|
|
- auto ¤tHP = gs->curB->si.wallState;
|
|
|
+ CHeroHandler::SBallisticsLevelInfo stackBallisticsParameters = getBallisticsInfo(stack);
|
|
|
|
|
|
- if (currentHP.at(wallPart) == EWallState::DESTROYED || currentHP.at(wallPart) == EWallState::NONE)
|
|
|
- {
|
|
|
- complain("catapult tried to attack already destroyed wall part!");
|
|
|
- break;
|
|
|
- }
|
|
|
+ auto wrapper = wrapAction(ba);
|
|
|
+ auto destination = target.empty() ? BattleHex(BattleHex::INVALID) : target.at(0).hexValue;
|
|
|
+ auto desiredTarget = gs->curB->battleHexToWallPart(destination);
|
|
|
|
|
|
- for (int g=0; g<stackBallisticsParameters.shots; ++g)
|
|
|
+ for (int shotNumber=0; shotNumber<stackBallisticsParameters.shots; ++shotNumber)
|
|
|
{
|
|
|
- bool hitSuccessfull = false;
|
|
|
- auto attackedPart = wallPart;
|
|
|
+ auto actualTarget = EWallPart::INVALID;
|
|
|
|
|
|
- do // catapult has chance to attack desired target. Otherwise - attacks randomly
|
|
|
+ if ( isWallPartAttackable(desiredTarget) &&
|
|
|
+ getRandomGenerator().nextInt(99) < getCatapultHitChance(desiredTarget, stackBallisticsParameters))
|
|
|
{
|
|
|
- if (currentHP.at(attackedPart) != EWallState::DESTROYED && // this part can be hit
|
|
|
- currentHP.at(attackedPart) != EWallState::NONE &&
|
|
|
- getRandomGenerator().nextInt(99) < getCatapultHitChance(attackedPart, stackBallisticsParameters))//hit is successful
|
|
|
- {
|
|
|
- hitSuccessfull = true;
|
|
|
- }
|
|
|
- else // select new target
|
|
|
- {
|
|
|
- std::vector<EWallPart::EWallPart> allowedTargets;
|
|
|
- for (size_t i=0; i< currentHP.size(); i++)
|
|
|
- {
|
|
|
- if(currentHP.at(i) != EWallState::DESTROYED &&
|
|
|
- currentHP.at(i) != EWallState::NONE)
|
|
|
- allowedTargets.push_back(EWallPart::EWallPart(i));
|
|
|
- }
|
|
|
- if (allowedTargets.empty())
|
|
|
- break;
|
|
|
- attackedPart = *RandomGeneratorUtil::nextItem(allowedTargets, getRandomGenerator());
|
|
|
- }
|
|
|
+ actualTarget = desiredTarget;
|
|
|
}
|
|
|
- while (!hitSuccessfull);
|
|
|
+ else
|
|
|
+ {
|
|
|
+ static const std::array<EWallPart, 4> walls = { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL };
|
|
|
+ static const std::array<EWallPart, 3> towers= { EWallPart::BOTTOM_TOWER, EWallPart::KEEP, EWallPart::UPPER_TOWER };
|
|
|
+ static const EWallPart gates = EWallPart::GATE;
|
|
|
|
|
|
- if (!hitSuccessfull) // break triggered - no target to shoot at
|
|
|
- break;
|
|
|
+ // in H3, catapult under automatic control will attack objects in following order:
|
|
|
+ // walls, gates, towers
|
|
|
+ std::vector<EWallPart> potentialTargets;
|
|
|
+ for (auto & part : walls )
|
|
|
+ if (isWallPartAttackable(part))
|
|
|
+ potentialTargets.push_back(part);
|
|
|
|
|
|
- CatapultAttack ca; //package for clients
|
|
|
- CatapultAttack::AttackInfo attack;
|
|
|
- attack.attackedPart = attackedPart;
|
|
|
- attack.destinationTile = destination;
|
|
|
- attack.damageDealt = 0;
|
|
|
- BattleUnitsChanged removeUnits;
|
|
|
+ if (potentialTargets.empty() && isWallPartAttackable(gates))
|
|
|
+ potentialTargets.push_back(gates);
|
|
|
|
|
|
- int dmgChance[] = { stackBallisticsParameters.noDmg, stackBallisticsParameters.oneDmg, stackBallisticsParameters.twoDmg }; //dmgChance[i] - chance for doing i dmg when hit is successful
|
|
|
+ if (potentialTargets.empty())
|
|
|
+ for (auto & part : towers )
|
|
|
+ if (isWallPartAttackable(part))
|
|
|
+ potentialTargets.push_back(part);
|
|
|
+
|
|
|
+ if (potentialTargets.empty())
|
|
|
+ break; // everything is gone, can't attack anymore
|
|
|
+
|
|
|
+ actualTarget = *RandomGeneratorUtil::nextItem(potentialTargets, getRandomGenerator());
|
|
|
+ }
|
|
|
+ assert(actualTarget != EWallPart::INVALID);
|
|
|
+
|
|
|
+ std::array<int, 3> damageChances = { stackBallisticsParameters.noDmg, stackBallisticsParameters.oneDmg, stackBallisticsParameters.twoDmg }; //dmgChance[i] - chance for doing i dmg when hit is successful
|
|
|
+ int totalChance = std::accumulate(damageChances.begin(), damageChances.end(), 0);
|
|
|
+ int damageRandom = getRandomGenerator().nextInt(totalChance - 1);
|
|
|
+ int dealtDamage = 0;
|
|
|
|
|
|
- int dmgRand = getRandomGenerator().nextInt(99);
|
|
|
- //accumulating dmgChance
|
|
|
- dmgChance[1] += dmgChance[0];
|
|
|
- dmgChance[2] += dmgChance[1];
|
|
|
//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;
|
|
|
}
|
|
|
+ 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);
|
|
|
|
|
|
//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
|
|
|
- attackedPart == EWallPart::BOTTOM_TOWER || attackedPart == EWallPart::UPPER_TOWER))
|
|
|
+ if (gs->curB->si.wallState[actualTarget] == EWallState::DESTROYED && (actualTarget == EWallPart::KEEP || actualTarget == EWallPart::BOTTOM_TOWER || actualTarget == EWallPart::UPPER_TOWER))
|
|
|
{
|
|
|
int posRemove = -1;
|
|
|
- switch(attackedPart)
|
|
|
+ switch(actualTarget)
|
|
|
{
|
|
|
case EWallPart::KEEP:
|
|
|
posRemove = BattleHex::CASTLE_CENTRAL_TOWER;
|
|
|
@@ -4950,18 +4959,13 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
|
|
|
{
|
|
|
if(elem->initialPosition == posRemove)
|
|
|
{
|
|
|
+ BattleUnitsChanged removeUnits;
|
|
|
removeUnits.changedStacks.emplace_back(elem->unitId(), UnitChanges::EOperation::REMOVE);
|
|
|
+ sendAndApply(&removeUnits);
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- ca.attacker = ba.stackNumber;
|
|
|
- ca.attackedParts.push_back(attack);
|
|
|
-
|
|
|
- sendAndApply(&ca);
|
|
|
-
|
|
|
- if(!removeUnits.changedStacks.empty())
|
|
|
- sendAndApply(&removeUnits);
|
|
|
}
|
|
|
//finish by scope guard
|
|
|
break;
|
|
|
@@ -6783,8 +6787,6 @@ void CGameHandler::runBattle()
|
|
|
if (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(Bonus::MANUAL_CONTROL, CreatureID::CATAPULT))
|
|
|
{
|
|
|
BattleAction attack;
|
|
|
- auto destination = *RandomGeneratorUtil::nextItem(attackableBattleHexes, getRandomGenerator());
|
|
|
- attack.aimToHex(destination);
|
|
|
attack.actionType = EActionType::CATAPULT;
|
|
|
attack.side = next->side;
|
|
|
attack.stackNumber = next->ID;
|