diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 2de90c41e..f6a8faba2 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -29,7 +29,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo &AttackInfo auto attacker = AttackInfo.attacker; auto enemy = AttackInfo.defender; - const int remainingCounterAttacks = getValOr(state.counterAttacksLeft, enemy, enemy->counterAttacksRemaining()); + const int remainingCounterAttacks = getValOr(state.counterAttacksLeft, enemy, enemy->counterAttacks.available()); const bool counterAttacksBlocked = attacker->hasBonusOfType(Bonus::BLOCKS_RETALIATION) || enemy->hasBonusOfType(Bonus::NO_RETALIATION); const int totalAttacks = 1 + AttackInfo.attackerBonuses->getBonuses(Selector::type(Bonus::ADDITIONAL_ATTACK), (Selector::effectRange (Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_MELEE_FIGHT))))->totalValue(); @@ -46,19 +46,15 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo &AttackInfo if(remainingCounterAttacks <= i || counterAttacksBlocked) ap.damageReceived = 0; - curBai.attackerCount = attacker->count - attacker->countKilledByAttack(ap.damageReceived).first; - curBai.defenderCount = enemy->count - enemy->countKilledByAttack(ap.damageDealt).first; - if(!curBai.attackerCount) + curBai.attackerHealth = attacker->healthAfterAttacked(ap.damageReceived); + curBai.defenderHealth = enemy->healthAfterAttacked(ap.damageDealt); + if(curBai.attackerHealth.getCount() <= 0) break; //TODO what about defender? should we break? but in pessimistic scenario defender might be alive } //TODO other damage related to attack (eg. fire shield and other abilities) - //Limit damages by total stack health - vstd::amin(ap.damageDealt, enemy->totalHealth()); - vstd::amin(ap.damageReceived, attacker->totalHealth()); - return ap; } diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 3bbc50431..cf4c2019c 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -55,7 +55,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack ) auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); std::map woundHpToStack; for(auto stack : healingTargets) - if(auto woundHp = stack->MaxHealth() - stack->firstHPleft) + if(auto woundHp = stack->MaxHealth() - stack->getFirstHPleft()) woundHpToStack[woundHp] = stack; if(woundHpToStack.empty()) return BattleAction::makeDefend(stack); diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index aeabd55ef..3853f1ff1 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -317,7 +317,7 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details) return; } - ui32 speed; + ui32 speed = 0; if(settings["session"]["spectate"].Bool()) { if(!settings["session"]["spectate-hero-speed"].isNull()) @@ -699,33 +699,46 @@ void CPlayerInterface::battleStacksHealedRes(const std::vectorbattleGetStackByID(healedStacks[0].first, false); - const CStack *defender = cb->battleGetStackByID(lifeDrainFrom, false); - int textOff = 0; + const CStack * attacker = cb->battleGetStackByID(healedStacks[0].first, false); + const CStack * defender = cb->battleGetStackByID(lifeDrainFrom, false); - if (attacker) + if(attacker && defender) { battleInt->displayEffect(52, attacker->position); //TODO: transparency - if (attacker->count > 1) - { - textOff += 1; - } CCS->soundh->playSound(soundBase::DRAINLIF); - //print info about life drain - auto txt = boost::format (CGI->generaltexth->allTexts[361 + textOff]) % attacker->getCreature()->nameSing % healedStacks[0].second % defender->getCreature()->namePl; - battleInt->console->addText(boost::to_string(txt)); + MetaString text; + attacker->addText(text, MetaString::GENERAL_TXT, 361); + attacker->addNameReplacement(text, false); + text.addReplacement(healedStacks[0].second); + defender->addNameReplacement(text, true); + battleInt->console->addText(text.toString()); + } + else + { + logGlobal->error("Unable to display life drain info"); } } - if (tentHeal) + if(tentHeal) { - std::string text = CGI->generaltexth->allTexts[414]; - boost::algorithm::replace_first(text, "%s", cb->battleGetStackByID(lifeDrainFrom, false)->getCreature()->nameSing); - boost::algorithm::replace_first(text, "%s", cb->battleGetStackByID(healedStacks[0].first, false)->getCreature()->nameSing); - boost::algorithm::replace_first(text, "%d", boost::lexical_cast(healedStacks[0].second)); - battleInt->console->addText(text); + const CStack * healer = cb->battleGetStackByID(lifeDrainFrom, false); + const CStack * target = cb->battleGetStackByID(healedStacks[0].first, false); + + if(healer && target) + { + MetaString text; + text.addTxt(MetaString::GENERAL_TXT, 414); + healer->addNameReplacement(text, false); + target->addNameReplacement(text, false); + text.addReplacement(healedStacks[0].second); + battleInt->console->addText(text.toString()); + } + else + { + logGlobal->error("Unable to display tent heal info"); + } } } @@ -939,53 +952,50 @@ void CPlayerInterface::battleStacksAttacked(const std::vectorstacksAreAttacked(arg); } -void CPlayerInterface::battleAttack(const BattleAttack *ba) +void CPlayerInterface::battleAttack(const BattleAttack * ba) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; assert(curAction); - if (ba->lucky()) //lucky hit + + const CStack * attacker = cb->battleGetStackByID(ba->stackAttacking); + + if(!attacker) { - const CStack *stack = cb->battleGetStackByID(ba->stackAttacking); - std::string hlp = CGI->generaltexth->allTexts[45]; - boost::algorithm::replace_first(hlp,"%s", (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str()); - battleInt->console->addText(hlp); - battleInt->displayEffect(18, stack->position); + logGlobal->error("Attacking stack not found"); + return; + } + + if(ba->lucky()) //lucky hit + { + battleInt->console->addText(attacker->formatGeneralMessage(-45)); + battleInt->displayEffect(18, attacker->position); CCS->soundh->playSound(soundBase::GOODLUCK); } - if (ba->unlucky()) //unlucky hit + if(ba->unlucky()) //unlucky hit { - const CStack *stack = cb->battleGetStackByID(ba->stackAttacking); - std::string hlp = CGI->generaltexth->allTexts[44]; - boost::algorithm::replace_first(hlp,"%s", (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str()); - battleInt->console->addText(hlp); - battleInt->displayEffect(48, stack->position); + battleInt->console->addText(attacker->formatGeneralMessage(-44)); + battleInt->displayEffect(48, attacker->position); CCS->soundh->playSound(soundBase::BADLUCK); } - if (ba->deathBlow()) + if(ba->deathBlow()) { - const CStack *stack = cb->battleGetStackByID(ba->stackAttacking); - std::string hlp = CGI->generaltexth->allTexts[(stack->count != 1) ? 366 : 365]; - boost::algorithm::replace_first(hlp,"%s", (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str()); - battleInt->console->addText(hlp); - for (auto & elem : ba->bsa) + battleInt->console->addText(attacker->formatGeneralMessage(365)); + for(auto & elem : ba->bsa) { const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked); battleInt->displayEffect(73, attacked->position); } CCS->soundh->playSound(soundBase::deathBlow); - } battleInt->waitForAnims(); - const CStack * attacker = cb->battleGetStackByID(ba->stackAttacking); - - if (ba->shot()) + if(ba->shot()) { - for (auto & elem : ba->bsa) + for(auto & elem : ba->bsa) { - if (!elem.isSecondary()) //display projectile only for primary target + if(!elem.isSecondary()) //display projectile only for primary target { const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked); battleInt->stackAttacking(attacker, attacked->position, attacked, true); @@ -995,27 +1005,27 @@ void CPlayerInterface::battleAttack(const BattleAttack *ba) else { int shift = 0; - if (ba->counter() && BattleHex::mutualPosition(curAction->destinationTile, attacker->position) < 0) + if(ba->counter() && BattleHex::mutualPosition(curAction->destinationTile, attacker->position) < 0) { int distp = BattleHex::getDistance(curAction->destinationTile + 1, attacker->position); int distm = BattleHex::getDistance(curAction->destinationTile - 1, attacker->position); - if ( distp < distm ) + if(distp < distm) shift = 1; else shift = -1; } const CStack * attacked = cb->battleGetStackByID(ba->bsa.begin()->stackAttacked); - battleInt->stackAttacking( attacker, ba->counter() ? curAction->destinationTile + shift : curAction->additionalInfo, attacked, false); + battleInt->stackAttacking(attacker, ba->counter() ? curAction->destinationTile + shift : curAction->additionalInfo, attacked, false); } //battleInt->waitForAnims(); //FIXME: freeze - if (ba->spellLike()) + if(ba->spellLike()) { //display hit animation SpellID spellID = ba->spellID; - battleInt->displaySpellHit(spellID,curAction->destinationTile); + battleInt->displaySpellHit(spellID, curAction->destinationTile); } } void CPlayerInterface::battleObstaclePlaced(const CObstacleInstance &obstacle) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 80ec224cb..afc2d0235 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -728,12 +728,12 @@ void BattleResultsApplied::applyCl(CClient *cl) INTERFACE_CALL_IF_PRESENT(PlayerColor::SPECTATOR, battleResultsApplied); } -void StacksHealedOrResurrected::applyCl(CClient *cl) +void StacksHealedOrResurrected::applyCl(CClient * cl) { std::vector > shiftedHealed; for(auto & elem : healedStacks) { - shiftedHealed.push_back(std::make_pair(elem.stackID, elem.healedHP)); + shiftedHealed.push_back(std::make_pair(elem.stackId, (ui32)elem.delta)); } BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(battleStacksHealedRes, shiftedHealed, lifeDrain, tentHealing, drainedFrom); } diff --git a/client/battle/CBattleInterface.cpp b/client/battle/CBattleInterface.cpp index a47150915..6335fe557 100644 --- a/client/battle/CBattleInterface.cpp +++ b/client/battle/CBattleInterface.cpp @@ -1356,25 +1356,8 @@ void CBattleInterface::spellCast(const BattleSpellCast *sc) void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) { - if(sse.stacks.size() == 1 && sse.effect.size() == 2 && sse.effect.back().sid == -1) - { - const Bonus & bns = sse.effect.front(); - if(bns.source == Bonus::OTHER && bns.type == Bonus::PRIMARY_SKILL) - { - //defensive stance - const CStack *stack = LOCPLINT->cb->battleGetStackByID(*sse.stacks.begin()); - int txtid = 120; - - if(stack->count != 1) - txtid++; //move to plural text - - BonusList defenseBonuses = *(stack->getBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::DEFENSE))); - defenseBonuses.remove_if(Bonus::UntilGetsTurn); //remove bonuses gained from defensive stance - int val = stack->Defense() - defenseBonuses.totalValue(); - auto txt = boost::format(CGI->generaltexth->allTexts[txtid]) % ((stack->count != 1) ? stack->getCreature()->namePl : stack->getCreature()->nameSing) % val; - console->addText(boost::to_string(txt)); - } - } + for(const MetaString & line : sse.battleLog) + console->addText(line.toString()); if(activeStack != nullptr) redrawBackgroundWithHexes(activeStack); @@ -1592,10 +1575,10 @@ void CBattleInterface::activateStack() //set casting flag to true if creature can use it to not check it every time const auto spellcaster = s->getBonusLocalFirst(Selector::type(Bonus::SPELLCASTER)), randomSpellcaster = s->getBonusLocalFirst(Selector::type(Bonus::RANDOM_SPELLCASTER)); - if (s->casts && (spellcaster || randomSpellcaster)) + if(s->canCast() && (spellcaster || randomSpellcaster)) { stackCanCastSpell = true; - if (randomSpellcaster) + if(randomSpellcaster) creatureSpellToCast = -1; //spell will be set later on cast else creatureSpellToCast = curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), s, CBattleInfoCallback::RANDOM_AIMED); //faerie dragon can cast only one spell until their next move @@ -1696,16 +1679,16 @@ void CBattleInterface::getPossibleActionsForStack(const CStack *stack, const boo { PossibleActions notPriority = INVALID; //first action will be prioritized over later ones - if (stack->casts) //TODO: check for battlefield effects that prevent casting? + if(stack->canCast()) //TODO: check for battlefield effects that prevent casting? { - if (stack->hasBonusOfType (Bonus::SPELLCASTER)) + if(stack->hasBonusOfType (Bonus::SPELLCASTER)) { - if (creatureSpellToCast != -1) + if(creatureSpellToCast != -1) { const CSpell *spell = SpellID(creatureSpellToCast).toSpell(); PossibleActions act = getCasterAction(spell, stack, ECastingMode::CREATURE_ACTIVE_CASTING); - if (forceCast) + if(forceCast) { //forced action to be only one possible possibleActions.push_back(act); @@ -1721,10 +1704,10 @@ void CBattleInterface::getPossibleActionsForStack(const CStack *stack, const boo if (stack->hasBonusOfType (Bonus::DAEMON_SUMMONING)) possibleActions.push_back (RISE_DEMONS); } - if (stack->shots && stack->hasBonusOfType (Bonus::SHOOTER)) - possibleActions.push_back (SHOOT); - if (stack->hasBonusOfType (Bonus::RETURN_AFTER_STRIKE)) - possibleActions.push_back (ATTACK_AND_RETURN); + if(stack->canShoot()) + possibleActions.push_back(SHOOT); + if(stack->hasBonusOfType(Bonus::RETURN_AFTER_STRIKE)) + possibleActions.push_back(ATTACK_AND_RETURN); possibleActions.push_back(ATTACK); //all active stacks can attack possibleActions.push_back(WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere @@ -1742,39 +1725,39 @@ void CBattleInterface::getPossibleActionsForStack(const CStack *stack, const boo } } -void CBattleInterface::printConsoleAttacked( const CStack *defender, int dmg, int killed, const CStack *attacker, bool multiple ) +void CBattleInterface::printConsoleAttacked(const CStack * defender, int dmg, int killed, const CStack * attacker, bool multiple) { std::string formattedText; - if (attacker) //ignore if stacks were killed by spell + if(attacker) //ignore if stacks were killed by spell { - boost::format txt = boost::format (CGI->generaltexth->allTexts[attacker->count > 1 ? 377 : 376]) % - (attacker->count > 1 ? attacker->getCreature()->namePl : attacker->getCreature()->nameSing) % dmg; - formattedText.append(boost::to_string(txt)); + MetaString text; + attacker->addText(text, MetaString::GENERAL_TXT, 376); + attacker->addNameReplacement(text); + text.addReplacement(dmg); + formattedText = text.toString(); } - if (killed > 0) + + if(killed > 0) { - if (attacker) + if(attacker) formattedText.append(" "); boost::format txt; - if (killed > 1) + if(killed > 1) { - txt = boost::format (CGI->generaltexth->allTexts[379]) % killed % (multiple ? CGI->generaltexth->allTexts[43] : defender->getCreature()->namePl); // creatures perish + txt = boost::format(CGI->generaltexth->allTexts[379]) % killed % (multiple ? CGI->generaltexth->allTexts[43] : defender->getCreature()->namePl); // creatures perish } else //killed == 1 { - txt = boost::format (CGI->generaltexth->allTexts[378]) % (multiple ? CGI->generaltexth->allTexts[42] : defender->getCreature()->nameSing); // creature perishes + txt = boost::format(CGI->generaltexth->allTexts[378]) % (multiple ? CGI->generaltexth->allTexts[42] : defender->getCreature()->nameSing); // creature perishes } std::string trimmed = boost::to_string(txt); boost::algorithm::trim(trimmed); // these default h3 texts have unnecessary new lines, so get rid of them before displaying formattedText.append(trimmed); } console->addText(formattedText); - } - - void CBattleInterface::endAction(const BattleAction* action) { const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); @@ -1960,19 +1943,11 @@ void CBattleInterface::startAction(const BattleAction* action) break; } - if (txtid > 0 && stack->count != 1) - txtid++; //move to plural text - else if (txtid < 0) - txtid = -txtid; - - if (txtid) - { - std::string name = (stack->count != 1) ? stack->getCreature()->namePl.c_str() : stack->getCreature()->nameSing.c_str(); - console->addText((boost::format(CGI->generaltexth->allTexts[txtid].c_str()) % name).str()); - } + if(txtid != 0) + console->addText(stack->formatGeneralMessage(txtid)); //displaying special abilities - switch (action->actionType) + switch(action->actionType) { case Battle::STACK_HEAL: displayEffect(74, action->destinationTile); @@ -2200,7 +2175,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType) if (!(shere->hasBonusOfType(Bonus::UNDEAD) || shere->hasBonusOfType(Bonus::NON_LIVING) || vstd::contains(shere->state, EBattleStackState::SUMMONED) - || vstd::contains(shere->state, EBattleStackState::CLONED) + || shere->isClone() || shere->hasBonusOfType(Bonus::SIEGE_WEAPON) )) legalAction = true; @@ -2304,7 +2279,7 @@ void CBattleInterface::handleHex(BattleHex myNumber, int eventType) realizeAction = [=] {giveCommand(Battle::SHOOT, myNumber, activeStack->ID);}; std::string estDmgText = formatDmgRange(curInt->cb->battleEstimateDamage(CRandomGenerator::getDefault(), sactive, shere)); //calculating estimated dmg //printing - Shoot %s (%d shots left, %s damage) - consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % sactive->shots % estDmgText).str(); + consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % sactive->shots.available() % estDmgText).str(); } break; case AIMED_SPELL_CREATURE: @@ -3273,10 +3248,10 @@ void CBattleInterface::showAliveStacks(SDL_Surface *to, std::vector bool { - if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->count == 1) //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature + if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->getCount() == 1) //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature return false; - if(stack->count == 0) //hide box when target is going to die anyway - do not display "0 creatures" + if(stack->getCount() == 0) //hide box when target is going to die anyway - do not display "0 creatures" return false; for(auto anim : pendingAnims) //no matter what other conditions below are, hide box when creature is playing hit animation @@ -3353,7 +3328,7 @@ void CBattleInterface::showAliveStacks(SDL_Surface *to, std::vectorID]->pos.x + xAdd + amountNormal->w/2, creAnims[stack->ID]->pos.y + yAdd + amountNormal->h/2); - graphics->fonts[FONT_TINY]->renderTextCenter(to, makeNumberShort(stack->count), Colors::WHITE, textPos); + graphics->fonts[FONT_TINY]->renderTextCenter(to, makeNumberShort(stack->getCount()), Colors::WHITE, textPos); } } } diff --git a/client/battle/CBattleInterface.h b/client/battle/CBattleInterface.h index ababbecfb..80e3766e8 100644 --- a/client/battle/CBattleInterface.h +++ b/client/battle/CBattleInterface.h @@ -53,7 +53,7 @@ class CBattleGameInterface; struct StackAttackedInfo { const CStack *defender; //attacked stack - unsigned int dmg; //damage dealt + int32_t dmg; //damage dealt unsigned int amountKilled; //how many creatures in stack has been killed const CStack *attacker; //attacking stack bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack diff --git a/client/battle/CBattleInterfaceClasses.cpp b/client/battle/CBattleInterfaceClasses.cpp index 8cee5a70d..a5f86ade1 100644 --- a/client/battle/CBattleInterfaceClasses.cpp +++ b/client/battle/CBattleInterfaceClasses.cpp @@ -595,9 +595,10 @@ void CClickableHex::mouseMoved(const SDL_MouseMotionEvent &sEvent) attackedStack->owner != myInterface->getCurrentPlayerInterface()->playerID && attackedStack->alive()) { - const std::string & attackedName = attackedStack->count == 1 ? attackedStack->getCreature()->nameSing : attackedStack->getCreature()->namePl; - auto txt = boost::format (CGI->generaltexth->allTexts[220]) % attackedName; - myInterface->console->alterTxt = boost::to_string(txt); + MetaString text; + text.addTxt(MetaString::GENERAL_TXT, 220); + attackedStack->addNameReplacement(text); + myInterface->console->alterTxt = text.toString(); setAlterText = true; } } @@ -744,9 +745,9 @@ void CStackQueue::StackBox::showAll(SDL_Surface * to) CIntObject::showAll(to); if(small) - printAtMiddleLoc(makeNumberShort(stack->count), pos.w/2, pos.h - 7, FONT_SMALL, Colors::WHITE, to); + printAtMiddleLoc(makeNumberShort(stack->getCount()), pos.w/2, pos.h - 7, FONT_SMALL, Colors::WHITE, to); else - printAtMiddleLoc(makeNumberShort(stack->count), pos.w/2, pos.h - 8, FONT_MEDIUM, Colors::WHITE, to); + printAtMiddleLoc(makeNumberShort(stack->getCount()), pos.w/2, pos.h - 8, FONT_MEDIUM, Colors::WHITE, to); } void CStackQueue::StackBox::setStack( const CStack *stack ) diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index 4e028f17f..ad799b56c 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -224,41 +224,46 @@ void CStackWindow::CWindowSection::createStackInfo(bool showExp, bool showArt) new CPicture("stackWindow/icons", 117, 32); const CStack * battleStack = parent->info->stack; - bool shooter = parent->info->stackNode->hasBonusOfType(Bonus::SHOOTER) && parent->info->stackNode->valOfBonuses(Bonus::SHOTS); - bool caster = parent->info->stackNode->valOfBonuses(Bonus::CASTS); - if (battleStack != nullptr) // in battle + auto morale = new MoraleLuckBox(true, genRect(42, 42, 321, 110)); + auto luck = new MoraleLuckBox(false, genRect(42, 42, 375, 110)); + + if(battleStack != nullptr) // in battle { printStatBase(EStat::ATTACK, CGI->generaltexth->primarySkillNames[0], parent->info->creature->Attack(), battleStack->Attack()); printStatBase(EStat::DEFENCE, CGI->generaltexth->primarySkillNames[1], parent->info->creature->Defense(false), battleStack->Defense()); printStatRange(EStat::DAMAGE, CGI->generaltexth->allTexts[199], parent->info->stackNode->getMinDamage() * dmgMultiply, battleStack->getMaxDamage() * dmgMultiply); - printStatBase(EStat::HEALTH, CGI->generaltexth->allTexts[388], parent->info->creature->valOfBonuses(Bonus::STACK_HEALTH), battleStack->valOfBonuses(Bonus::STACK_HEALTH)); + printStatBase(EStat::HEALTH, CGI->generaltexth->allTexts[388], parent->info->creature->MaxHealth(), battleStack->MaxHealth()); printStatBase(EStat::SPEED, CGI->generaltexth->zelp[441].first, parent->info->creature->Speed(), battleStack->Speed()); - if (shooter) - printStatBase(EStat::SHOTS, CGI->generaltexth->allTexts[198], battleStack->valOfBonuses(Bonus::SHOTS), battleStack->shots); - if (caster) - printStatBase(EStat::MANA, CGI->generaltexth->allTexts[399], battleStack->valOfBonuses(Bonus::CASTS), battleStack->casts); - printStat(EStat::HEALTH_LEFT, CGI->generaltexth->allTexts[200], battleStack->firstHPleft); + if(battleStack->isShooter()) + printStatBase(EStat::SHOTS, CGI->generaltexth->allTexts[198], battleStack->shots.total(), battleStack->shots.available()); + if(battleStack->isCaster()) + printStatBase(EStat::MANA, CGI->generaltexth->allTexts[399], battleStack->casts.total(), battleStack->casts.available()); + printStat(EStat::HEALTH_LEFT, CGI->generaltexth->allTexts[200], battleStack->getFirstHPleft()); + + morale->set(battleStack); + luck->set(battleStack); } else { + const bool shooter = parent->info->stackNode->hasBonusOfType(Bonus::SHOOTER) && parent->info->stackNode->valOfBonuses(Bonus::SHOTS); + const bool caster = parent->info->stackNode->valOfBonuses(Bonus::CASTS); + printStatBase(EStat::ATTACK, CGI->generaltexth->primarySkillNames[0], parent->info->creature->Attack(), parent->info->stackNode->Attack()); printStatBase(EStat::DEFENCE, CGI->generaltexth->primarySkillNames[1], parent->info->creature->Defense(false), parent->info->stackNode->Defense()); printStatRange(EStat::DAMAGE, CGI->generaltexth->allTexts[199], parent->info->stackNode->getMinDamage() * dmgMultiply, parent->info->stackNode->getMaxDamage() * dmgMultiply); - printStatBase(EStat::HEALTH, CGI->generaltexth->allTexts[388], parent->info->creature->valOfBonuses(Bonus::STACK_HEALTH), parent->info->stackNode->valOfBonuses(Bonus::STACK_HEALTH)); + printStatBase(EStat::HEALTH, CGI->generaltexth->allTexts[388], parent->info->creature->MaxHealth(), parent->info->stackNode->MaxHealth()); printStatBase(EStat::SPEED, CGI->generaltexth->zelp[441].first, parent->info->creature->Speed(), parent->info->stackNode->Speed()); - if (shooter) + if(shooter) printStat(EStat::SHOTS, CGI->generaltexth->allTexts[198], parent->info->stackNode->valOfBonuses(Bonus::SHOTS)); - if (caster) + if(caster) printStat(EStat::MANA, CGI->generaltexth->allTexts[399], parent->info->stackNode->valOfBonuses(Bonus::CASTS)); - } - auto morale = new MoraleLuckBox(true, genRect(42, 42, 321, 110)); - morale->set(parent->info->stackNode); - auto luck = new MoraleLuckBox(false, genRect(42, 42, 375, 110)); - luck->set(parent->info->stackNode); + morale->set(parent->info->stackNode); + luck->set(parent->info->stackNode); + } if (showExp) { @@ -881,7 +886,7 @@ CStackWindow::CStackWindow(const CStack * stack, bool popup): info->stack = stack; info->stackNode = stack->base; info->creature = stack->type; - info->creatureCount = stack->count; + info->creatureCount = stack->getCount(); info->popupWindow = popup; init(); } diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 6e7f8ee0e..3076e9ec3 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -567,7 +567,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState) { return s->owner == player && vstd::contains(s->state, EBattleStackState::SUMMONED) - && !vstd::contains(s->state, EBattleStackState::CLONED); + && !s->isClone(); }); for(const CStack * s : stacks) { diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 16a3f925e..ad6b86be2 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -507,3 +507,15 @@ CGeneralTextHandler::CGeneralTextHandler() } } } + +int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t count) const +{ + if(textIndex == 0) + return 0; + else if(textIndex < 0) + return -textIndex; + else if(count == 1) + return textIndex; + else + return textIndex + 1; +} diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index 6c67a6aaf..68eecc441 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -37,7 +37,7 @@ namespace Unicode /// NOTE: usage of these functions should be avoided if possible std::string DLL_LINKAGE fromUnicode(const std::string & text); std::string DLL_LINKAGE fromUnicode(const std::string & text, const std::string & encoding); - + ///delete (amount) UTF characters from right DLL_LINKAGE void trimRight(std::string & text, const size_t amount = 1); }; @@ -103,7 +103,7 @@ public: std::vector overview;//text for Kingdom Overview window std::vector colors; //names of player colors ("red",...) std::vector capColors; //names of player colors with first letter capitalized ("Red",...) - std::vector turnDurations; //turn durations for pregame (1 Minute ... Unlimited) + std::vector turnDurations; //turn durations for pregame (1 Minute ... Unlimited) //towns std::vector tcommands, hcommands, fcommands; //texts for town screen, town hall screen and fort screen @@ -143,6 +143,8 @@ public: static void readToVector(std::string sourceName, std::vector &dest); + int32_t pluralText(const int32_t textIndex, const int32_t count) const; + CGeneralTextHandler(); CGeneralTextHandler(const CGeneralTextHandler&) = delete; CGeneralTextHandler operator=(const CGeneralTextHandler&) = delete; diff --git a/lib/CStack.cpp b/lib/CStack.cpp index 7d8c8126e..620a64365 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -9,74 +9,365 @@ */ #include "StdInc.h" #include "CStack.h" +#include "CGeneralTextHandler.h" #include "battle/BattleInfo.h" #include "spells/CSpellHandler.h" #include "CRandomGenerator.h" #include "NetPacks.h" -CStack::CStack(const CStackInstance *Base, PlayerColor O, int I, ui8 Side, SlotID S) - : base(Base), ID(I), owner(O), slot(S), side(Side), - counterAttacksPerformed(0),counterAttacksTotalCache(0), cloneID(-1), - firstHPleft(-1), position(), shots(0), casts(0), resurrected(0) +///CAmmo +CAmmo::CAmmo(const CStack * Owner, CSelector totalSelector): + CStackResource(Owner), totalProxy(Owner, totalSelector) +{ + +} + +int32_t CAmmo::available() const +{ + return total() - used; +} + +bool CAmmo::canUse(int32_t amount) const +{ + return available() - amount >= 0; +} + +void CAmmo::reset() +{ + used = 0; +} + +int32_t CAmmo::total() const +{ + return totalProxy->totalValue(); +} + +void CAmmo::use(int32_t amount) +{ + if(available() - amount < 0) + { + logGlobal->error("Stack ammo overuse"); + used += available(); + } + else + used += amount; +} + +///CShots +CShots::CShots(const CStack * Owner): + CAmmo(Owner, Selector::type(Bonus::SHOTS)) +{ + +} + +void CShots::use(int32_t amount) +{ + //don't remove ammo if we control a working ammo cart + bool hasAmmoCart = false; + + for(const CStack * st : owner->battle->stacks) + { + if(owner->battle->battleMatchOwner(st, owner, true) && st->getCreature()->idNumber == CreatureID::AMMO_CART && st->alive()) + { + hasAmmoCart = true; + break; + } + } + + if(!hasAmmoCart) + CAmmo::use(amount); +} + +///CCasts +CCasts::CCasts(const CStack * Owner): + CAmmo(Owner, Selector::type(Bonus::CASTS)) +{ + +} + +///CRetaliations +CRetaliations::CRetaliations(const CStack * Owner): + CAmmo(Owner, Selector::type(Bonus::ADDITIONAL_RETALIATION)), totalCache(0) +{ + +} + +int32_t CRetaliations::total() const +{ + //after dispell bonus should remain during current round + int32_t val = 1 + totalProxy->totalValue(); + vstd::amax(totalCache, val); + return totalCache; +} + +void CRetaliations::reset() +{ + CAmmo::reset(); + totalCache = 0; +} + +///CHealth +CHealth::CHealth(const IUnitHealthInfo * Owner): + owner(Owner) +{ + reset(); +} + +CHealth::CHealth(const CHealth & other): + owner(other.owner), + firstHPleft(other.firstHPleft), + fullUnits(other.fullUnits), + resurrected(other.resurrected) +{ + +} + +void CHealth::init() +{ + reset(); + fullUnits = owner->unitBaseAmount() > 1 ? owner->unitBaseAmount() - 1 : 0; + firstHPleft = owner->unitBaseAmount() > 0 ? owner->unitMaxHealth() : 0; +} + +void CHealth::addResurrected(int32_t amount) +{ + resurrected += amount; + vstd::amax(resurrected, 0); +} + +int64_t CHealth::available() const +{ + return static_cast(firstHPleft) + owner->unitMaxHealth() * fullUnits; +} + +int64_t CHealth::total() const +{ + return static_cast(owner->unitMaxHealth()) * owner->unitBaseAmount(); +} + +void CHealth::damage(int32_t & amount) +{ + const int32_t oldCount = getCount(); + + const bool withKills = amount >= firstHPleft; + + if(withKills) + { + int64_t totalHealth = available(); + if(amount > totalHealth) + amount = totalHealth; + totalHealth -= amount; + if(totalHealth <= 0) + { + fullUnits = 0; + firstHPleft = 0; + } + else + { + setFromTotal(totalHealth); + } + } + else + { + firstHPleft -= amount; + } + + addResurrected(getCount() - oldCount); +} + +void CHealth::heal(int32_t & amount, EHealLevel level, EHealPower power) +{ + const int32_t unitHealth = owner->unitMaxHealth(); + const int32_t oldCount = getCount(); + + int32_t maxHeal = std::numeric_limits::max(); + + switch(level) + { + case EHealLevel::HEAL: + maxHeal = std::max(0, unitHealth - firstHPleft); + break; + case EHealLevel::RESURRECT: + maxHeal = total() - available(); + break; + default: + assert(level == EHealLevel::OVERHEAL); + break; + } + + vstd::amax(maxHeal, 0); + vstd::abetween(amount, 0, maxHeal); + + if(amount == 0) + return; + + int64_t availableHealth = available(); + + availableHealth += amount; + setFromTotal(availableHealth); + + if(power == EHealPower::ONE_BATTLE) + addResurrected(getCount() - oldCount); + else + assert(power == EHealPower::PERMANENT); +} + +void CHealth::setFromTotal(const int64_t totalHealth) +{ + const int32_t unitHealth = owner->unitMaxHealth(); + firstHPleft = totalHealth % unitHealth; + fullUnits = totalHealth / unitHealth; + + if(firstHPleft == 0 && fullUnits > 1) + { + firstHPleft = unitHealth; + fullUnits -= 1; + } +} + +void CHealth::reset() +{ + fullUnits = 0; + firstHPleft = 0; + resurrected = 0; +} + +int32_t CHealth::getCount() const +{ + return fullUnits + (firstHPleft > 0 ? 1 : 0); +} + +int32_t CHealth::getFirstHPleft() const +{ + return firstHPleft; +} + +int32_t CHealth::getResurrected() const +{ + return resurrected; +} + +void CHealth::fromInfo(const CHealthInfo & info) +{ + firstHPleft = info.firstHPleft; + fullUnits = info.fullUnits; + resurrected = info.resurrected; +} + +void CHealth::toInfo(CHealthInfo & info) const +{ + info.firstHPleft = firstHPleft; + info.fullUnits = fullUnits; + info.resurrected = resurrected; +} + +void CHealth::takeResurrected() +{ + if(resurrected != 0) + { + int64_t totalHealth = available(); + + totalHealth -= resurrected * owner->unitMaxHealth(); + vstd::amax(totalHealth, 0); + setFromTotal(totalHealth); + resurrected = 0; + } +} + +///CStack +CStack::CStack(const CStackInstance * Base, PlayerColor O, int I, ui8 Side, SlotID S): + base(Base), ID(I), owner(O), slot(S), side(Side), + counterAttacks(this), shots(this), casts(this), health(this), cloneID(-1), + position() { assert(base); type = base->type; - count = baseAmount = base->count; + baseAmount = base->count; + health.init(); //??? setNodeType(STACK_BATTLE); } -CStack::CStack() + +CStack::CStack(): + counterAttacks(this), shots(this), casts(this), health(this) { init(); setNodeType(STACK_BATTLE); } -CStack::CStack(const CStackBasicDescriptor *stack, PlayerColor O, int I, ui8 Side, SlotID S) - : base(nullptr), ID(I), owner(O), slot(S), side(Side), - counterAttacksPerformed(0), counterAttacksTotalCache(0), cloneID(-1), - firstHPleft(-1), position(), shots(0), casts(0), resurrected(0) + +CStack::CStack(const CStackBasicDescriptor * stack, PlayerColor O, int I, ui8 Side, SlotID S): + base(nullptr), ID(I), owner(O), slot(S), side(Side), + counterAttacks(this), shots(this), casts(this), health(this), cloneID(-1), + position() { type = stack->type; - count = baseAmount = stack->count; + baseAmount = stack->count; + health.init(); //??? setNodeType(STACK_BATTLE); } +int32_t CStack::getKilled() const +{ + int32_t res = baseAmount - health.getCount() + health.getResurrected(); + vstd::amax(res, 0); + return res; +} + +int32_t CStack::getCount() const +{ + return health.getCount(); +} + +int32_t CStack::getFirstHPleft() const +{ + return health.getFirstHPleft(); +} + +const CCreature * CStack::getCreature() const +{ + return type; +} + void CStack::init() { base = nullptr; type = nullptr; ID = -1; - count = baseAmount = -1; - firstHPleft = -1; + baseAmount = -1; owner = PlayerColor::NEUTRAL; slot = SlotID(255); side = 1; position = BattleHex(); - counterAttacksPerformed = 0; - counterAttacksTotalCache = 0; cloneID = -1; - - shots = 0; - casts = 0; - resurrected = 0; } -void CStack::postInit() +void CStack::localInit(BattleInfo * battleInfo) { - assert(type); - assert(getParentNodes().size()); - - firstHPleft = MaxHealth(); - shots = getCreature()->valOfBonuses(Bonus::SHOTS); - counterAttacksPerformed = 0; - counterAttacksTotalCache = 0; - casts = valOfBonuses(Bonus::CASTS); - resurrected = 0; + battle = battleInfo; cloneID = -1; + assert(type); + + exportBonuses(); + if(base) //stack originating from "real" stack in garrison -> attach to it + { + attachTo(const_cast(base)); + } + else //attach directly to obj to which stack belongs and creature type + { + CArmedInstance * army = battle->battleGetArmyObject(side); + attachTo(army); + attachTo(const_cast(type)); + } + + shots.reset(); + counterAttacks.reset(); + casts.reset(); + health.init(); } ui32 CStack::level() const { - if (base) + if(base) return base->getLevel(); //creatture or commander else return std::max(1, (int)getCreature()->level); //war machine, clone etc @@ -85,19 +376,19 @@ ui32 CStack::level() const si32 CStack::magicResistance() const { si32 magicResistance; - if (base) //TODO: make war machines receive aura of magic resistance + if(base) //TODO: make war machines receive aura of magic resistance { magicResistance = base->magicResistance(); int auraBonus = 0; - for (const CStack * stack : base->armyObj->battle-> batteAdjacentCreatures(this)) - { - if (stack->owner == owner) + for(const CStack * stack : base->armyObj->battle-> batteAdjacentCreatures(this)) { - vstd::amax(auraBonus, stack->valOfBonuses(Bonus::SPELL_RESISTANCE_AURA)); //max value + if(stack->owner == owner) + { + vstd::amax(auraBonus, stack->valOfBonuses(Bonus::SPELL_RESISTANCE_AURA)); //max value + } } - } magicResistance += auraBonus; - vstd::amin (magicResistance, 100); + vstd::amin(magicResistance, 100); } else magicResistance = type->magicResistance(); @@ -106,18 +397,38 @@ si32 CStack::magicResistance() const bool CStack::willMove(int turn /*= 0*/) const { - return ( turn ? true : !vstd::contains(state, EBattleStackState::DEFENDING) ) - && !moved(turn) - && canMove(turn); + return (turn ? true : !vstd::contains(state, EBattleStackState::DEFENDING)) + && !moved(turn) + && canMove(turn); } -bool CStack::canMove( int turn /*= 0*/ ) const +bool CStack::canMove(int turn /*= 0*/) const { return alive() - && !hasBonus(Selector::type(Bonus::NOT_ACTIVE).And(Selector::turns(turn))); //eg. Ammo Cart or blinded creature + && !hasBonus(Selector::type(Bonus::NOT_ACTIVE).And(Selector::turns(turn))); //eg. Ammo Cart or blinded creature } -bool CStack::moved( int turn /*= 0*/ ) const +bool CStack::canCast() const +{ + return casts.canUse(1);//do not check specific cast abilities here +} + +bool CStack::isCaster() const +{ + return casts.total() > 0;//do not check specific cast abilities here +} + +bool CStack::canShoot() const +{ + return shots.canUse(1) && hasBonusOfType(Bonus::SHOOTER); +} + +bool CStack::isShooter() const +{ + return shots.total() > 0 && hasBonusOfType(Bonus::SHOOTER); +} + +bool CStack::moved(int turn /*= 0*/) const { if(!turn) return vstd::contains(state, EBattleStackState::MOVED); @@ -197,26 +508,27 @@ std::vector CStack::getSurroundingHexes(BattleHex attackerPos) const { const int WN = GameConstants::BFIELD_WIDTH; if(side == BattleSide::ATTACKER) - { //position is equal to front hex - BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN+2 : WN+1 ), hexes); - BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN+1 : WN ), hexes); - BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN : WN-1 ), hexes); + { + //position is equal to front hex + BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN + 2 : WN + 1), hexes); + BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN + 1 : WN), hexes); + BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN : WN - 1), hexes); BattleHex::checkAndPush(hex - 2, hexes); BattleHex::checkAndPush(hex + 1, hexes); - BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN-2 : WN-1 ), hexes); - BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN-1 : WN ), hexes); - BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN : WN+1 ), hexes); + BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN - 2 : WN - 1), hexes); + BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN - 1 : WN), hexes); + BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN : WN + 1), hexes); } else { - BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN+1 : WN ), hexes); - BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN : WN-1 ), hexes); - BattleHex::checkAndPush(hex - ( (hex/WN)%2 ? WN-1 : WN-2 ), hexes); + BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN + 1 : WN), hexes); + BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN : WN - 1), hexes); + BattleHex::checkAndPush(hex - ((hex / WN) % 2 ? WN - 1 : WN - 2), hexes); BattleHex::checkAndPush(hex + 2, hexes); BattleHex::checkAndPush(hex - 1, hexes); - BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN-1 : WN ), hexes); - BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN : WN+1 ), hexes); - BattleHex::checkAndPush(hex + ( (hex/WN)%2 ? WN+1 : WN+2 ), hexes); + BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN - 1 : WN), hexes); + BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN : WN + 1), hexes); + BattleHex::checkAndPush(hex + ((hex / WN) % 2 ? WN + 1 : WN + 2), hexes); } return hexes; } @@ -248,15 +560,15 @@ std::vector CStack::activeSpells() const std::stringstream cachingStr; cachingStr << "!type_" << Bonus::NONE << "source_" << Bonus::SPELL_EFFECT; CSelector selector = Selector::sourceType(Bonus::SPELL_EFFECT) - .And(CSelector([](const Bonus *b)->bool - { - return b->type != Bonus::NONE; - })); + .And(CSelector([](const Bonus * b)->bool + { + return b->type != Bonus::NONE; + })); TBonusListPtr spellEffects = getBonuses(selector, Selector::all, cachingStr.str()); for(const std::shared_ptr it : *spellEffects) { - if (!vstd::contains(ret, it->sid)) //do not duplicate spells with multiple effects + if(!vstd::contains(ret, it->sid)) //do not duplicate spells with multiple effects ret.push_back(it->sid); } @@ -273,22 +585,17 @@ const CGHeroInstance * CStack::getMyHero() const if(base) return dynamic_cast(base->armyObj); else //we are attached directly? - for(const CBonusSystemNode *n : getParentNodes()) + for(const CBonusSystemNode * n : getParentNodes()) if(n->getNodeType() == HERO) return dynamic_cast(n); return nullptr; } -ui32 CStack::totalHealth() const -{ - return ((count > 0) ? MaxHealth() * (count-1) : 0) + firstHPleft;//do not hide possible invalid firstHPleft for dead stack -} - std::string CStack::nodeName() const { std::ostringstream oss; - oss << "Battle stack [" << ID << "]: " << count << " creatures of "; + oss << "Battle stack [" << ID << "]: " << health.getCount() << " creatures of "; if(type) oss << type->namePl; else @@ -300,65 +607,79 @@ std::string CStack::nodeName() const return oss.str(); } -std::pair CStack::countKilledByAttack(int damageReceived) const +CHealth CStack::healthAfterAttacked(int32_t & damage) const { - int newRemainingHP = 0; - int killedCount = damageReceived / MaxHealth(); - unsigned damageFirst = damageReceived % MaxHealth(); + return healthAfterAttacked(damage, health); +} - if (damageReceived && vstd::contains(state, EBattleStackState::CLONED)) // block ability should not kill clone (0 damage) +CHealth CStack::healthAfterAttacked(int32_t & damage, const CHealth & customHealth) const +{ + CHealth res = customHealth; + + if(isClone()) { - killedCount = count; + // block ability should not kill clone (0 damage) + if(damage > 0) + { + damage = 1;//??? what should be actual damage against clone? + res.reset(); + } } else { - if( firstHPleft <= damageFirst ) - { - killedCount++; - newRemainingHP = firstHPleft + MaxHealth() - damageFirst; - } - else - { - newRemainingHP = firstHPleft - damageFirst; - } + res.damage(damage); } - if(killedCount == count) - newRemainingHP = 0; - - return std::make_pair(killedCount, newRemainingHP); + return res; } -void CStack::prepareAttacked(BattleStackAttacked &bsa, CRandomGenerator & rand, boost::optional customCount /*= boost::none*/) const +CHealth CStack::healthAfterHealed(int32_t & toHeal, EHealLevel level, EHealPower power) const { - auto afterAttack = countKilledByAttack(bsa.damageAmount); + CHealth res = health; - bsa.killedAmount = afterAttack.first; - bsa.newHP = afterAttack.second; + if(level == EHealLevel::HEAL && power == EHealPower::ONE_BATTLE) + logGlobal->error("Heal for one battle does not make sense", nodeName(), toHeal); + else if(isClone()) + logGlobal->error("Attempt to heal clone: %s for %d HP", nodeName(), toHeal); + else + res.heal(toHeal, level, power); + return res; +} - if(bsa.damageAmount && vstd::contains(state, EBattleStackState::CLONED)) // block ability should not kill clone (0 damage) +void CStack::prepareAttacked(BattleStackAttacked & bsa, CRandomGenerator & rand) const +{ + prepareAttacked(bsa, rand, health); +} + +void CStack::prepareAttacked(BattleStackAttacked & bsa, CRandomGenerator & rand, const CHealth & customHealth) const +{ + CHealth afterAttack = healthAfterAttacked(bsa.damageAmount, customHealth); + + bsa.killedAmount = customHealth.getCount() - afterAttack.getCount(); + afterAttack.toInfo(bsa.newHealth); + bsa.newHealth.stackId = ID; + bsa.newHealth.delta = -bsa.damageAmount; + + if(afterAttack.available() <= 0 && isClone()) { bsa.flags |= BattleStackAttacked::CLONE_KILLED; return; // no rebirth I believe } - const int countToUse = customCount ? *customCount : count; - - if(countToUse <= bsa.killedAmount) //stack killed + if(afterAttack.available() <= 0) //stack killed { - bsa.newAmount = 0; bsa.flags |= BattleStackAttacked::KILLED; - bsa.killedAmount = countToUse; //we cannot kill more creatures than we have int resurrectFactor = valOfBonuses(Bonus::REBIRTH); - if(resurrectFactor > 0 && casts) //there must be casts left + if(resurrectFactor > 0 && canCast()) //there must be casts left { - int resurrectedStackCount = base->count * resurrectFactor / 100; + int resurrectedStackCount = baseAmount * resurrectFactor / 100; // last stack has proportional chance to rebirth - auto diff = base->count * resurrectFactor / 100.0 - resurrectedStackCount; - if (diff > rand.nextDouble(0, 0.99)) + //FIXME: diff is always 0 + auto diff = baseAmount * resurrectFactor / 100.0 - resurrectedStackCount; + if(diff > rand.nextDouble(0, 0.99)) { resurrectedStackCount += 1; } @@ -372,64 +693,45 @@ void CStack::prepareAttacked(BattleStackAttacked &bsa, CRandomGenerator & rand, if(resurrectedStackCount > 0) { bsa.flags |= BattleStackAttacked::REBIRTH; - bsa.newAmount = resurrectedStackCount; //risky? - bsa.newHP = MaxHealth(); //resore full health + //TODO: use StackHealedOrResurrected + bsa.newHealth.firstHPleft = MaxHealth(); + bsa.newHealth.fullUnits = resurrectedStackCount - 1; + bsa.newHealth.resurrected = 0; //TODO: add one-battle rebirth? } } } - else - { - bsa.newAmount = countToUse - bsa.killedAmount; - } } bool CStack::isMeleeAttackPossible(const CStack * attacker, const CStack * defender, BattleHex attackerPos /*= BattleHex::INVALID*/, BattleHex defenderPos /*= BattleHex::INVALID*/) { - if (!attackerPos.isValid()) - { + if(!attackerPos.isValid()) attackerPos = attacker->position; - } - if (!defenderPos.isValid()) - { + if(!defenderPos.isValid()) defenderPos = defender->position; - } return - (BattleHex::mutualPosition(attackerPos, defenderPos) >= 0) //front <=> front - || (attacker->doubleWide() //back <=> front - && BattleHex::mutualPosition(attackerPos + (attacker->side == BattleSide::ATTACKER ? -1 : 1), defenderPos) >= 0) - || (defender->doubleWide() //front <=> back - && BattleHex::mutualPosition(attackerPos, defenderPos + (defender->side == BattleSide::ATTACKER ? -1 : 1)) >= 0) + (BattleHex::mutualPosition(attackerPos, defenderPos) >= 0)//front <=> front + || (attacker->doubleWide()//back <=> front + && BattleHex::mutualPosition(attackerPos + (attacker->side == BattleSide::ATTACKER ? -1 : 1), defenderPos) >= 0) + || (defender->doubleWide()//front <=> back + && BattleHex::mutualPosition(attackerPos, defenderPos + (defender->side == BattleSide::ATTACKER ? -1 : 1)) >= 0) || (defender->doubleWide() && attacker->doubleWide()//back <=> back - && BattleHex::mutualPosition(attackerPos + (attacker->side == BattleSide::ATTACKER ? -1 : 1), defenderPos + (defender->side == BattleSide::ATTACKER ? -1 : 1)) >= 0); + && BattleHex::mutualPosition(attackerPos + (attacker->side == BattleSide::ATTACKER ? -1 : 1), defenderPos + (defender->side == BattleSide::ATTACKER ? -1 : 1)) >= 0); } -bool CStack::ableToRetaliate() const //FIXME: crash after clone is killed +bool CStack::ableToRetaliate() const { return alive() - && (counterAttacksPerformed < counterAttacksTotal() || hasBonusOfType(Bonus::UNLIMITED_RETALIATIONS)) - && !hasBonusOfType(Bonus::SIEGE_WEAPON) - && !hasBonusOfType(Bonus::HYPNOTIZED) - && !hasBonusOfType(Bonus::NO_RETALIATION); -} - -ui8 CStack::counterAttacksTotal() const -{ - //after dispell bonus should remain during current round - ui8 val = 1 + valOfBonuses(Bonus::ADDITIONAL_RETALIATION); - vstd::amax(counterAttacksTotalCache, val); - return counterAttacksTotalCache; -} - -si8 CStack::counterAttacksRemaining() const -{ - return counterAttacksTotal() - counterAttacksPerformed; + && (counterAttacks.canUse() || hasBonusOfType(Bonus::UNLIMITED_RETALIATIONS)) + && !hasBonusOfType(Bonus::SIEGE_WEAPON) + && !hasBonusOfType(Bonus::HYPNOTIZED) + && !hasBonusOfType(Bonus::NO_RETALIATION); } std::string CStack::getName() const { - return (count > 1) ? type->namePl : type->nameSing; //War machines can't use base + return (health.getCount() == 1) ? type->nameSing : type->namePl; //War machines can't use base } bool CStack::isValidTarget(bool allowDead/* = false*/) const @@ -442,9 +744,14 @@ bool CStack::isDead() const return !alive() && !isGhost(); } +bool CStack::isClone() const +{ + return vstd::contains(state, EBattleStackState::CLONED); +} + bool CStack::isGhost() const { - return vstd::contains(state,EBattleStackState::GHOST); + return vstd::contains(state, EBattleStackState::GHOST); } bool CStack::isTurret() const @@ -454,9 +761,9 @@ bool CStack::isTurret() const bool CStack::canBeHealed() const { - return firstHPleft < MaxHealth() - && isValidTarget() - && !hasBonusOfType(Bonus::SIEGE_WEAPON); + return getFirstHPleft() < MaxHealth() + && isValidTarget() + && !hasBonusOfType(Bonus::SIEGE_WEAPON); } void CStack::makeGhost() @@ -467,26 +774,13 @@ void CStack::makeGhost() bool CStack::alive() const //determines if stack is alive { - return vstd::contains(state,EBattleStackState::ALIVE); -} - -ui32 CStack::calculateHealedHealthPoints(ui32 toHeal, const bool resurrect) const -{ - if(!resurrect && !alive()) - { - logGlobal->warnStream() <<"Attempt to heal corpse detected."; - return 0; - } - - return std::min(toHeal, MaxHealth() - firstHPleft + (resurrect ? (baseAmount - count) * MaxHealth() : 0)); + return vstd::contains(state, EBattleStackState::ALIVE); } ui8 CStack::getSpellSchoolLevel(const CSpell * spell, int * outSelectedSchool) const { int skill = valOfBonuses(Selector::typeSubtype(Bonus::SPELLCASTER, spell->id)); - vstd::abetween(skill, 0, 3); - return skill; } @@ -503,37 +797,91 @@ int CStack::getEffectLevel(const CSpell * spell) const int CStack::getEffectPower(const CSpell * spell) const { - return valOfBonuses(Bonus::CREATURE_SPELL_POWER) * count / 100; + return valOfBonuses(Bonus::CREATURE_SPELL_POWER) * health.getCount() / 100; } int CStack::getEnchantPower(const CSpell * spell) const { int res = valOfBonuses(Bonus::CREATURE_ENCHANT_POWER); - if(res<=0) + if(res <= 0) res = 3;//default for creatures return res; } int CStack::getEffectValue(const CSpell * spell) const { - return valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spell->id.toEnum()) * count; + return valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spell->id.toEnum()) * health.getCount(); } const PlayerColor CStack::getOwner() const { - return owner; + return battle->battleGetOwner(this); } void CStack::getCasterName(MetaString & text) const { //always plural name in case of spell cast. - text.addReplacement(MetaString::CRE_PL_NAMES, type->idNumber.num); + addNameReplacement(text, true); } -void CStack::getCastDescription(const CSpell * spell, const std::vector & attacked, MetaString & text) const +void CStack::getCastDescription(const CSpell * spell, const std::vector & attacked, MetaString & text) const { text.addTxt(MetaString::GENERAL_TXT, 565);//The %s casts %s //todo: use text 566 for single creature getCasterName(text); text.addReplacement(MetaString::SPELL_NAME, spell->id.toEnum()); } + +int32_t CStack::unitMaxHealth() const +{ + return MaxHealth(); +} + +int32_t CStack::unitBaseAmount() const +{ + return baseAmount; +} + +void CStack::addText(MetaString & text, ui8 type, int32_t serial, const boost::logic::tribool & plural) const +{ + if(boost::logic::indeterminate(plural)) + serial = VLC->generaltexth->pluralText(serial, health.getCount()); + else if(plural) + serial = VLC->generaltexth->pluralText(serial, 2); + else + serial = VLC->generaltexth->pluralText(serial, 1); + + text.addTxt(type, serial); +} + +void CStack::addNameReplacement(MetaString & text, const boost::logic::tribool & plural) const +{ + if(boost::logic::indeterminate(plural)) + text.addCreReplacement(type->idNumber, health.getCount()); + else if(plural) + text.addReplacement(MetaString::CRE_PL_NAMES, type->idNumber.num); + else + text.addReplacement(MetaString::CRE_SING_NAMES, type->idNumber.num); +} + +std::string CStack::formatGeneralMessage(const int32_t baseTextId) const +{ + const int32_t textId = VLC->generaltexth->pluralText(baseTextId, health.getCount()); + + MetaString text; + text.addTxt(MetaString::GENERAL_TXT, textId); + text.addCreReplacement(type->idNumber, health.getCount()); + + return text.toString(); +} + +void CStack::setHealth(const CHealthInfo & value) +{ + health.reset(); + health.fromInfo(value); +} + +void CStack::setHealth(const CHealth & value) +{ + health = value; +} diff --git a/lib/CStack.h b/lib/CStack.h index b5309d5f2..17a40f2af 100644 --- a/lib/CStack.h +++ b/lib/CStack.h @@ -7,64 +7,186 @@ * Full text of license available in license.txt file, in main folder * */ + #pragma once #include "battle/BattleHex.h" #include "CCreatureHandler.h" #include "mapObjects/CGHeroInstance.h" // for commander serialization struct BattleStackAttacked; +struct BattleInfo; +class CStack; +class CHealthInfo; -class DLL_LINKAGE CStack : public CBonusSystemNode, public CStackBasicDescriptor, public ISpellCaster +template +class DLL_LINKAGE CStackResource { public: - const CStackInstance *base; //garrison slot from which stack originates (nullptr for war machines, summoned cres, etc) + CStackResource(const CStack * Owner): + owner(Owner) + { + reset(); + } + + virtual void reset() + { + used = 0; + }; + +protected: + const CStack * owner; + Quantity used; +}; + +class DLL_LINKAGE CAmmo : public CStackResource +{ +public: + CAmmo(const CStack * Owner, CSelector totalSelector); + + int32_t available() const; + bool canUse(int32_t amount = 1) const; + virtual void reset() override; + virtual int32_t total() const; + virtual void use(int32_t amount = 1); + + template void serialize(Handler & h, const int version) + { + if(!h.saving) + reset(); + h & used; + } +protected: + CBonusProxy totalProxy; +}; + +class DLL_LINKAGE CShots : public CAmmo +{ +public: + CShots(const CStack * Owner); + void use(int32_t amount = 1) override; +}; + +class DLL_LINKAGE CCasts : public CAmmo +{ +public: + CCasts(const CStack * Owner); +}; + +class DLL_LINKAGE CRetaliations : public CAmmo +{ +public: + CRetaliations(const CStack * Owner); + int32_t total() const override; + void reset() override; +private: + mutable int32_t totalCache; +}; + +class DLL_LINKAGE IUnitHealthInfo +{ +public: + virtual int32_t unitMaxHealth() const = 0; + virtual int32_t unitBaseAmount() const = 0; +}; + +class DLL_LINKAGE CHealth +{ +public: + CHealth(const IUnitHealthInfo * Owner); + CHealth(const CHealth & other); + + void init(); + void reset(); + + void damage(int32_t & amount); + void heal(int32_t & amount, EHealLevel level, EHealPower power); + + int32_t getCount() const; + int32_t getFirstHPleft() const; + int32_t getResurrected() const; + + int64_t available() const; + int64_t total() const; + + void toInfo(CHealthInfo & info) const; + void fromInfo(const CHealthInfo & info); + + void takeResurrected(); + + template void serialize(Handler & h, const int version) + { + if(!h.saving) + reset(); + h & firstHPleft & fullUnits & resurrected; + } +private: + void addResurrected(int32_t amount); + void setFromTotal(const int64_t totalHealth); + const IUnitHealthInfo * owner; + + int32_t firstHPleft; + int32_t fullUnits; + int32_t resurrected; +}; + +class DLL_LINKAGE CStack : public CBonusSystemNode, public ISpellCaster, public IUnitHealthInfo +{ +public: + const CStackInstance * base; //garrison slot from which stack originates (nullptr for war machines, summoned cres, etc) ui32 ID; //unique ID of stack ui32 baseAmount; - ui32 firstHPleft; //HP of first creature in stack - PlayerColor owner; //owner - player colour (255 for neutrals) + const CCreature * type; + + PlayerColor owner; //owner - player color (255 for neutrals) SlotID slot; //slot - position in garrison (may be 255 for neutrals/called creatures) ui8 side; BattleHex position; //position on battlefield; -2 - keep, -3 - lower tower, -4 - upper tower - ///how many times this stack has been counterattacked this round - ui8 counterAttacksPerformed; - ///cached total count of counterattacks; should be cleared each round;do not serialize - mutable ui8 counterAttacksTotalCache; - si16 shots; //how many shots left - ui8 casts; //how many casts left - TQuantity resurrected; // these units will be taken back after battle is over + + CRetaliations counterAttacks; + CShots shots; + CCasts casts; + CHealth health; + ///id of alive clone of this stack clone if any si32 cloneID; std::set state; - //overrides - const CCreature* getCreature() const {return type;} - CStack(const CStackInstance *base, PlayerColor O, int I, ui8 Side, SlotID S); //c-tor - CStack(const CStackBasicDescriptor *stack, PlayerColor O, int I, ui8 Side, SlotID S = SlotID(255)); //c-tor + CStack(const CStackInstance * base, PlayerColor O, int I, ui8 Side, SlotID S); //c-tor + CStack(const CStackBasicDescriptor * stack, PlayerColor O, int I, ui8 Side, SlotID S = SlotID(255)); //c-tor CStack(); //c-tor ~CStack(); + + int32_t getKilled() const; + int32_t getCount() const; + int32_t getFirstHPleft() const; + const CCreature * getCreature() const; + std::string nodeName() const override; void init(); //set initial (invalid) values - void postInit(); //used to finish initialization when inheriting creature parameters is working + void localInit(BattleInfo * battleInfo); std::string getName() const; //plural or singular bool willMove(int turn = 0) const; //if stack has remaining move this turn bool ableToRetaliate() const; //if stack can retaliate after attacked - ///how many times this stack can counterattack in one round - ui8 counterAttacksTotal() const; - ///how many times this stack can counterattack in one round more - si8 counterAttacksRemaining() const; + bool moved(int turn = 0) const; //if stack was already moved this turn bool waited(int turn = 0) const; + + bool canCast() const; + bool isCaster() const; + bool canMove(int turn = 0) const; //if stack can move + + bool canShoot() const; + bool isShooter() const; + bool canBeHealed() const; //for first aid tent - only harmed stacks that are not war machines - ///returns actual heal value based on internal state - ui32 calculateHealedHealthPoints(ui32 toHeal, const bool resurrect) const; + ui32 level() const; si32 magicResistance() const override; //include aura of resistance std::vector activeSpells() const; //returns vector of active spell IDs sorted by time of cast - const CGHeroInstance *getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise - ui32 totalHealth() const; // total health for all creatures in stack; + const CGHeroInstance * getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise static bool isMeleeAttackPossible(const CStack * attacker, const CStack * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); @@ -79,11 +201,17 @@ public: BattleHex::EDir destShiftDir() const; - std::pair countKilledByAttack(int damageReceived) const; //returns pair - void prepareAttacked(BattleStackAttacked &bsa, CRandomGenerator & rand, boost::optional customCount = boost::none) const; //requires bsa.damageAmout filled + CHealth healthAfterAttacked(int32_t & damage) const; + CHealth healthAfterAttacked(int32_t & damage, const CHealth & customHealth) const; + + CHealth healthAfterHealed(int32_t & toHeal, EHealLevel level, EHealPower power) const; + + void prepareAttacked(BattleStackAttacked & bsa, CRandomGenerator & rand) const; //requires bsa.damageAmout filled + void prepareAttacked(BattleStackAttacked & bsa, CRandomGenerator & rand, const CHealth & customHealth) const; //requires bsa.damageAmout filled ///ISpellCaster - ui8 getSpellSchoolLevel(const CSpell * spell, int *outSelectedSchool = nullptr) const override; + + ui8 getSpellSchoolLevel(const CSpell * spell, int * outSelectedSchool = nullptr) const override; ui32 getSpellBonus(const CSpell * spell, ui32 base, const CStack * affectedStack) const override; ///default spell school level for effect calculation @@ -99,23 +227,36 @@ public: int getEffectValue(const CSpell * spell) const override; const PlayerColor getOwner() const override; - void getCasterName(MetaString & text) const override; - void getCastDescription(const CSpell * spell, const std::vector & attacked, MetaString & text) const override; + ///IUnitHealthInfo + + int32_t unitMaxHealth() const override; + int32_t unitBaseAmount() const override; + + ///MetaStrings + + void addText(MetaString & text, ui8 type, int32_t serial, const boost::logic::tribool & plural = boost::logic::indeterminate) const; + void addNameReplacement(MetaString & text, const boost::logic::tribool & plural = boost::logic::indeterminate) const; + std::string formatGeneralMessage(const int32_t baseTextId) const; + + ///Non const API for NetPacks + ///stack will be ghost in next battle state update void makeGhost(); + void setHealth(const CHealthInfo & value); + void setHealth(const CHealth & value); - template void serialize(Handler &h, const int version) + template void serialize(Handler & h, const int version) { assert(isIndependentNode()); - h & static_cast(*this); - h & static_cast(*this); - h & ID & baseAmount & firstHPleft & owner & slot & side & position & state & counterAttacksPerformed - & shots & casts & count & resurrected; + h & static_cast(*this); + h & type; + h & ID & baseAmount & owner & slot & side & position & state; + h & shots & casts & counterAttacks & health; - const CArmedInstance *army = (base ? base->armyObj : nullptr); + const CArmedInstance * army = (base ? base->armyObj : nullptr); SlotID extSlot = (base ? base->armyObj->findStack(base) : SlotID()); if(h.saving) @@ -128,7 +269,7 @@ public: if(extSlot == SlotID::COMMANDER_SLOT_PLACEHOLDER) { auto hero = dynamic_cast(army); - assert (hero); + assert(hero); base = hero->commander; } else if(slot == SlotID::SUMMONED_SLOT_PLACEHOLDER || slot == SlotID::ARROW_TOWERS_SLOT || slot == SlotID::WAR_MACHINES_SLOT) @@ -150,8 +291,13 @@ public: } bool alive() const; + bool isClone() const; bool isDead() const; bool isGhost() const; //determines if stack was removed bool isValidTarget(bool allowDead = false) const; //non-turret non-ghost stacks (can be attacked or be object of magic effect) bool isTurret() const; + + friend class CShots; //for BattleInfo access +private: + const BattleInfo * battle; //do not serialize }; diff --git a/lib/GameConstants.h b/lib/GameConstants.h index b86472791..9218f7f67 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -1089,6 +1089,19 @@ enum class EMetaclass: ui8 RESOURCE }; +enum class EHealLevel: ui8 +{ + HEAL, + RESURRECT, + OVERHEAL +}; + +enum class EHealPower : ui8 +{ + ONE_BATTLE, + PERMANENT +}; + // Typedef declarations typedef ui8 TFaction; typedef si64 TExpType; diff --git a/lib/HeroBonus.cpp b/lib/HeroBonus.cpp index 06a4e1388..0ea23c2a7 100644 --- a/lib/HeroBonus.cpp +++ b/lib/HeroBonus.cpp @@ -79,6 +79,29 @@ const std::map bonusPropagatorMap = {"GLOBAL_EFFECT", std::make_shared(CBonusSystemNode::GLOBAL_EFFECTS)} }; //untested +///CBonusProxy +CBonusProxy::CBonusProxy(const IBonusBearer * Target, CSelector Selector): + cachedLast(0), target(Target), selector(Selector), data() +{ + +} + +TBonusListPtr CBonusProxy::get() const +{ + if(CBonusSystemNode::treeChanged != cachedLast || !data) + { + //TODO: support limiters + data = target->getAllBonuses(selector, nullptr); + data->eliminateDuplicates(); + cachedLast = CBonusSystemNode::treeChanged; + } + return data; +} + +const BonusList * CBonusProxy::operator->() const +{ + return get().get(); +} #define BONUS_LOG_LINE(x) logBonus->traceStream() << x diff --git a/lib/HeroBonus.h b/lib/HeroBonus.h index fcf3ac167..6b452f785 100644 --- a/lib/HeroBonus.h +++ b/lib/HeroBonus.h @@ -13,6 +13,7 @@ class CCreature; struct Bonus; +class IBonusBearer; class CBonusSystemNode; class ILimiter; class IPropagator; @@ -63,7 +64,20 @@ public: } }; +class DLL_LINKAGE CBonusProxy : public boost::noncopyable +{ +public: + CBonusProxy(const IBonusBearer * Target, CSelector Selector); + TBonusListPtr get() const; + + const BonusList * operator->() const; +private: + mutable int cachedLast; + const IBonusBearer * target; + CSelector selector; + mutable TBonusListPtr data; +}; #define BONUS_TREE_DESERIALIZATION_FIX if(!h.saving && h.smartPointerSerialization) deserializationFix(); @@ -684,6 +698,8 @@ public: BONUS_TREE_DESERIALIZATION_FIX //h & parents & children; } + + friend class CBonusProxy; }; namespace NBonus diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 68dd702a2..d42c54d34 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1315,34 +1315,22 @@ struct BattleStackMoved : public CPackForClient struct StacksHealedOrResurrected : public CPackForClient { StacksHealedOrResurrected() - :lifeDrain(false), tentHealing(false), drainedFrom(0), cure(false), canOverheal(false) + :lifeDrain(false), tentHealing(false), drainedFrom(0), cure(false) {} DLL_LINKAGE void applyGs(CGameState *gs); void applyCl(CClient *cl); - struct HealInfo - { - ui32 stackID; - ui32 healedHP; - bool lowLevelResurrection; //in case this stack is resurrected by this heal, it will be marked as SUMMONED //TODO: replace with separate counter - template void serialize(Handler &h, const int version) - { - h & stackID & healedHP & lowLevelResurrection; - } - }; - - std::vector healedStacks; + std::vector healedStacks; bool lifeDrain; //if true, this heal is an effect of life drain or soul steal bool tentHealing; //if true, than it's healing via First Aid Tent si32 drainedFrom; //if life drain or soul steal - then stack life was drain from, if tentHealing - stack that is a healer bool cure; //archangel cast also remove negative effects - bool canOverheal; //to allow healing over initial stack amount template void serialize(Handler &h, const int version) { h & healedStacks & lifeDrain & tentHealing & drainedFrom; - h & cure & canOverheal; + h & cure; } }; @@ -1350,7 +1338,8 @@ struct BattleStackAttacked : public CPackForClient { BattleStackAttacked(): stackAttacked(0), attackerID(0), - newAmount(0), newHP(0), killedAmount(0), damageAmount(0), + killedAmount(0), damageAmount(0), + newHealth(), flags(0), effect(0), spellID(SpellID::NONE) {}; void applyFirstCl(CClient * cl); @@ -1358,7 +1347,9 @@ struct BattleStackAttacked : public CPackForClient DLL_LINKAGE void applyGs(CGameState *gs); ui32 stackAttacked, attackerID; - ui32 newAmount, newHP, killedAmount, damageAmount; + ui32 killedAmount; + si32 damageAmount; + CHealthInfo newHealth; enum EFlags {KILLED = 1, EFFECT = 2/*deprecated */, SECONDARY = 4, REBIRTH = 8, CLONE_KILLED = 16, SPELL_EFFECT = 32 /*, BONUS_EFFECT = 64 */}; ui32 flags; //uses EFlags (above) ui32 effect; //set only if flag EFFECT is set @@ -1396,7 +1387,7 @@ struct BattleStackAttacked : public CPackForClient } template void serialize(Handler &h, const int version) { - h & stackAttacked & attackerID & newAmount & newHP & flags & killedAmount & damageAmount & effect + h & stackAttacked & attackerID & newHealth & flags & killedAmount & damageAmount & effect & healedStacks; h & spellID; } @@ -1540,10 +1531,12 @@ struct SetStackEffect : public CPackForClient std::vector cumulativeEffects; //bonuses to apply std::vector > cumulativeUniqueBonuses; //bonuses per single stack + std::vector battleLog; template void serialize(Handler &h, const int version) { h & stacks & effect & uniqueBonuses; h & cumulativeEffects & cumulativeUniqueBonuses; + h & battleLog; } }; diff --git a/lib/NetPacksBase.h b/lib/NetPacksBase.h index 0be195786..034bb9102 100644 --- a/lib/NetPacksBase.h +++ b/lib/NetPacksBase.h @@ -188,3 +188,22 @@ struct ArtifactLocation h & artHolder & slot; } }; + +class CHealthInfo +{ +public: + CHealthInfo(): + stackId(0), delta(0), firstHPleft(0), fullUnits(0), resurrected(0) + { + } + uint32_t stackId; + int32_t delta; + int32_t firstHPleft; + int32_t fullUnits; + int32_t resurrected; + + template void serialize(Handler & h, const int version) + { + h & stackId & delta & firstHPleft & fullUnits & resurrected; + } +}; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index d39672acd..16806e588 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1247,12 +1247,11 @@ DLL_LINKAGE void BattleNextRound::applyGs(CGameState *gs) s->state -= EBattleStackState::HAD_MORALE; s->state -= EBattleStackState::FEAR; s->state -= EBattleStackState::DRAINED_MANA; - s->counterAttacksPerformed = 0; - s->counterAttacksTotalCache = 0; + s->counterAttacks.reset(); // new turn effects s->updateBonuses(Bonus::NTurns); - if(s->alive() && vstd::contains(s->state, EBattleStackState::CLONED)) + if(s->alive() && s->isClone()) { //cloned stack has special lifetime marker //check it after bonuses updated in battleTurnPassed() @@ -1280,36 +1279,40 @@ DLL_LINKAGE void BattleSetActiveStack::applyGs(CGameState *gs) DLL_LINKAGE void BattleTriggerEffect::applyGs(CGameState *gs) { - CStack *st = gs->curB->getStack(stackID); - switch (effect) + CStack * st = gs->curB->getStack(stackID); + assert(st); + switch(effect) { - case Bonus::HP_REGENERATION: - st->firstHPleft += val; - vstd::amin (st->firstHPleft, (ui32)st->MaxHealth()); - break; - case Bonus::MANA_DRAIN: - { - CGHeroInstance * h = gs->getHero(ObjectInstanceID(additionalInfo)); - st->state.insert (EBattleStackState::DRAINED_MANA); - h->mana -= val; - vstd::amax(h->mana, 0); - break; - } - case Bonus::POISON: - { - auto b = st->getBonusLocalFirst(Selector::source(Bonus::SPELL_EFFECT, SpellID::POISON) - .And(Selector::type(Bonus::STACK_HEALTH))); - if (b) - b->val = val; - break; - } - case Bonus::ENCHANTER: - break; - case Bonus::FEAR: - st->state.insert(EBattleStackState::FEAR); - break; - default: - logNetwork->warnStream() << "Unrecognized trigger effect type "<< effect; + case Bonus::HP_REGENERATION: + { + int32_t toHeal = val; + CHealth health = st->healthAfterHealed(toHeal, EHealLevel::HEAL, EHealPower::PERMANENT); + st->setHealth(health); + break; + } + case Bonus::MANA_DRAIN: + { + CGHeroInstance * h = gs->getHero(ObjectInstanceID(additionalInfo)); + st->state.insert (EBattleStackState::DRAINED_MANA); + h->mana -= val; + vstd::amax(h->mana, 0); + break; + } + case Bonus::POISON: + { + auto b = st->getBonusLocalFirst(Selector::source(Bonus::SPELL_EFFECT, SpellID::POISON) + .And(Selector::type(Bonus::STACK_HEALTH))); + if (b) + b->val = val; + break; + } + case Bonus::ENCHANTER: + break; + case Bonus::FEAR: + st->state.insert(EBattleStackState::FEAR); + break; + default: + logNetwork->warnStream() << "Unrecognized trigger effect type "<< effect; } } @@ -1392,11 +1395,14 @@ void BattleStackMoved::applyGs(CGameState *gs) DLL_LINKAGE void BattleStackAttacked::applyGs(CGameState *gs) { - CStack *at = gs->curB->getStack(stackAttacked); + CStack * at = gs->curB->getStack(stackAttacked); assert(at); at->popBonuses(Bonus::UntilBeingAttacked); - at->count = newAmount; - at->firstHPleft = newHP; + + if(willRebirth()) + at->health.reset();//kill stack first + else + at->setHealth(newHealth); if(killed()) { @@ -1413,16 +1419,29 @@ DLL_LINKAGE void BattleStackAttacked::applyGs(CGameState *gs) } } //life drain handling - for (auto & elem : healedStacks) - { + for(auto & elem : healedStacks) elem.applyGs(gs); - } - if (willRebirth()) + + if(willRebirth()) { - at->casts--; - at->state.insert(EBattleStackState::ALIVE); //hmm? + //TODO: handle rebirth with StacksHealedOrResurrected + at->casts.use(); + at->state.insert(EBattleStackState::ALIVE); + at->setHealth(newHealth); + + //removing all spells effects + auto selector = [](const Bonus * b) + { + //Special case: DISRUPTING_RAY is "immune" to dispell + //Other even PERMANENT effects can be removed + if(b->source == Bonus::SPELL_EFFECT) + return b->sid != SpellID::DISRUPTING_RAY; + else + return false; + }; + at->popBonuses(selector); } - if (cloneKilled()) + if(cloneKilled()) { //"hide" killed creatures instead so we keep info about it at->makeGhost(); @@ -1436,35 +1455,20 @@ DLL_LINKAGE void BattleStackAttacked::applyGs(CGameState *gs) //killed summoned creature should be removed like clone if(killed() && vstd::contains(at->state, EBattleStackState::SUMMONED)) - { at->makeGhost(); - } } -DLL_LINKAGE void BattleAttack::applyGs(CGameState *gs) +DLL_LINKAGE void BattleAttack::applyGs(CGameState * gs) { - CStack *attacker = gs->curB->getStack(stackAttacking); + CStack * attacker = gs->curB->getStack(stackAttacking); + assert(attacker); + if(counter()) - attacker->counterAttacksPerformed++; + attacker->counterAttacks.use(); if(shot()) - { - //don't remove ammo if we have a working ammo cart - bool hasAmmoCart = false; - for(const CStack * st : gs->curB->stacks) - { - if(st->owner == attacker->owner && st->getCreature()->idNumber == CreatureID::AMMO_CART && st->alive()) - { - hasAmmoCart = true; - break; - } - } + attacker->shots.use(); - if (!hasAmmoCart) - { - attacker->shots--; - } - } for(BattleStackAttacked & stackAttacked : bsa) stackAttacked.applyGs(gs); @@ -1619,7 +1623,8 @@ DLL_LINKAGE void StacksHealedOrResurrected::applyGs(CGameState *gs) { for(auto & elem : healedStacks) { - CStack * changedStack = gs->curB->getStack(elem.stackID, false); + CStack * changedStack = gs->curB->getStack(elem.stackId, false); + assert(changedStack); //checking if we resurrect a stack that is under a living stack auto accessibility = gs->curB->getAccesibility(); @@ -1634,29 +1639,14 @@ DLL_LINKAGE void StacksHealedOrResurrected::applyGs(CGameState *gs) bool resurrected = !changedStack->alive(); //indicates if stack is resurrected or just healed if(resurrected) { - if(changedStack->count > 0 || changedStack->firstHPleft > 0) - logGlobal->warn("Dead stack %s with positive total HP %d", changedStack->nodeName(), changedStack->totalHealth()); + if(auto totalHealth = changedStack->health.available()) + logGlobal->warn("Dead stack %s with positive total HP %d", changedStack->nodeName(), totalHealth); changedStack->state.insert(EBattleStackState::ALIVE); } - int res; - if(canOverheal) //for example WoG ghost soul steal ability allows getting more units than before battle - res = elem.healedHP / changedStack->MaxHealth(); - else - res = std::min(elem.healedHP / changedStack->MaxHealth() , changedStack->baseAmount - changedStack->count); - changedStack->count += res; - if(elem.lowLevelResurrection) - changedStack->resurrected += res; - changedStack->firstHPleft += elem.healedHP - res * changedStack->MaxHealth(); - if(changedStack->firstHPleft > changedStack->MaxHealth()) - { - changedStack->firstHPleft -= changedStack->MaxHealth(); - if(changedStack->baseAmount > changedStack->count) - { - changedStack->count += 1; - } - } - vstd::amin(changedStack->firstHPleft, changedStack->MaxHealth()); + + changedStack->setHealth(elem); + if(resurrected) { //removing all spells effects @@ -1674,7 +1664,7 @@ DLL_LINKAGE void StacksHealedOrResurrected::applyGs(CGameState *gs) else if(cure) { //removing all effects from negative spells - auto selector = [](const Bonus* b) + auto selector = [](const Bonus * b) { //Special case: DISRUPTING_RAY is "immune" to dispell //Other even PERMANENT effects can be removed @@ -1795,7 +1785,7 @@ DLL_LINKAGE void BattleStacksRemoved::applyGs(CGameState *gs) DLL_LINKAGE void BattleStackAdded::applyGs(CGameState *gs) { newStackID = 0; - if (!BattleHex(pos).isValid()) + if(!BattleHex(pos).isValid()) { logNetwork->warnStream() << "No place found for new stack!"; return; @@ -1803,33 +1793,32 @@ DLL_LINKAGE void BattleStackAdded::applyGs(CGameState *gs) CStackBasicDescriptor csbd(creID, amount); CStack * addedStack = gs->curB->generateNewStack(csbd, side, SlotID::SUMMONED_SLOT_PLACEHOLDER, pos); //TODO: netpacks? - if (summoned) + if(summoned) addedStack->state.insert(EBattleStackState::SUMMONED); - gs->curB->localInitStack(addedStack); - gs->curB->stacks.push_back(addedStack); //the stack is not "SUMMONED", it is permanent + addedStack->localInit(gs->curB.get()); + gs->curB->stacks.push_back(addedStack); newStackID = addedStack->ID; } -DLL_LINKAGE void BattleSetStackProperty::applyGs(CGameState *gs) +DLL_LINKAGE void BattleSetStackProperty::applyGs(CGameState * gs) { CStack * stack = gs->curB->getStack(stackID); - switch (which) + switch(which) { case CASTS: { - if (absolute) - stack->casts = val; + if(absolute) + logNetwork->error("Can not change casts in absolute mode"); else - stack->casts += val; - vstd::amax(stack->casts, 0); + stack->casts.use(-val); break; } case ENCHANTER_COUNTER: { auto & counter = gs->curB->sides[gs->curB->whatSide(stack->owner)].enchanterCounter; - if (absolute) + if(absolute) counter = val; else counter += val; diff --git a/lib/battle/BattleAttackInfo.cpp b/lib/battle/BattleAttackInfo.cpp index 2135683f0..5066d302d 100644 --- a/lib/battle/BattleAttackInfo.cpp +++ b/lib/battle/BattleAttackInfo.cpp @@ -9,10 +9,10 @@ */ #include "StdInc.h" #include "BattleAttackInfo.h" -#include "../CStack.h" -BattleAttackInfo::BattleAttackInfo(const CStack * Attacker, const CStack * Defender, bool Shooting) +BattleAttackInfo::BattleAttackInfo(const CStack * Attacker, const CStack * Defender, bool Shooting): + attackerHealth(Attacker->health), defenderHealth(Defender->health) { attacker = Attacker; defender = Defender; @@ -23,9 +23,6 @@ BattleAttackInfo::BattleAttackInfo(const CStack * Attacker, const CStack * Defen attackerPosition = Attacker->position; defenderPosition = Defender->position; - attackerCount = Attacker->count; - defenderCount = Defender->count; - shooting = Shooting; chargedFields = 0; @@ -41,7 +38,7 @@ BattleAttackInfo BattleAttackInfo::reverse() const std::swap(ret.attacker, ret.defender); std::swap(ret.attackerBonuses, ret.defenderBonuses); std::swap(ret.attackerPosition, ret.defenderPosition); - std::swap(ret.attackerCount, ret.defenderCount); + std::swap(ret.attackerHealth, ret.defenderHealth); ret.shooting = false; ret.chargedFields = 0; diff --git a/lib/battle/BattleAttackInfo.h b/lib/battle/BattleAttackInfo.h index 7ced108e1..da57ea69a 100644 --- a/lib/battle/BattleAttackInfo.h +++ b/lib/battle/BattleAttackInfo.h @@ -9,8 +9,8 @@ */ #pragma once #include "BattleHex.h" +#include "../CStack.h" -class CStack; class IBonusBearer; struct DLL_LINKAGE BattleAttackInfo @@ -19,7 +19,8 @@ struct DLL_LINKAGE BattleAttackInfo const CStack *attacker, *defender; BattleHex attackerPosition, defenderPosition; - int attackerCount, defenderCount; + CHealth attackerHealth, defenderHealth; + bool shooting; int chargedFields; diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index e27928b51..6055f3e30 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -78,15 +78,22 @@ std::pair< std::vector, int > BattleInfo::getPath(BattleHex start, Ba return std::make_pair(path, reachability.distances[dest]); } -ui32 BattleInfo::calculateDmg(const CStack* attacker, const CStack* defender, +ui32 BattleInfo::calculateDmg(const CStack * attacker, const CStack * defender, bool shooting, ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg, CRandomGenerator & rand) { - TDmgRange range = calculateDmgRange(attacker, defender, shooting, charge, lucky, unlucky, deathBlow, ballistaDoubleDmg); + BattleAttackInfo bai(attacker, defender, shooting); + bai.chargedFields = charge; + bai.luckyHit = lucky; + bai.unluckyHit = unlucky; + bai.deathBlow = deathBlow; + bai.ballistaDoubleDamage = ballistaDoubleDmg; + + TDmgRange range = calculateDmgRange(bai); if(range.first != range.second) { ui32 sum = 0; - ui32 howManyToAv = std::min(10, attacker->count); + ui32 howManyToAv = std::min(10, attacker->getCount()); for(int g=0; g * casualties) const for(auto & elem : stacks)//setting casualties { const CStack * const st = elem; - si32 killed = (st->alive() ? (st->baseAmount - st->count + st->resurrected) : st->baseAmount); - vstd::amax(killed, 0); - if(killed) + si32 killed = st->getKilled(); + if(killed > 0) casualties[st->side][st->getCreature()->idNumber] += killed; } } @@ -140,29 +146,12 @@ void BattleInfo::localInit() armyObj->attachTo(this); } - for(CStack *s : stacks) - localInitStack(s); + for(CStack * s : stacks) + s->localInit(this); exportBonuses(); } -void BattleInfo::localInitStack(CStack * s) -{ - s->exportBonuses(); - if(s->base) //stack originating from "real" stack in garrison -> attach to it - { - s->attachTo(const_cast(s->base)); - } - else //attach directly to obj to which stack belongs and creature type - { - CArmedInstance *army = battleGetArmyObject(s->side); - s->attachTo(army); - assert(s->type); - s->attachTo(const_cast(s->type)); - } - s->postInit(); -} - namespace CGH { static void readBattlePositions(const JsonNode &node, std::vector< std::vector > & dest) diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index 07c9d0481..a6618d05f 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -72,7 +72,6 @@ struct DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallb void localInit(); - void localInitStack(CStack * s); static BattleInfo * setupBattle(int3 tile, ETerrainType terrain, BFieldType battlefieldType, const CArmedInstance * armies[2], const CGHeroInstance * heroes[2], bool creatureBank, const CGTownInstance * town); //bool hasNativeStack(ui8 side) const; diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index ef815cb4c..b655c97f6 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -529,22 +529,14 @@ bool CBattleInfoCallback::battleCanShoot(const CStack * stack, BattleHex dest) c if(stack->getCreature()->idNumber == CreatureID::CATAPULT && dst) //catapult cannot attack creatures return false; - if(stack->hasBonusOfType(Bonus::SHOOTER)//it's shooter - && battleMatchOwner(stack, dst) - && dst->alive() - && (!battleIsStackBlocked(stack) || stack->hasBonusOfType(Bonus::FREE_SHOOTING)) - && stack->shots - ) + if(stack->canShoot() + && battleMatchOwner(stack, dst) + && dst->alive() + && (!battleIsStackBlocked(stack) || stack->hasBonusOfType(Bonus::FREE_SHOOTING))) return true; return false; } -TDmgRange CBattleInfoCallback::calculateDmgRange(const CStack* attacker, const CStack* defender, bool shooting, - ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg) const -{ - return calculateDmgRange(attacker, defender, attacker->count, shooting, charge, lucky, unlucky, deathBlow, ballistaDoubleDmg); -} - TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const { auto battleBonusValue = [&](const IBonusBearer * bearer, CSelector selector) -> int @@ -559,8 +551,8 @@ TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) }; double additiveBonus = 1.0, multBonus = 1.0, - minDmg = info.attackerBonuses->getMinDamage() * info.attackerCount,//TODO: ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT - maxDmg = info.attackerBonuses->getMaxDamage() * info.attackerCount; + minDmg = info.attackerBonuses->getMinDamage() * info.attackerHealth.getCount(),//TODO: ONLY_MELEE_FIGHT / ONLY_DISTANCE_FIGHT + maxDmg = info.attackerBonuses->getMaxDamage() * info.attackerHealth.getCount(); const CCreature *attackerType = info.attacker->getCreature(), *defenderType = info.defender->getCreature(); @@ -774,19 +766,6 @@ TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) return returnedVal; } -TDmgRange CBattleInfoCallback::calculateDmgRange(const CStack* attacker, const CStack* defender, TQuantity attackerCount, - bool shooting, ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg) const -{ - BattleAttackInfo bai(attacker, defender, shooting); - bai.attackerCount = attackerCount; - bai.chargedFields = charge; - bai.luckyHit = lucky; - bai.unluckyHit = unlucky; - bai.deathBlow = deathBlow; - bai.ballistaDoubleDamage = ballistaDoubleDmg; - return calculateDmgRange(bai); -} - TDmgRange CBattleInfoCallback::battleEstimateDamage(CRandomGenerator & rand, const CStack * attacker, const CStack * defender, TDmgRange * retaliationDmg) const { RETURN_IF_NOT_BATTLE(std::make_pair(0, 0)); @@ -816,10 +795,10 @@ std::pair CBattleInfoCallback::battleEstimateDamage(CRandomGenerator { BattleStackAttacked bsa; bsa.damageAmount = ret.*pairElems[i]; - bai.defender->prepareAttacked(bsa, rand, bai.defenderCount); + bai.defender->prepareAttacked(bsa, rand, bai.defenderHealth); auto retaliationAttack = bai.reverse(); - retaliationAttack.attackerCount = bsa.newAmount; + retaliationAttack.attackerHealth = retaliationAttack.attacker->healthAfterAttacked(bsa.damageAmount); retaliationDmg->*pairElems[!i] = calculateDmgRange(retaliationAttack).*pairElems[!i]; } } @@ -1494,7 +1473,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c { auto walker = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack { - return !stack->shots; + return !stack->canShoot(); }); if (!walker) @@ -1505,7 +1484,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c { auto shooter = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack { - return stack->hasBonusOfType(Bonus::SHOOTER) && stack->shots; + return stack->canShoot(); }); if (!shooter) continue; @@ -1527,19 +1506,19 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c case SpellID::CURE: //only damaged units { //do not cast on affected by debuffs - if (subject->firstHPleft >= subject->MaxHealth()) + if(!subject->canBeHealed()) continue; } break; case SpellID::BLOODLUST: { - if (subject->shots) //if can shoot - only if enemy uits are adjacent + if(subject->canShoot()) //TODO: if can shoot - only if enemy units are adjacent continue; } break; case SpellID::PRECISION: { - if (!(subject->hasBonusOfType(Bonus::SHOOTER) && subject->shots)) + if(!subject->canShoot()) continue; } break; @@ -1611,7 +1590,7 @@ int CBattleInfoCallback::battleGetSurrenderCost(PlayerColor Player) const double discount = 0; for(const CStack * s : battleAliveStacks(side.get())) if(s->base) //we pay for our stack that comes from our army slots - condition eliminates summoned cres and war machines - ret += s->getCreature()->cost[Res::GOLD] * s->count; + ret += s->getCreature()->cost[Res::GOLD] * s->getCount(); //todo: extract CStack method if(const CGHeroInstance * h = battleGetFightingHero(side.get())) discount += h->valOfBonuses(Bonus::SURRENDER_DISCOUNT); diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index 04f4b7c7b..0dca1233c 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -59,8 +59,6 @@ public: std::set batteAdjacentCreatures (const CStack * stack) const; TDmgRange calculateDmgRange(const BattleAttackInfo & info) const; //charge - number of hexes travelled before attack (for champion's jousting); returns pair - TDmgRange calculateDmgRange(const CStack* attacker, const CStack* defender, TQuantity attackerCount, bool shooting, ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg) const; //charge - number of hexes travelled before attack (for champion's jousting); returns pair - TDmgRange calculateDmgRange(const CStack* attacker, const CStack* defender, bool shooting, ui8 charge, bool lucky, bool unlucky, bool deathBlow, bool ballistaDoubleDmg) const; //charge - number of hexes travelled before attack (for champion's jousting); returns pair //hextowallpart //int battleGetWallUnderHex(BattleHex hex) const; //returns part of destructible wall / gate / keep under given hex or -1 if not found std::pair battleEstimateDamage(CRandomGenerator & rand, const BattleAttackInfo & bai, std::pair * retaliationDmg = nullptr) const; //estimates damage dealt by attacker to defender; it may be not precise especially when stack has randomly working bonuses; returns pair diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index 44befb406..c3edbbe7a 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -360,8 +360,17 @@ bool CBattleInfoEssentials::battleMatchOwner(const CStack * attacker, const CSta return true; else if(attacker == defender) return positivness; + else + return battleMatchOwner(battleGetOwner(attacker), defender, positivness); +} + +bool CBattleInfoEssentials::battleMatchOwner(const PlayerColor & attacker, const CStack * defender, const boost::logic::tribool positivness /* = false*/) const +{ + RETURN_IF_NOT_BATTLE(false); + if(boost::logic::indeterminate(positivness)) + return true; else if(defender->owner != battleGetOwner(defender)) return true; //mind controlled unit is attackable for both sides else - return (battleGetOwner(attacker) == battleGetOwner(defender)) == positivness; + return (attacker == battleGetOwner(defender)) == positivness; } diff --git a/lib/battle/CBattleInfoEssentials.h b/lib/battle/CBattleInfoEssentials.h index 5086318b8..0d8156a6c 100644 --- a/lib/battle/CBattleInfoEssentials.h +++ b/lib/battle/CBattleInfoEssentials.h @@ -101,4 +101,5 @@ public: ///check that stacks are controlled by same|other player(s) depending on positiveness ///mind control included bool battleMatchOwner(const CStack * attacker, const CStack * defender, const boost::logic::tribool positivness = false) const; + bool battleMatchOwner(const PlayerColor & attacker, const CStack * defender, const boost::logic::tribool positivness = false) const; }; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 8fc7d7f7c..57db26b5c 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -366,7 +366,6 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor *dst /*= if(dst != this) continue; - int slot = -1; ArtifactID aid = creature->warMachine; const CArtifact * art = aid.toArtifact(); @@ -1080,7 +1079,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b const CreatureID creatureTypes[] = {CreatureID::SKELETON, CreatureID::WALKING_DEAD, CreatureID::WIGHTS, CreatureID::LICHES}; const bool improvedNecromancy = hasBonusOfType(Bonus::IMPROVED_NECROMANCY); const CCreature *raisedUnitType = VLC->creh->creatures[creatureTypes[improvedNecromancy ? necromancyLevel : 0]]; - const ui32 raisedUnitHP = raisedUnitType->valOfBonuses(Bonus::STACK_HEALTH); + const ui32 raisedUnitHP = raisedUnitType->MaxHealth(); //calculate creatures raised from each defeated stack for (auto & casualtie : casualties) @@ -1088,7 +1087,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b // Get lost enemy hit points convertible to units. CCreature * c = VLC->creh->creatures[casualtie.first]; - const ui32 raisedHP = c->valOfBonuses(Bonus::STACK_HEALTH) * casualtie.second * necromancySkill; + const ui32 raisedHP = c->MaxHealth() * casualtie.second * necromancySkill; raisedUnits += std::min(raisedHP / raisedUnitHP, casualtie.second * necromancySkill); //limit to % of HP and % of original stack count } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index d5a7713a3..0a0f96afb 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -1941,7 +1941,7 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const if(drown) { cb->changeStackCount(StackLocation(h, i->first), -drown); - xp += drown * i->second->type->valOfBonuses(Bonus::STACK_HEALTH); + xp += drown * i->second->type->MaxHealth(); } } diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 8341a4120..9ce33b443 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -12,7 +12,7 @@ #include "../ConstTransitivePtr.h" #include "../GameConstants.h" -const ui32 SERIALIZATION_VERSION = 774; +const ui32 SERIALIZATION_VERSION = 775; const ui32 MINIMAL_SERIALIZATION_VERSION = 753; const std::string SAVEGAME_MAGIC = "VCMISVG"; diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index 5849ece60..0db0b6b18 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -26,21 +26,25 @@ HealingSpellMechanics::HealingSpellMechanics(const CSpell * s): void HealingSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const { EHealLevel healLevel = getHealLevel(parameters.effectLevel); + EHealPower healPower = getHealPower(parameters.effectLevel); + int hpGained = calculateHealedHP(env, parameters, ctx); StacksHealedOrResurrected shr; shr.lifeDrain = false; shr.tentHealing = false; + //special case for Archangel shr.cure = parameters.mode == ECastingMode::CREATURE_ACTIVE_CASTING && owner->id == SpellID::RESURRECTION; - const bool resurrect = (healLevel != EHealLevel::HEAL); for(auto & attackedCre : ctx.attackedCres) { - StacksHealedOrResurrected::HealInfo hi; - hi.stackID = (attackedCre)->ID; - int stackHPgained = parameters.caster->getSpellBonus(owner, hpGained, attackedCre); - hi.healedHP = attackedCre->calculateHealedHealthPoints(stackHPgained, resurrect); - hi.lowLevelResurrection = (healLevel == EHealLevel::RESURRECT); + int32_t stackHPgained = parameters.caster->getSpellBonus(owner, hpGained, attackedCre); + CHealth health = attackedCre->healthAfterHealed(stackHPgained, healLevel, healPower); + + CHealthInfo hi; + health.toInfo(hi); + hi.stackId = attackedCre->ID; + hi.delta = stackHPgained; shr.healedStacks.push_back(hi); } if(!shr.healedStacks.empty()) @@ -147,7 +151,7 @@ void CloneMechanics::applyBattleEffects(const SpellCastEnvironment * env, const bsa.side = parameters.casterSide; bsa.summoned = true; bsa.pos = parameters.cb->getAvaliableHex(bsa.creID, parameters.casterSide); - bsa.amount = clonedStack->count; + bsa.amount = clonedStack->getCount(); env->sendAndApply(&bsa); BattleSetStackProperty ssp; @@ -174,19 +178,16 @@ void CloneMechanics::applyBattleEffects(const SpellCastEnvironment * env, const ESpellCastProblem::ESpellCastProblem CloneMechanics::isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const { //can't clone already cloned creature - if(vstd::contains(obj->state, EBattleStackState::CLONED)) + if(obj->isClone()) return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; + //can`t clone if old clone still alive if(obj->cloneID != -1) return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; ui8 schoolLevel; if(caster) - { schoolLevel = caster->getEffectLevel(owner); - } else - { schoolLevel = 3;//todo: remove - } if(schoolLevel < 3) { @@ -211,11 +212,16 @@ void CureMechanics::applyBattle(BattleInfo * battle, const BattleSpellCast * pac doDispell(battle, packet, dispellSelector); } -HealingSpellMechanics::EHealLevel CureMechanics::getHealLevel(int effectLevel) const +EHealLevel CureMechanics::getHealLevel(int effectLevel) const { return EHealLevel::HEAL; } +EHealPower CureMechanics::getHealPower(int effectLevel) const +{ + return EHealPower::PERMANENT; +} + bool CureMechanics::dispellSelector(const Bonus * b) { if(b->source == Bonus::SPELL_EFFECT) @@ -436,10 +442,10 @@ ESpellCastProblem::ESpellCastProblem HypnotizeMechanics::isImmuneByStack(const I if(nullptr != caster) { //TODO: what with other creatures casting hypnotize, Faerie Dragons style? - ui32 subjectHealth = obj->totalHealth(); + int64_t subjectHealth = obj->health.available(); //apply 'damage' bonus for hypnotize, including hero specialty - ui32 maxHealth = caster->getSpellBonus(owner, owner->calculateRawEffectValue(caster->getEffectLevel(owner), caster->getEffectPower(owner)), obj); - if (subjectHealth > maxHealth) + int64_t maxHealth = caster->getSpellBonus(owner, owner->calculateRawEffectValue(caster->getEffectLevel(owner), caster->getEffectPower(owner)), obj); + if(subjectHealth > maxHealth) return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; } return DefaultSpellMechanics::isImmuneByStack(caster, obj); @@ -798,13 +804,18 @@ RisingSpellMechanics::RisingSpellMechanics(const CSpell * s): { } -HealingSpellMechanics::EHealLevel RisingSpellMechanics::getHealLevel(int effectLevel) const +EHealLevel RisingSpellMechanics::getHealLevel(int effectLevel) const +{ + return EHealLevel::RESURRECT; +} + +EHealPower RisingSpellMechanics::getHealPower(int effectLevel) const { //this may be even distinct class if((effectLevel <= 1) && (owner->id == SpellID::RESURRECTION)) - return EHealLevel::RESURRECT; - - return EHealLevel::TRUE_RESURRECT; + return EHealPower::ONE_BATTLE; + else + return EHealPower::PERMANENT; } ///SacrificeMechanics @@ -889,7 +900,7 @@ int SacrificeMechanics::calculateHealedHP(const SpellCastEnvironment* env, const return 0; } - return (parameters.effectPower + victim->MaxHealth() + owner->getPower(parameters.effectLevel)) * victim->count; + return (parameters.effectPower + victim->MaxHealth() + owner->getPower(parameters.effectLevel)) * victim->getCount(); } bool SacrificeMechanics::requiresCreatureTarget() const @@ -905,22 +916,23 @@ SpecialRisingSpellMechanics::SpecialRisingSpellMechanics(const CSpell * s): ESpellCastProblem::ESpellCastProblem SpecialRisingSpellMechanics::canBeCast(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const { - //find alive possible target - const CStack * stackToHeal = cb->getStackIf([ctx, this](const CStack * s) + auto mainFilter = [cb, ctx, this](const CStack * s) -> bool { - const bool ownerMatches = !ctx.ti.smart || s->owner == ctx.caster->getOwner(); - - return ownerMatches && s->isValidTarget(false) && s->coversPos(ctx.destination) && ESpellCastProblem::OK == owner->isImmuneByStack(ctx.caster, s); + const bool ownerMatches = !ctx.ti.smart || cb->battleMatchOwner(ctx.caster->getOwner(), s, owner->getPositiveness()); + return ownerMatches && s->coversPos(ctx.destination) && ESpellCastProblem::OK == owner->isImmuneByStack(ctx.caster, s); + }; + //find alive possible target + const CStack * stackToHeal = cb->getStackIf([mainFilter](const CStack * s) + { + return s->isValidTarget(false) && mainFilter(s); }); if(nullptr == stackToHeal) { //find dead possible target if there is no alive target - stackToHeal = cb->getStackIf([ctx, this](const CStack * s) + stackToHeal = cb->getStackIf([mainFilter](const CStack * s) { - const bool ownerMatches = !ctx.ti.smart || s->owner == ctx.caster->getOwner(); - - return ownerMatches && s->isValidTarget(true) && s->coversPos(ctx.destination) && ESpellCastProblem::OK == owner->isImmuneByStack(ctx.caster, s); + return s->isValidTarget(true) && mainFilter(s); }); //we have found dead target @@ -930,7 +942,7 @@ ESpellCastProblem::ESpellCastProblem SpecialRisingSpellMechanics::canBeCast(cons { const CStack * other = cb->getStackIf([hex, stackToHeal](const CStack * s) { - return s->isValidTarget(false) && s->coversPos(hex) && s != stackToHeal; + return s->isValidTarget(true) && s->coversPos(hex) && s != stackToHeal; }); if(nullptr != other) return ESpellCastProblem::NO_APPROPRIATE_TARGET;//alive stack blocks resurrection @@ -949,16 +961,22 @@ ESpellCastProblem::ESpellCastProblem SpecialRisingSpellMechanics::isImmuneByStac // following does apply to resurrect and animate dead(?) only // for sacrifice health calculation and health limit check don't matter - if(obj->count >= obj->baseAmount) + if(obj->getCount() >= obj->baseAmount) return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; - //FIXME: Archangels can cast immune stack and this should be applied for them and not hero -// if(caster) -// { -// auto maxHealth = calculateHealedHP(caster, obj, nullptr); -// if (maxHealth < obj->MaxHealth()) //must be able to rise at least one full creature -// return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; -// } + //FIXME: code duplication with BattleSpellCastParameters + auto getEffectValue = [&]() -> si32 + { + si32 effectValue = caster->getEffectValue(owner); + return (effectValue == 0) ? owner->calculateRawEffectValue(caster->getEffectLevel(owner), caster->getEffectPower(owner)) : effectValue; + }; + + if(caster) + { + auto maxHealth = getEffectValue(); + if (maxHealth < obj->MaxHealth()) //must be able to rise at least one full creature + return ESpellCastProblem::STACK_IMMUNE_TO_SPELL; + } return DefaultSpellMechanics::isImmuneByStack(caster,obj); } @@ -983,7 +1001,7 @@ ESpellCastProblem::ESpellCastProblem SummonMechanics::canBeCast(const CBattleInf { return (st->owner == caster->getOwner()) && (vstd::contains(st->state, EBattleStackState::SUMMONED)) - && (!vstd::contains(st->state, EBattleStackState::CLONED)) + && (!st->isClone()) && (st->getCreature()->idNumber != creatureToSummon); }); diff --git a/lib/spells/BattleSpellMechanics.h b/lib/spells/BattleSpellMechanics.h index c7b5a9002..da52b32ef 100644 --- a/lib/spells/BattleSpellMechanics.h +++ b/lib/spells/BattleSpellMechanics.h @@ -18,18 +18,12 @@ class SpellCreatedObstacle; class DLL_LINKAGE HealingSpellMechanics : public DefaultSpellMechanics { public: - enum class EHealLevel - { - HEAL, - RESURRECT, - TRUE_RESURRECT - }; - HealingSpellMechanics(const CSpell * s); protected: void applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const override; virtual int calculateHealedHP(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const; virtual EHealLevel getHealLevel(int effectLevel) const = 0; + virtual EHealPower getHealPower(int effectLevel) const = 0; }; class DLL_LINKAGE AntimagicMechanics : public DefaultSpellMechanics @@ -63,6 +57,7 @@ public: void applyBattle(BattleInfo * battle, const BattleSpellCast * packet) const override final; ESpellCastProblem::ESpellCastProblem isImmuneByStack(const ISpellCaster * caster, const CStack * obj) const override; EHealLevel getHealLevel(int effectLevel) const override final; + EHealPower getHealPower(int effectLevel) const override final; private: static bool dispellSelector(const Bonus * b); }; @@ -177,7 +172,8 @@ class DLL_LINKAGE RisingSpellMechanics : public HealingSpellMechanics { public: RisingSpellMechanics(const CSpell * s); - EHealLevel getHealLevel(int effectLevel) const override; + EHealLevel getHealLevel(int effectLevel) const override final; + EHealPower getHealPower(int effectLevel) const override final; }; class DLL_LINKAGE SacrificeMechanics : public RisingSpellMechanics diff --git a/lib/spells/CDefaultSpellMechanics.cpp b/lib/spells/CDefaultSpellMechanics.cpp index 9241c5fc4..4cdcc297a 100644 --- a/lib/spells/CDefaultSpellMechanics.cpp +++ b/lib/spells/CDefaultSpellMechanics.cpp @@ -346,24 +346,11 @@ void DefaultSpellMechanics::battleLog(std::vector & logLines, const auto attackedStack = attacked.at(0); - auto getPluralFormat = [attackedStack](const int baseTextID) -> si32 - { - return attackedStack->count > 1 ? baseTextID + 1 : baseTextID; - }; - - auto logSimple = [attackedStack, &logLines, getPluralFormat](const int baseTextID) + auto addLogLine = [attackedStack, &logLines](const int baseTextID, const boost::logic::tribool & plural) { MetaString line; - line.addTxt(MetaString::GENERAL_TXT, getPluralFormat(baseTextID)); - line.addReplacement(*attackedStack); - logLines.push_back(line); - }; - - auto logPlural = [attackedStack, &logLines, getPluralFormat](const int baseTextID) - { - MetaString line; - line.addTxt(MetaString::GENERAL_TXT, baseTextID); - line.addReplacement(MetaString::CRE_PL_NAMES, attackedStack->getCreature()->idNumber.num); + attackedStack->addText(line, MetaString::GENERAL_TXT, baseTextID, plural); + attackedStack->addNameReplacement(line, plural); logLines.push_back(line); }; @@ -372,26 +359,26 @@ void DefaultSpellMechanics::battleLog(std::vector & logLines, const switch(owner->id) { case SpellID::STONE_GAZE: - logSimple(558); + addLogLine(558, boost::logic::indeterminate); break; case SpellID::POISON: - logSimple(561); + addLogLine(561, boost::logic::indeterminate); break; case SpellID::BIND: - logPlural(560);//Roots and vines bind the %s to the ground! + addLogLine(-560, true);//"Roots and vines bind the %s to the ground!" break; case SpellID::DISEASE: - logSimple(553); + addLogLine(553, boost::logic::indeterminate); break; case SpellID::PARALYZE: - logSimple(563); + addLogLine(563, boost::logic::indeterminate); break; case SpellID::AGE: { - //The %s shrivel with age, and lose %d hit points." + //"The %s shrivel with age, and lose %d hit points." MetaString line; - line.addTxt(MetaString::GENERAL_TXT, getPluralFormat(551)); - line.addReplacement(MetaString::CRE_PL_NAMES, attackedStack->getCreature()->idNumber.num); + attackedStack->addText(line, MetaString::GENERAL_TXT, 551); + attackedStack->addNameReplacement(line); //todo: display effect from only this cast TBonusListPtr bl = attackedStack->getBonuses(Selector::type(Bonus::STACK_HEALTH)); @@ -403,7 +390,7 @@ void DefaultSpellMechanics::battleLog(std::vector & logLines, const break; case SpellID::THUNDERBOLT: { - logPlural(367); + addLogLine(-367, true); MetaString line; //todo: handle newlines in metastring std::string text = VLC->generaltexth->allTexts[343].substr(1, VLC->generaltexth->allTexts[343].size() - 1); //Does %d points of damage. @@ -413,22 +400,22 @@ void DefaultSpellMechanics::battleLog(std::vector & logLines, const } break; case SpellID::DISPEL_HELPFUL_SPELLS: - logPlural(555); + addLogLine(-555, true); break; case SpellID::DEATH_STARE: - if (damageToDisplay > 0) + if(damageToDisplay > 0) { MetaString line; - if (damageToDisplay > 1) + if(damageToDisplay > 1) { line.addTxt(MetaString::GENERAL_TXT, 119); //%d %s die under the terrible gaze of the %s. line.addReplacement(damageToDisplay); - line.addReplacement(MetaString::CRE_PL_NAMES, attackedStack->getCreature()->idNumber.num); + attackedStack->addNameReplacement(line, true); } else { line.addTxt(MetaString::GENERAL_TXT, 118); //One %s dies under the terrible gaze of the %s. - line.addReplacement(MetaString::CRE_SING_NAMES, attackedStack->getCreature()->idNumber.num); + attackedStack->addNameReplacement(line, false); } parameters.caster->getCasterName(line); logLines.push_back(line); @@ -650,9 +637,9 @@ std::vector DefaultSpellMechanics::getAffectedStacks(const CBatt return attackedCres; } -std::vector DefaultSpellMechanics::calculateAffectedStacks(const CBattleInfoCallback* cb, const SpellTargetingContext& ctx) const +std::vector DefaultSpellMechanics::calculateAffectedStacks(const CBattleInfoCallback * cb, const SpellTargetingContext & ctx) const { - std::set attackedCres;//std::set to exclude multiple occurrences of two hex creatures + std::set attackedCres;//std::set to exclude multiple occurrences of two hex creatures const auto side = cb->playerToSide(ctx.caster->getOwner()); if(!side) @@ -665,10 +652,9 @@ std::vector DefaultSpellMechanics::calculateAffectedStacks(const auto mainFilter = [=](const CStack * s) { - const bool positiveToAlly = owner->isPositive() && s->owner == ctx.caster->getOwner(); - const bool negativeToEnemy = owner->isNegative() && s->owner != ctx.caster->getOwner(); + const bool ownerMatches = cb->battleMatchOwner(ctx.caster->getOwner(), s, owner->getPositiveness()); const bool validTarget = s->isValidTarget(!ctx.ti.onlyAlive); //todo: this should be handled by spell class - const bool positivenessFlag = !ctx.ti.smart || owner->isNeutral() || positiveToAlly || negativeToEnemy; + const bool positivenessFlag = !ctx.ti.smart || ownerMatches; return positivenessFlag && validTarget; }; @@ -701,7 +687,7 @@ std::vector DefaultSpellMechanics::calculateAffectedStacks(const else if(ctx.ti.massive) { TStacks stacks = cb->battleGetStacksIf(mainFilter); - for (auto stack : stacks) + for(auto stack : stacks) attackedCres.insert(stack); } else //custom range from attackedHexes @@ -732,28 +718,12 @@ ESpellCastProblem::ESpellCastProblem DefaultSpellMechanics::canBeCast(const CBat { std::vector affected = getAffectedStacks(cb, ctx); - //allow to cast spell if affects is at least one smart target + //allow to cast spell if it affects at least one smart target bool targetExists = false; for(const CStack * stack : affected) { - bool casterStack = stack->owner == ctx.caster->getOwner(); - - switch (owner->positiveness) - { - case CSpell::POSITIVE: - if(casterStack) - targetExists = true; - break; - case CSpell::NEUTRAL: - targetExists = true; - break; - case CSpell::NEGATIVE: - if(!casterStack) - targetExists = true; - break; - } - + targetExists = cb->battleMatchOwner(ctx.caster->getOwner(), stack, owner->getPositiveness()); if(targetExists) break; } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 9144ac663..b426d8866 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -219,26 +219,9 @@ ESpellCastProblem::ESpellCastProblem CSpell::canBeCast(const CBattleInfoCallback for(const CStack * stack : cb->battleGetAllStacks()) { - bool immune = !(stack->isValidTarget(!tinfo.onlyAlive) && ESpellCastProblem::OK == isImmuneByStack(caster, stack)); - bool casterStack = stack->owner == caster->getOwner(); - - if(!immune) - { - switch (positiveness) - { - case CSpell::POSITIVE: - if(casterStack) - targetExists = true; - break; - case CSpell::NEUTRAL: - targetExists = true; - break; - case CSpell::NEGATIVE: - if(!casterStack) - targetExists = true; - break; - } - } + const bool immune = !(stack->isValidTarget(!tinfo.onlyAlive) && ESpellCastProblem::OK == isImmuneByStack(caster, stack)); + const bool ownerMatches = cb->battleMatchOwner(caster->getOwner(), stack, getPositiveness()); + targetExists = !immune && ownerMatches; if(targetExists) break; } diff --git a/lib/spells/CreatureSpellMechanics.cpp b/lib/spells/CreatureSpellMechanics.cpp index f2d26b4b2..96eca08dc 100644 --- a/lib/spells/CreatureSpellMechanics.cpp +++ b/lib/spells/CreatureSpellMechanics.cpp @@ -72,7 +72,7 @@ void DeathStareMechanics::applyBattleEffects(const SpellCastEnvironment * env, c si32 damageToDisplay = parameters.effectPower; if(!ctx.attackedCres.empty()) - vstd::amin(damageToDisplay, (*ctx.attackedCres.begin())->count); //stack is already reduced after attack + vstd::amin(damageToDisplay, (*ctx.attackedCres.begin())->getCount()); //stack is already reduced after attack ctx.setDamageToDisplay(damageToDisplay); @@ -81,7 +81,7 @@ void DeathStareMechanics::applyBattleEffects(const SpellCastEnvironment * env, c BattleStackAttacked bsa; bsa.flags |= BattleStackAttacked::SPELL_EFFECT; bsa.spellID = owner->id; - bsa.damageAmount = parameters.effectPower * (attackedCre)->valOfBonuses(Bonus::STACK_HEALTH);//todo: move here all DeathStare calculation + bsa.damageAmount = parameters.effectPower * (attackedCre)->MaxHealth();//todo: move here all DeathStare calculation bsa.stackAttacked = (attackedCre)->ID; bsa.attackerID = -1; (attackedCre)->prepareAttacked(bsa, env->getRandomGenerator()); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index db196548e..83fcb0093 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -950,7 +950,7 @@ void CGameHandler::applyBattleEffects(BattleAttack &bat, const CStack *att, cons def->prepareAttacked(bsa, getRandomGenerator()); //calculate casualties //life drain handling - if (att->hasBonusOfType(Bonus::LIFE_DRAIN) && def->isLiving()) + if(att->hasBonusOfType(Bonus::LIFE_DRAIN) && def->isLiving()) { StacksHealedOrResurrected shi; shi.lifeDrain = true; @@ -958,48 +958,49 @@ void CGameHandler::applyBattleEffects(BattleAttack &bat, const CStack *att, cons shi.cure = false; shi.drainedFrom = def->ID; - StacksHealedOrResurrected::HealInfo hi; - hi.stackID = att->ID; - hi.healedHP = att->calculateHealedHealthPoints(bsa.damageAmount * att->valOfBonuses (Bonus::LIFE_DRAIN) / 100, true); - hi.lowLevelResurrection = false; + int32_t toHeal = bsa.damageAmount * att->valOfBonuses(Bonus::LIFE_DRAIN) / 100; + CHealth health = att->healthAfterHealed(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); + + CHealthInfo hi; + health.toInfo(hi); + hi.stackId = att->ID; + hi.delta = toHeal; shi.healedStacks.push_back(hi); - if (hi.healedHP > 0) - { + if(hi.delta > 0) bsa.healedStacks.push_back(shi); - } } //soul steal handling - if (att->hasBonusOfType(Bonus::SOUL_STEAL) && def->isLiving()) + if(att->hasBonusOfType(Bonus::SOUL_STEAL) && def->isLiving()) { StacksHealedOrResurrected shi; shi.lifeDrain = true; shi.tentHealing = false; shi.cure = false; - shi.canOverheal = true; shi.drainedFrom = def->ID; - for (int i = 0; i < 2; i++) //we can have two bonuses - one with subtype 0 and another with subtype 1 + for(int i = 0; i < 2; i++) //we can have two bonuses - one with subtype 0 and another with subtype 1 { - if (att->hasBonusOfType(Bonus::SOUL_STEAL, i)) + if(att->hasBonusOfType(Bonus::SOUL_STEAL, i)) { - StacksHealedOrResurrected::HealInfo hi; - hi.stackID = att->ID; - hi.healedHP = bsa.killedAmount * att->valOfBonuses(Bonus::SOUL_STEAL, i) * att->MaxHealth(); - hi.lowLevelResurrection = (bool)i; - shi.healedStacks.push_back(hi); + int32_t toHeal = bsa.killedAmount * att->valOfBonuses(Bonus::SOUL_STEAL, i) * att->MaxHealth(); + CHealth health = att->healthAfterHealed(toHeal, EHealLevel::OVERHEAL, ((i == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT)); + CHealthInfo hi; + health.toInfo(hi); + hi.stackId = att->ID; + hi.delta = toHeal; + if(hi.delta > 0) + shi.healedStacks.push_back(hi); } } - if (std::any_of(shi.healedStacks.begin(), shi.healedStacks.end(), [](StacksHealedOrResurrected::HealInfo healInfo) { return healInfo.healedHP > 0; })) - { + if(!shi.healedStacks.empty()) bsa.healedStacks.push_back(shi); - } } bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated //fire shield handling - if (!bat.shot() && !vstd::contains(def->state, EBattleStackState::CLONED) && + if(!bat.shot() && !def->isClone() && def->hasBonusOfType(Bonus::FIRE_SHIELD) && !att->hasBonusOfType(Bonus::FIRE_IMMUNITY)) { // TODO: Fire shield damage should be calculated separately after BattleAttack applied. @@ -1011,7 +1012,7 @@ void CGameHandler::applyBattleEffects(BattleAttack &bat, const CStack *att, cons bsa2.flags |= BattleStackAttacked::EFFECT; //FIXME: play animation upon efreet and not attacker bsa2.effect = 11; - bsa2.damageAmount = (std::min(def->totalHealth(), bsa.damageAmount) * def->valOfBonuses(Bonus::FIRE_SHIELD)) / 100; //TODO: scale with attack/defense + bsa2.damageAmount = (std::min(def->health.available(), bsa.damageAmount) * def->valOfBonuses(Bonus::FIRE_SHIELD)) / 100; //TODO: scale with attack/defense att->prepareAttacked(bsa2, getRandomGenerator()); bat.bsa.push_back(bsa2); } @@ -3830,11 +3831,29 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) } case Battle::DEFEND: { - //defensive stance //TODO: remove this bonus when stack becomes active + //defensive stance SetStackEffect sse; - sse.effect.push_back(Bonus(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, 20, -1, PrimarySkill::DEFENSE, Bonus::PERCENT_TO_ALL)); - sse.effect.push_back(Bonus(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, gs->curB->battleGetStackByID(ba.stackNumber)->valOfBonuses(Bonus::DEFENSIVE_STANCE), - -1, PrimarySkill::DEFENSE, Bonus::ADDITIVE_VALUE)); + Bonus bonus1(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, 20, -1, PrimarySkill::DEFENSE, Bonus::PERCENT_TO_ALL); + Bonus bonus2(Bonus::STACK_GETS_TURN, Bonus::PRIMARY_SKILL, Bonus::OTHER, stack->valOfBonuses(Bonus::DEFENSIVE_STANCE), + -1, PrimarySkill::DEFENSE, Bonus::ADDITIVE_VALUE); + BonusList defence = *stack->getBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::DEFENSE)); + int oldDefenceValue = defence.totalValue(); + + defence.push_back(std::make_shared(bonus1)); + defence.push_back(std::make_shared(bonus2)); + + int difference = defence.totalValue() - oldDefenceValue; + + MetaString text; + stack->addText(text, MetaString::GENERAL_TXT, 120); + stack->addNameReplacement(text); + text.addReplacement(difference); + + sse.battleLog.push_back(text); + + sse.effect.push_back(bonus1); + sse.effect.push_back(bonus2); + sse.stacks.push_back(ba.stackNumber); sendAndApply(&sse); @@ -4005,12 +4024,12 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) int additionalAttacks = stack->getBonuses(Selector::type (Bonus::ADDITIONAL_ATTACK), (Selector::effectRange(Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_DISTANCE_FIGHT))))->totalValue(); - for (int i = 0; i < additionalAttacks; ++i) + for(int i = 0; i < additionalAttacks; ++i) { if ( stack->alive() && destinationStack->alive() - && stack->shots + && stack->shots.canUse() ) { BattleAttack bat; @@ -4177,37 +4196,37 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) const CStack *healer = gs->curB->battleGetStackByID(ba.stackNumber), *destStack = gs->curB->battleGetStackByPos(ba.destinationTile); - ui32 healed = 0; - if (healer == nullptr || destStack == nullptr || !healer->hasBonusOfType(Bonus::HEALER)) + if(healer == nullptr || destStack == nullptr || !healer->hasBonusOfType(Bonus::HEALER)) { complain("There is either no healer, no destination, or healer cannot heal :P"); } else { - ui32 maxiumHeal = healer->count * std::max(10, attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::FIRST_AID)); - healed = destStack->calculateHealedHealthPoints(maxiumHeal, false); - } + int32_t toHeal = healer->getCount() * std::max(10, attackingHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::FIRST_AID)); - if (healed == 0) - { - //nothing to heal.. should we complain? - } - else - { - StacksHealedOrResurrected shr; - shr.lifeDrain = false; - shr.tentHealing = true; - shr.cure = false; - shr.drainedFrom = ba.stackNumber; + //TODO: allow resurrection for mods + CHealth health = destStack->healthAfterHealed(toHeal, EHealLevel::HEAL, EHealPower::PERMANENT); - StacksHealedOrResurrected::HealInfo hi; - hi.healedHP = healed; - hi.lowLevelResurrection = false; - hi.stackID = destStack->ID; + if(toHeal == 0) + { + logGlobal->warn("Nothing to heal"); + } + else + { + StacksHealedOrResurrected shr; + shr.lifeDrain = false; + shr.tentHealing = true; + shr.cure = false; + shr.drainedFrom = ba.stackNumber; - shr.healedStacks.push_back(hi); - sendAndApply(&shr); + CHealthInfo hi; + health.toInfo(hi); + hi.stackId = destStack->ID; + hi.delta = toHeal; + shr.healedStacks.push_back(hi); + sendAndApply(&shr); + } } break; } @@ -4223,7 +4242,7 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) bsa.side = summoner->side; bsa.creID = summonedType; - ui64 risedHp = summoner->count * summoner->valOfBonuses(Bonus::DAEMON_SUMMONING, bsa.creID.toEnum()); + ui64 risedHp = summoner->getCount() * summoner->valOfBonuses(Bonus::DAEMON_SUMMONING, bsa.creID.toEnum()); ui64 targetHealth = destStack->getCreature()->MaxHealth() * destStack->baseAmount; ui64 canRiseHp = std::min(targetHealth, risedHp); @@ -4489,12 +4508,12 @@ void CGameHandler::stackTurnTrigger(const CStack *st) if (st->hasBonusOfType(Bonus::HP_REGENERATION)) { bte.effect = Bonus::HP_REGENERATION; - bte.val = std::min((int)(st->MaxHealth() - st->firstHPleft), st->valOfBonuses(Bonus::HP_REGENERATION)); + bte.val = std::min((int)(st->MaxHealth() - st->getFirstHPleft()), st->valOfBonuses(Bonus::HP_REGENERATION)); } if (st->hasBonusOfType(Bonus::FULL_HP_REGENERATION)) { bte.effect = Bonus::HP_REGENERATION; - bte.val = st->MaxHealth() - st->firstHPleft; + bte.val = st->MaxHealth() - st->getFirstHPleft(); } if (bte.val) //anything to heal sendAndApply(&bte); @@ -4551,7 +4570,7 @@ void CGameHandler::stackTurnTrigger(const CStack *st) } BonusList bl = *(st->getBonuses(Selector::type(Bonus::ENCHANTER))); int side = gs->curB->whatSide(st->owner); - if (st->casts && !gs->curB->sides.at(side).enchanterCounter) + if(st->canCast() && !gs->curB->sides.at(side).enchanterCounter) { bool cast = false; while (!bl.empty() && !cast) @@ -5216,7 +5235,7 @@ void CGameHandler::attackCasting(const BattleAttack & bat, Bonus::BonusType atta const CStack * oneOfAttacked = nullptr; for (auto & elem : bat.bsa) { - if (elem.newAmount > 0 && !elem.isSecondary()) //apply effects only to first target stack if it's alive + if ((elem.newHealth.fullUnits > 0 || elem.newHealth.firstHPleft > 0) && !elem.isSecondary()) //apply effects only to first target stack if it's alive { oneOfAttacked = gs->curB->battleGetStackByID(elem.stackAttacked); break; @@ -5282,7 +5301,10 @@ void CGameHandler::handleAfterAttackCasting(const BattleAttack & bat) if (!attacker || bat.bsa.empty()) // can be already dead return; - const CStack *defender = gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked); + const CStack * defender = gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked); + + if(!defender) + return;//already dead auto cast = [=](SpellID spellID, int power) { @@ -5291,7 +5313,7 @@ void CGameHandler::handleAfterAttackCasting(const BattleAttack & bat) BattleSpellCastParameters parameters(gs->curB, attacker, spell); parameters.spellLvl = 0; parameters.effectLevel = 0; - parameters.aimToStack(gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)); + parameters.aimToStack(defender); parameters.effectPower = power; parameters.mode = ECastingMode::AFTER_ATTACK_CASTING; parameters.cast(spellEnv); @@ -5299,13 +5321,13 @@ void CGameHandler::handleAfterAttackCasting(const BattleAttack & bat) attackCasting(bat, Bonus::SPELL_AFTER_ATTACK, attacker); - if (bat.bsa.at(0).newAmount <= 0) + if(!defender->alive()) { //don't try death stare or acid breath on dead stack (crash!) return; } - if (attacker->hasBonusOfType(Bonus::DEATH_STARE) && bat.bsa.size()) + if(attacker->hasBonusOfType(Bonus::DEATH_STARE)) { // mechanics of Death Stare as in H3: // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution @@ -5314,45 +5336,50 @@ void CGameHandler::handleAfterAttackCasting(const BattleAttack & bat) double chanceToKill = attacker->valOfBonuses(Bonus::DEATH_STARE, 0) / 100.0f; vstd::amin(chanceToKill, 1); //cap at 100% - std::binomial_distribution<> distribution(attacker->count, chanceToKill); + std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); int staredCreatures = distribution(getRandomGenerator().getStdGenerator()); double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 - int maxToKill = (attacker->count + cap - 1) / cap; //not much more than chance * count + int maxToKill = (attacker->getCount() + cap - 1) / cap; //not much more than chance * count vstd::amin(staredCreatures, maxToKill); - staredCreatures += (attacker->level() * attacker->valOfBonuses(Bonus::DEATH_STARE, 1)) / gs->curB->battleGetStackByID(bat.bsa.at(0).stackAttacked)->level(); - if (staredCreatures) + staredCreatures += (attacker->level() * attacker->valOfBonuses(Bonus::DEATH_STARE, 1)) / defender->level(); + if(staredCreatures) { - if (bat.bsa.at(0).newAmount > 0) //TODO: death stare was not originally available for multiple-hex attacks, but... - cast(SpellID::DEATH_STARE, staredCreatures); + //TODO: death stare was not originally available for multiple-hex attacks, but... + cast(SpellID::DEATH_STARE, staredCreatures); } } + if(!defender->alive()) + return; + int acidDamage = 0; TBonusListPtr acidBreath = attacker->getBonuses(Selector::type(Bonus::ACID_BREATH)); - for (const std::shared_ptr b : *acidBreath) + for(const std::shared_ptr b : *acidBreath) { - if (b->additionalInfo > getRandomGenerator().nextInt(99)) + if(b->additionalInfo > getRandomGenerator().nextInt(99)) acidDamage += b->val; } - if (acidDamage) - { - cast(SpellID::ACID_BREATH_DAMAGE, acidDamage * attacker->count); - } - if (attacker->hasBonusOfType(Bonus::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability + if(acidDamage) + cast(SpellID::ACID_BREATH_DAMAGE, acidDamage * attacker->getCount()); + + if(!defender->alive()) + return; + + if(attacker->hasBonusOfType(Bonus::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability { double chanceToTrigger = attacker->valOfBonuses(Bonus::TRANSMUTATION) / 100.0f; vstd::amin(chanceToTrigger, 1); //cap at 100% - if (getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + if(getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) return; int bonusAdditionalInfo = attacker->getBonus(Selector::type(Bonus::TRANSMUTATION))->additionalInfo; - if (defender->getCreature()->idNumber == bonusAdditionalInfo || + if(defender->getCreature()->idNumber == bonusAdditionalInfo || (bonusAdditionalInfo == -1 && defender->getCreature()->idNumber == attacker->getCreature()->idNumber)) return; @@ -5360,17 +5387,15 @@ void CGameHandler::handleAfterAttackCasting(const BattleAttack & bat) resurrectInfo.pos = defender->position; resurrectInfo.side = defender->side; - if (bonusAdditionalInfo != -1) + if(bonusAdditionalInfo != -1) resurrectInfo.creID = (CreatureID)bonusAdditionalInfo; else resurrectInfo.creID = attacker->getCreature()->idNumber; - if (attacker->hasBonusOfType((Bonus::TRANSMUTATION), 0)) - { - resurrectInfo.amount = std::max((defender->count * defender->MaxHealth()) / resurrectInfo.creID.toCreature()->MaxHealth(), 1u); - } + if(attacker->hasBonusOfType((Bonus::TRANSMUTATION), 0)) + resurrectInfo.amount = std::max((defender->getCount() * defender->MaxHealth()) / resurrectInfo.creID.toCreature()->MaxHealth(), 1u); else if (attacker->hasBonusOfType((Bonus::TRANSMUTATION), 1)) - resurrectInfo.amount = defender->count; + resurrectInfo.amount = defender->getCount(); else return; //wrong subtype @@ -5647,7 +5672,7 @@ void CGameHandler::runBattle() if (accessibility.accessible(hex, guardianIsBig, stack->side)) //without this multiple creatures can occupy one hex { BattleStackAdded newStack; - newStack.amount = std::max(1, (int)(stack->count * 0.01 * summonInfo->val)); + newStack.amount = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); newStack.creID = creatureData.num; newStack.side = stack->side; newStack.summoned = true; @@ -6308,36 +6333,34 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, Battl heroWithDeadCommander = ObjectInstanceID(); PlayerColor color = army->tempOwner; - if (color == PlayerColor::UNFLAGGABLE) + if(color == PlayerColor::UNFLAGGABLE) color = PlayerColor::NEUTRAL; - for (CStack *st : bat->stacks) + for(CStack * st : bat->stacks) { - if (vstd::contains(st->state, EBattleStackState::SUMMONED)) //don't take into account temporary summoned stacks + if(vstd::contains(st->state, EBattleStackState::SUMMONED)) //don't take into account temporary summoned stacks continue; - if (st->owner != color) //remove only our stacks + if(st->owner != color) //remove only our stacks continue; logGlobal->debug("Calculating casualties for %s", st->nodeName()); - //FIXME: this info is also used in BattleInfo::calculateCasualties, refactor - st->count = std::max (0, st->count - st->resurrected); + st->health.takeResurrected(); - if (st->slot == SlotID::ARROW_TOWERS_SLOT) + if(st->slot == SlotID::ARROW_TOWERS_SLOT) { - //do nothing logGlobal->debug("Ignored arrow towers stack."); } - else if (st->slot == SlotID::WAR_MACHINES_SLOT) + else if(st->slot == SlotID::WAR_MACHINES_SLOT) { auto warMachine = st->type->warMachine; - if (warMachine == ArtifactID::NONE) + if(warMachine == ArtifactID::NONE) { logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName()); } //catapult artifact remain even if "creature" killed in siege - else if (warMachine != ArtifactID::CATAPULT && !st->count) + else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0) { logGlobal->debug("War machine has been destroyed"); auto hero = dynamic_ptr_cast (army); @@ -6347,16 +6370,16 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, Battl logGlobal->error("War machine in army without hero"); } } - else if (st->slot == SlotID::SUMMONED_SLOT_PLACEHOLDER) + else if(st->slot == SlotID::SUMMONED_SLOT_PLACEHOLDER) { - if (st->alive() && st->count > 0) + if(st->alive() && st->getCount() > 0) { - logGlobal->debug("Permanently summoned %d units.", st->count); + logGlobal->debug("Permanently summoned %d units.", st->getCount()); const CreatureID summonedType = st->type->idNumber; - summoned[summonedType] += st->count; + summoned[summonedType] += st->getCount(); } } - else if (st->slot == SlotID::COMMANDER_SLOT_PLACEHOLDER) + else if(st->slot == SlotID::COMMANDER_SLOT_PLACEHOLDER) { if (nullptr == st->base) { @@ -6365,10 +6388,10 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, Battl else { auto c = dynamic_cast (st->base); - if (c) + if(c) { auto h = dynamic_cast (army); - if (h && h->commander == c && (st->count == 0 || !st->alive())) + if(h && h->commander == c && (st->getCount() == 0 || !st->alive())) { logGlobal->debug("Commander is dead."); heroWithDeadCommander = army->id; //TODO: unify commander handling @@ -6378,25 +6401,26 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, Battl logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName()); } } - else if (st->base && !army->slotEmpty(st->slot)) + else if(st->base && !army->slotEmpty(st->slot)) { - if (st->count == 0 || !st->alive()) + logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->slot)); + if(st->getCount() == 0 || !st->alive()) { logGlobal->debug("Stack has been destroyed."); StackLocation sl(army, st->slot); newStackCounts.push_back(TStackAndItsNewCount(sl, 0)); } - else if (st->count < army->getStackCount(st->slot)) + else if(st->getCount() < army->getStackCount(st->slot)) { - logGlobal->debug("Stack lost %d units.", army->getStackCount(st->slot) - st->count); + logGlobal->debug("Stack lost %d units.", army->getStackCount(st->slot) - st->getCount()); StackLocation sl(army, st->slot); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->count)); + newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); } - else if (st->count > army->getStackCount(st->slot)) + else if(st->getCount() > army->getStackCount(st->slot)) { - logGlobal->debug("Stack gained %d units.", st->count - army->getStackCount(st->slot)); + logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->slot)); StackLocation sl(army, st->slot); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->count)); + newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); } } else diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ee6f4ed7d..9b37330e2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,6 +13,7 @@ set(test_SRCS CMemoryBufferTest.cpp CVcmiTestConfig.cpp MapComparer.cpp + battle/CHealthTest.cpp ) set(test_HEADERS diff --git a/test/Test.cbp b/test/Test.cbp index 0b30c8ccc..90b818e37 100644 --- a/test/Test.cbp +++ b/test/Test.cbp @@ -76,6 +76,7 @@