/* * CUnitState.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CUnitState.h" #include "../NetPacks.h" #include "../CCreatureHandler.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" namespace battle { CTotalsProxy::CTotalsProxy(const IBonusBearer * Target, CSelector Selector, int InitialValue) : target(Target), selector(Selector), initialValue(InitialValue), meleeCachedLast(0), meleeValue(0), rangedCachedLast(0), rangedValue(0) { } CTotalsProxy::CTotalsProxy(const CTotalsProxy & other) : target(other.target), selector(other.selector), initialValue(other.initialValue), meleeCachedLast(other.meleeCachedLast), meleeValue(other.meleeValue), rangedCachedLast(other.rangedCachedLast), rangedValue(other.rangedValue) { } CTotalsProxy & CTotalsProxy::operator=(const CTotalsProxy & other) { initialValue = other.initialValue; meleeCachedLast = other.meleeCachedLast; meleeValue = other.meleeValue; rangedCachedLast = other.rangedCachedLast; rangedValue = other.rangedValue; return *this; } int CTotalsProxy::getMeleeValue() const { static const auto limit = Selector::effectRange(Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_MELEE_FIGHT)); const auto treeVersion = target->getTreeVersion(); if(treeVersion != meleeCachedLast) { auto bonuses = target->getBonuses(selector, limit); meleeValue = initialValue + bonuses->totalValue(); meleeCachedLast = treeVersion; } return meleeValue; } int CTotalsProxy::getRangedValue() const { static const auto limit = Selector::effectRange(Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_DISTANCE_FIGHT)); const auto treeVersion = target->getTreeVersion(); if(treeVersion != rangedCachedLast) { auto bonuses = target->getBonuses(selector, limit); rangedValue = initialValue + bonuses->totalValue(); rangedCachedLast = treeVersion; } return rangedValue; } ///CCheckProxy CCheckProxy::CCheckProxy(const IBonusBearer * Target, CSelector Selector) : target(Target), selector(Selector), cachedLast(0), hasBonus(false) { } CCheckProxy::CCheckProxy(const CCheckProxy & other) : target(other.target), selector(other.selector), cachedLast(other.cachedLast), hasBonus(other.hasBonus) { } bool CCheckProxy::getHasBonus() const { const auto treeVersion = target->getTreeVersion(); if(treeVersion != cachedLast) { hasBonus = target->hasBonus(selector); cachedLast = treeVersion; } return hasBonus; } ///CAmmo CAmmo::CAmmo(const battle::Unit * Owner, CSelector totalSelector) : used(0), owner(Owner), totalProxy(Owner, totalSelector) { reset(); } CAmmo::CAmmo(const CAmmo & other) : used(other.used), owner(other.owner), totalProxy(other.totalProxy) { } CAmmo & CAmmo::operator= (const CAmmo & other) { used = other.used; totalProxy = other.totalProxy; return *this; } int32_t CAmmo::available() const { return total() - used; } bool CAmmo::canUse(int32_t amount) const { return !isLimited() || (available() - amount >= 0); } bool CAmmo::isLimited() const { return true; } void CAmmo::reset() { used = 0; } int32_t CAmmo::total() const { return totalProxy->totalValue(); } void CAmmo::use(int32_t amount) { if(!isLimited()) return; if(available() - amount < 0) { logGlobal->error("Stack ammo overuse. total: %d, used: %d, requested: %d", total(), used, amount); used += available(); } else used += amount; } void CAmmo::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("used", used, 0); } ///CShots CShots::CShots(const battle::Unit * Owner) : CAmmo(Owner, Selector::type(Bonus::SHOTS)), shooter(Owner, Selector::type(Bonus::SHOOTER)) { } CShots::CShots(const CShots & other) : CAmmo(other), env(other.env), shooter(other.shooter) { } CShots & CShots::operator=(const CShots & other) { CAmmo::operator=(other); shooter = other.shooter; return *this; } bool CShots::isLimited() const { return !env->unitHasAmmoCart(owner) || !shooter.getHasBonus(); } void CShots::setEnv(const IUnitEnvironment * env_) { env = env_; } int32_t CShots::total() const { if(shooter.getHasBonus()) return CAmmo::total(); else return 0; } ///CCasts CCasts::CCasts(const battle::Unit * Owner): CAmmo(Owner, Selector::type(Bonus::CASTS)) { } CCasts::CCasts(const CCasts & other) : CAmmo(other) { } CCasts & CCasts::operator=(const CCasts & other) { CAmmo::operator=(other); return *this; } ///CRetaliations CRetaliations::CRetaliations(const battle::Unit * Owner) : CAmmo(Owner, Selector::type(Bonus::ADDITIONAL_RETALIATION)), totalCache(0), noRetaliation(Owner, Selector::type(Bonus::SIEGE_WEAPON).Or(Selector::type(Bonus::HYPNOTIZED)).Or(Selector::type(Bonus::NO_RETALIATION))), unlimited(Owner, Selector::type(Bonus::UNLIMITED_RETALIATIONS)) { } CRetaliations::CRetaliations(const CRetaliations & other) : CAmmo(other), totalCache(other.totalCache), noRetaliation(other.noRetaliation), unlimited(other.unlimited) { } CRetaliations & CRetaliations::operator=(const CRetaliations & other) { CAmmo::operator=(other); totalCache = other.totalCache; noRetaliation = other.noRetaliation; unlimited = other.unlimited; return *this; } bool CRetaliations::isLimited() const { return !unlimited.getHasBonus() || noRetaliation.getHasBonus(); } int32_t CRetaliations::total() const { if(noRetaliation.getHasBonus()) return 0; //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; } void CRetaliations::serializeJson(JsonSerializeFormat & handler) { CAmmo::serializeJson(handler); //we may be serialized in the middle of turn handler.serializeInt("totalCache", totalCache, 0); } ///CHealth CHealth::CHealth(const battle::Unit * Owner): owner(Owner) { reset(); } CHealth::CHealth(const CHealth & other): owner(other.owner), firstHPleft(other.firstHPleft), fullUnits(other.fullUnits), resurrected(other.resurrected) { } CHealth & CHealth::operator=(const CHealth & other) { //do not change owner firstHPleft = other.firstHPleft; fullUnits = other.fullUnits; resurrected = other.resurrected; return *this; } void CHealth::init() { reset(); fullUnits = owner->unitBaseAmount() > 1 ? owner->unitBaseAmount() - 1 : 0; firstHPleft = owner->unitBaseAmount() > 0 ? owner->MaxHealth() : 0; } void CHealth::addResurrected(int32_t amount) { resurrected += amount; vstd::amax(resurrected, 0); } int64_t CHealth::available() const { return static_cast(firstHPleft) + owner->MaxHealth() * fullUnits; } int64_t CHealth::total() const { return static_cast(owner->MaxHealth()) * owner->unitBaseAmount(); } void CHealth::damage(int64_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(int64_t & amount, EHealLevel level, EHealPower power) { const int32_t unitHealth = owner->MaxHealth(); const int32_t oldCount = getCount(); int64_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->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::takeResurrected() { if(resurrected != 0) { int64_t totalHealth = available(); totalHealth -= resurrected * owner->MaxHealth(); vstd::amax(totalHealth, 0); setFromTotal(totalHealth); resurrected = 0; } } void CHealth::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("firstHPleft", firstHPleft, 0); handler.serializeInt("fullUnits", fullUnits, 0); handler.serializeInt("resurrected", resurrected, 0); } ///CUnitState CUnitState::CUnitState() : env(nullptr), cloned(false), defending(false), defendingAnim(false), drainedMana(false), fear(false), hadMorale(false), ghost(false), ghostPending(false), movedThisRound(false), summoned(false), waiting(false), casts(this), counterAttacks(this), health(this), shots(this), totalAttacks(this, Selector::type(Bonus::ADDITIONAL_ATTACK), 1), minDamage(this, Selector::typeSubtype(Bonus::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(Bonus::CREATURE_DAMAGE, 1)), 0), maxDamage(this, Selector::typeSubtype(Bonus::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(Bonus::CREATURE_DAMAGE, 2)), 0), attack(this, Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::ATTACK), 0), defence(this, Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::DEFENSE), 0), inFrenzy(this, Selector::type(Bonus::IN_FRENZY)), cloneLifetimeMarker(this, Selector::type(Bonus::NONE).And(Selector::source(Bonus::SPELL_EFFECT, SpellID::CLONE))), cloneID(-1), position() { } CUnitState & CUnitState::operator=(const CUnitState & other) { //do not change unit and bonus info cloned = other.cloned; defending = other.defending; defendingAnim = other.defendingAnim; drainedMana = other.drainedMana; fear = other.fear; hadMorale = other.hadMorale; ghost = other.ghost; ghostPending = other.ghostPending; movedThisRound = other.movedThisRound; summoned = other.summoned; waiting = other.waiting; casts = other.casts; counterAttacks = other.counterAttacks; health = other.health; shots = other.shots; totalAttacks = other.totalAttacks; minDamage = other.minDamage; maxDamage = other.maxDamage; attack = other.attack; defence = other.defence; inFrenzy = other.inFrenzy; cloneLifetimeMarker = other.cloneLifetimeMarker; cloneID = other.cloneID; position = other.position; return *this; } int32_t CUnitState::creatureIndex() const { return static_cast(creatureId().toEnum()); } CreatureID CUnitState::creatureId() const { return unitType()->idNumber; } int32_t CUnitState::creatureLevel() const { return static_cast(unitType()->level); } bool CUnitState::doubleWide() const { return unitType()->doubleWide; } int32_t CUnitState::creatureCost() const { return unitType()->cost[Res::GOLD]; } int32_t CUnitState::creatureIconIndex() const { return unitType()->iconIndex; } int32_t CUnitState::getCasterUnitId() const { return static_cast(unitId()); } ui8 CUnitState::getSpellSchoolLevel(const spells::Spell * spell, int * outSelectedSchool) const { int skill = valOfBonuses(Selector::typeSubtype(Bonus::SPELLCASTER, spell->getIndex())); vstd::abetween(skill, 0, 3); return skill; } int64_t CUnitState::getSpellBonus(const spells::Spell * spell, int64_t base, const Unit * affectedStack) const { //does not have sorcery-like bonuses (yet?) return base; } int64_t CUnitState::getSpecificSpellBonus(const spells::Spell * spell, int64_t base) const { return base; } int CUnitState::getEffectLevel(const spells::Spell * spell) const { return getSpellSchoolLevel(spell); } int CUnitState::getEffectPower(const spells::Spell * spell) const { return valOfBonuses(Bonus::CREATURE_SPELL_POWER) * getCount() / 100; } int CUnitState::getEnchantPower(const spells::Spell * spell) const { int res = valOfBonuses(Bonus::CREATURE_ENCHANT_POWER); if(res <= 0) res = 3;//default for creatures return res; } int64_t CUnitState::getEffectValue(const spells::Spell * spell) const { return static_cast(getCount()) * valOfBonuses(Bonus::SPECIFIC_SPELL_POWER, spell->getIndex()); } const PlayerColor CUnitState::getOwner() const { return env->unitEffectiveOwner(this); } void CUnitState::getCasterName(MetaString & text) const { //always plural name in case of spell cast. addNameReplacement(text, true); } void CUnitState::getCastDescription(const spells::Spell * 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->getIndex()); } bool CUnitState::ableToRetaliate() const { return alive() && counterAttacks.canUse(); } bool CUnitState::alive() const { return health.getCount() > 0; } bool CUnitState::isGhost() const { return ghost; } bool CUnitState::isValidTarget(bool allowDead) const { return (alive() || (allowDead && isDead())) && getPosition().isValid() && !isTurret(); } bool CUnitState::isClone() const { return cloned; } bool CUnitState::hasClone() const { return cloneID > 0; } bool CUnitState::canCast() const { return casts.canUse(1);//do not check specific cast abilities here } bool CUnitState::isCaster() const { return casts.total() > 0;//do not check specific cast abilities here } bool CUnitState::canShoot() const { return shots.canUse(1); } bool CUnitState::isShooter() const { return shots.total() > 0; } int32_t CUnitState::getKilled() const { int32_t res = unitBaseAmount() - health.getCount() + health.getResurrected(); vstd::amax(res, 0); return res; } int32_t CUnitState::getCount() const { return health.getCount(); } int32_t CUnitState::getFirstHPleft() const { return health.getFirstHPleft(); } int64_t CUnitState::getAvailableHealth() const { return health.available(); } int64_t CUnitState::getTotalHealth() const { return health.total(); } BattleHex CUnitState::getPosition() const { return position; } void CUnitState::setPosition(BattleHex hex) { position = hex; } int32_t CUnitState::getInitiative(int turn) const { return valOfBonuses(Selector::type(Bonus::STACKS_SPEED).And(Selector::turns(turn))); } bool CUnitState::canMove(int turn) const { return alive() && !hasBonus(Selector::type(Bonus::NOT_ACTIVE).And(Selector::turns(turn))); //eg. Ammo Cart or blinded creature } bool CUnitState::defended(int turn) const { if(!turn) return defending; else return false; } bool CUnitState::moved(int turn) const { if(!turn) return movedThisRound; else return false; } bool CUnitState::willMove(int turn) const { return (turn ? true : !defending) && !moved(turn) && canMove(turn); } bool CUnitState::waited(int turn) const { if(!turn) return waiting; else return false; } int CUnitState::battleQueuePhase(int turn) const { if(turn <= 0 && waited()) //consider waiting state only for ongoing round { if(hadMorale) return 2; else return 3; } else if(creatureIndex() == CreatureID::CATAPULT || isTurret()) //catapult and turrets are first { return 0; } else { return 1; } } int CUnitState::getTotalAttacks(bool ranged) const { return ranged ? totalAttacks.getRangedValue() : totalAttacks.getMeleeValue(); } int CUnitState::getMinDamage(bool ranged) const { return ranged ? minDamage.getRangedValue() : minDamage.getMeleeValue(); } int CUnitState::getMaxDamage(bool ranged) const { return ranged ? maxDamage.getRangedValue() : maxDamage.getMeleeValue(); } int CUnitState::getAttack(bool ranged) const { int ret = ranged ? attack.getRangedValue() : attack.getMeleeValue(); if(!inFrenzy->empty()) { double frenzyPower = (double)inFrenzy->totalValue() / 100; frenzyPower *= (double) (ranged ? defence.getRangedValue() : defence.getMeleeValue()); ret += frenzyPower; } vstd::amax(ret, 0); return ret; } int CUnitState::getDefence(bool ranged) const { if(!inFrenzy->empty()) { return 0; } else { int ret = ranged ? defence.getRangedValue() : defence.getMeleeValue(); vstd::amax(ret, 0); return ret; } } std::shared_ptr CUnitState::acquire() const { auto ret = std::make_shared(this, this); ret->localInit(env); *ret = *this; return ret; } std::shared_ptr CUnitState::acquireState() const { auto ret = std::make_shared(this, this); ret->localInit(env); *ret = *this; return ret; } void CUnitState::serializeJson(JsonSerializeFormat & handler) { if(!handler.saving) reset(); handler.serializeBool("cloned", cloned); handler.serializeBool("defending", defending); handler.serializeBool("defendingAnim", defendingAnim); handler.serializeBool("drainedMana", drainedMana); handler.serializeBool("fear", fear); handler.serializeBool("hadMorale", hadMorale); handler.serializeBool("ghost", ghost); handler.serializeBool("ghostPending", ghostPending); handler.serializeBool("moved", movedThisRound); handler.serializeBool("summoned", summoned); handler.serializeBool("waiting", waiting); handler.serializeStruct("casts", casts); handler.serializeStruct("counterAttacks", counterAttacks); handler.serializeStruct("health", health); handler.serializeStruct("shots", shots); handler.serializeInt("cloneID", cloneID); handler.serializeInt("position", position); } void CUnitState::localInit(const IUnitEnvironment * env_) { env = env_; shots.setEnv(env); reset(); health.init(); } void CUnitState::reset() { cloned = false; defending = false; defendingAnim = false; drainedMana = false; fear = false; hadMorale = false; ghost = false; ghostPending = false; movedThisRound = false; summoned = false; waiting = false; casts.reset(); counterAttacks.reset(); health.reset(); shots.reset(); cloneID = -1; position = BattleHex::INVALID; } void CUnitState::save(JsonNode & data) { //TODO: use instance resolver data.clear(); JsonSerializer ser(nullptr, data); ser.serializeStruct("state", *this); } void CUnitState::load(const JsonNode & data) { //TODO: use instance resolver reset(); JsonDeserializer deser(nullptr, data); deser.serializeStruct("state", *this); } void CUnitState::damage(int64_t & amount) { if(cloned) { // block ability should not kill clone (0 damage) if(amount > 0) { amount = 1;//TODO: what should be actual damage against clone? health.reset(); } } else { health.damage(amount); } if(health.available() <= 0 && (cloned || summoned)) ghostPending = true; } void CUnitState::heal(int64_t & amount, EHealLevel level, EHealPower power) { if(level == EHealLevel::HEAL && power == EHealPower::ONE_BATTLE) logGlobal->error("Heal for one battle does not make sense"); else if(cloned) logGlobal->error("Attempt to heal clone"); else health.heal(amount, level, power); } void CUnitState::afterAttack(bool ranged, bool counter) { if(counter) counterAttacks.use(); if(ranged) shots.use(); } void CUnitState::afterNewRound() { defending = false; waiting = false; movedThisRound = false; hadMorale = false; fear = false; drainedMana = false; counterAttacks.reset(); if(alive() && isClone()) { if(!cloneLifetimeMarker.getHasBonus()) makeGhost(); } } void CUnitState::afterGetsTurn() { //if moving second time this round it must be high morale bonus if(movedThisRound) hadMorale = true; } void CUnitState::makeGhost() { health.reset(); ghostPending = true; } void CUnitState::onRemoved() { health.reset(); ghostPending = false; ghost = true; } CUnitStateDetached::CUnitStateDetached(const IUnitInfo * unit_, const IBonusBearer * bonus_) : CUnitState(), unit(unit_), bonus(bonus_) { } const TBonusListPtr CUnitStateDetached::getAllBonuses(const CSelector & selector, const CSelector & limit, const CBonusSystemNode * root, const std::string & cachingStr) const { return bonus->getAllBonuses(selector, limit, root, cachingStr); } int64_t CUnitStateDetached::getTreeVersion() const { return bonus->getTreeVersion(); } CUnitStateDetached & CUnitStateDetached::operator=(const CUnitState & other) { CUnitState::operator=(other); return *this; } uint32_t CUnitStateDetached::unitId() const { return unit->unitId(); } ui8 CUnitStateDetached::unitSide() const { return unit->unitSide(); } const CCreature * CUnitStateDetached::unitType() const { return unit->unitType(); } PlayerColor CUnitStateDetached::unitOwner() const { return unit->unitOwner(); } SlotID CUnitStateDetached::unitSlot() const { return unit->unitSlot(); } int32_t CUnitStateDetached::unitBaseAmount() const { return unit->unitBaseAmount(); } void CUnitStateDetached::spendMana(const spells::PacketSender * server, const int spellCost) const { if(spellCost != 1) logGlobal->warn("Unexpected spell cost %d for creature", spellCost); //this is evil, but //use of netpacks in detached state is an error //non const API is more evil for hero const_cast(this)->casts.use(spellCost); } }