1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-06-15 00:05:02 +02:00

Unified CStack ammo, casts and counterattacks

* it is possible now to add casts and shoots OTF (f.e. with spell bonus)

Centralized stack 'ammo' loading from bonus system.
* introduced small proxy class for local bonus cache
(no need to use global cache if particular selector used on node only in one place)
* handle killing resurrected creatures
* use IBonusBearer::MaxHealth() where possible
* Fixed https://bugs.vcmi.eu/view.php?id=2486
* Possible fix for 0 HP after resurrection.
* Hack-fixed https://bugs.vcmi.eu/view.php?id=2584
* Unified CStack health API
* Use CHealth for CStack count and health points
* increased SERIALIZATION_VERSION
This commit is contained in:
AlexVinS
2017-07-04 14:24:46 +03:00
parent 3634af10ba
commit 4f14f22d3a
33 changed files with 1128 additions and 636 deletions

View File

@ -9,68 +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)
///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 CStack * Owner):
owner(Owner)
{
reset();
}
CHealth::CHealth(const CHealth & other):
owner(other.owner),
firstHPleft(other.firstHPleft),
fullUnits(other.fullUnits),
resurrected(other.resurrected)
{
}
void CHealth::init(const int32_t baseAmount)
{
reset();
fullUnits = baseAmount > 1 ? baseAmount - 1 : 0;
firstHPleft = baseAmount > 0 ? owner->MaxHealth() : 0;
}
void CHealth::addResurrected(int32_t amount)
{
resurrected += amount;
vstd::amax(resurrected, 0);
}
int64_t CHealth::available() const
{
return static_cast<int64_t>(firstHPleft) + owner->MaxHealth() * fullUnits;
}
int64_t CHealth::total() const
{
return static_cast<int64_t>(owner->MaxHealth()) * owner->baseAmount;
}
void CHealth::damage(int32_t & amount)
{
if(owner->isClone())
{
// block ability should not kill clone (0 damage)
if(amount > 0)
reset();
return;
}
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->MaxHealth();
const int32_t oldCount = getCount();
int32_t maxHeal = std::numeric_limits<int32_t>::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 totalHealth = total();
totalHealth += amount;
setFromTotal(totalHealth);
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->MaxHealth();
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.stackId = owner->ID;
info.firstHPleft = firstHPleft;
info.fullUnits = fullUnits;
info.resurrected = resurrected;
}
void CHealth::takeResurrected()
{
int64_t totalHealth = total();
totalHealth -= resurrected * owner->MaxHealth();
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),
counterAttacksPerformed(0),counterAttacksTotalCache(0), cloneID(-1),
firstHPleft(-1), position(), shots(0), casts(0), resurrected(0)
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(baseAmount); //???
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)
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)
counterAttacks(this), shots(this), casts(this), health(this), cloneID(-1),
position()
{
type = stack->type;
count = baseAmount = stack->count;
baseAmount = stack->count;
health.init(baseAmount); //???
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)
{
battle = battleInfo;
assert(type);
assert(getParentNodes().size());
firstHPleft = MaxHealth();
shots = getCreature()->valOfBonuses(Bonus::SHOTS);
counterAttacksPerformed = 0;
counterAttacksTotalCache = 0;
casts = valOfBonuses(Bonus::CASTS);
resurrected = 0;
exportBonuses();
if(base) //stack originating from "real" stack in garrison -> attach to it
{
attachTo(const_cast<CStackInstance *>(base));
}
else //attach directly to obj to which stack belongs and creature type
{
CArmedInstance * army = battle->battleGetArmyObject(side);
attachTo(army);
attachTo(const_cast<CCreature *>(type));
}
shots.reset();
counterAttacks.reset();
casts.reset();
health.init(baseAmount);
cloneID = -1;
}
@ -117,6 +414,26 @@ bool CStack::canMove( int turn /*= 0*/ ) const
&& !hasBonus(Selector::type(Bonus::NOT_ACTIVE).And(Selector::turns(turn))); //eg. Ammo Cart or blinded creature
}
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)
@ -282,13 +599,13 @@ const CGHeroInstance * CStack::getMyHero() const
ui32 CStack::totalHealth() const
{
return ((count > 0) ? MaxHealth() * (count-1) : 0) + firstHPleft;//do not hide possible invalid firstHPleft for dead stack
return health.available();//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 +617,60 @@ std::string CStack::nodeName() const
return oss.str();
}
std::pair<int,int> CStack::countKilledByAttack(int damageReceived) const
CHealth CStack::healthAfterAttacked(int32_t & damage) const
{
int newRemainingHP = 0;
int killedCount = damageReceived / MaxHealth();
unsigned damageFirst = damageReceived % MaxHealth();
if (damageReceived && vstd::contains(state, EBattleStackState::CLONED)) // block ability should not kill clone (0 damage)
{
killedCount = count;
}
else
{
if( firstHPleft <= damageFirst )
{
killedCount++;
newRemainingHP = firstHPleft + MaxHealth() - damageFirst;
}
else
{
newRemainingHP = firstHPleft - damageFirst;
}
}
if(killedCount == count)
newRemainingHP = 0;
return std::make_pair(killedCount, newRemainingHP);
CHealth res = health;
res.damage(damage);
return res;
}
void CStack::prepareAttacked(BattleStackAttacked &bsa, CRandomGenerator & rand, boost::optional<int> 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 = customHealth;
afterAttack.damage(bsa.damageAmount);
bsa.killedAmount = customHealth.getCount() - afterAttack.getCount();
afterAttack.toInfo(bsa.newHealth);
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,27 +684,21 @@ 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
@ -405,31 +711,18 @@ bool CStack::isMeleeAttackPossible(const CStack * attacker, const CStack * defen
}
bool CStack::ableToRetaliate() const //FIXME: crash after clone is killed
bool CStack::ableToRetaliate() const
{
return alive()
&& (counterAttacksPerformed < counterAttacksTotal() || hasBonusOfType(Bonus::UNLIMITED_RETALIATIONS))
&& (counterAttacks.canUse() || 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;
}
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 +735,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,7 +752,7 @@ bool CStack::isTurret() const
bool CStack::canBeHealed() const
{
return firstHPleft < MaxHealth()
return getFirstHPleft() < MaxHealth()
&& isValidTarget()
&& !hasBonusOfType(Bonus::SIEGE_WEAPON);
}
@ -467,26 +765,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<ui32>(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,20 +788,20 @@ 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
@ -527,7 +812,7 @@ const PlayerColor CStack::getOwner() const
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<const CStack*> & attacked, MetaString & text) const
@ -537,3 +822,47 @@ void CStack::getCastDescription(const CSpell * spell, const std::vector<const CS
getCasterName(text);
text.addReplacement(MetaString::SPELL_NAME, spell->id.toEnum());
}
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;
}