1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-22 22:13:35 +02:00
vcmi/lib/battle/CUnitState.cpp
Ivan Savenko 3dd4fa2528 Reduce usage of pointers to VLC entities
Final goal (of multiple PR's) is to remove all remaining pointers from
serializeable game state, and replace them with either identifiers or
with shared/unique pointers.

CGTownInstance::town and CGHeroInstance::type members have been removed.
Now this data is computed dynamically using subID member.

VLC entity of a town can now be accessed via following methods:
- getFactionID() returns ID of a faction
- getFaction() returns pointer to a faction
- getTown() returns pointer to a town

VLC entity of a hero can now be accessed via following methods:
- getHeroTypeID() returns ID of a hero
- getHeroClassID() returns ID of a hero class
- getHeroType() returns pointer to a hero
- getHeroClass() returns pointer to a hero class
2024-10-10 12:28:08 +00:00

969 lines
21 KiB
C++

/*
* 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 <vcmi/spells/Spell.h>
#include "../CCreatureHandler.h"
#include "../serializer/JsonDeserializer.h"
#include "../serializer/JsonSerializer.h"
VCMI_LIB_NAMESPACE_BEGIN
namespace battle
{
///CAmmo
CAmmo::CAmmo(const battle::Unit * Owner, CSelector totalSelector):
used(0),
owner(Owner),
totalProxy(Owner, std::move(totalSelector))
{
reset();
}
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 (available() - amount >= 0) || !isLimited();
}
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()(BonusType::SHOTS)),
shooter(Owner, Selector::type()(BonusType::SHOOTER))
{
}
CShots & CShots::operator=(const CShots & other)
{
CAmmo::operator=(other);
shooter = other.shooter;
return *this;
}
bool CShots::isLimited() const
{
return !shooter.getHasBonus() || !env->unitHasAmmoCart(owner);
}
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()(BonusType::CASTS))
{
}
///CRetaliations
CRetaliations::CRetaliations(const battle::Unit * Owner)
: CAmmo(Owner, Selector::type()(BonusType::ADDITIONAL_RETALIATION)),
totalCache(0),
noRetaliation(Owner, Selector::type()(BonusType::SIEGE_WEAPON).Or(Selector::type()(BonusType::HYPNOTIZED)).Or(Selector::type()(BonusType::NO_RETALIATION))),
unlimited(Owner, Selector::type()(BonusType::UNLIMITED_RETALIATIONS))
{
}
bool CRetaliations::isLimited() const
{
return !unlimited.getHasBonus() || noRetaliation.getHasBonus();
}
int32_t CRetaliations::total() const
{
if(noRetaliation.getHasBonus())
return 0;
//after dispel 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::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->getMaxHealth() : 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->getMaxHealth() * fullUnits;
}
int64_t CHealth::total() const
{
return static_cast<int64_t>(owner->getMaxHealth()) * 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 -= static_cast<int32_t>(amount);
}
addResurrected(getCount() - oldCount);
}
HealInfo CHealth::heal(int64_t & amount, EHealLevel level, EHealPower power)
{
const int32_t unitHealth = owner->getMaxHealth();
const int32_t oldCount = getCount();
int64_t maxHeal = std::numeric_limits<int64_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, int64_t(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);
return HealInfo(amount, getCount() - oldCount);
}
void CHealth::setFromTotal(const int64_t totalHealth)
{
const int32_t unitHealth = owner->getMaxHealth();
firstHPleft = totalHealth % unitHealth;
fullUnits = static_cast<int32_t>(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->getMaxHealth();
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),
castSpellThisTurn(false),
ghost(false),
ghostPending(false),
movedThisRound(false),
summoned(false),
waiting(false),
waitedThisTurn(false),
casts(this),
counterAttacks(this),
health(this),
shots(this),
totalAttacks(this, Selector::type()(BonusType::ADDITIONAL_ATTACK), 1),
minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin)), 0),
maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax)), 0),
attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)), 0),
defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)), 0),
inFrenzy(this, Selector::type()(BonusType::IN_FRENZY)),
cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::CLONE))))),
cloneID(-1)
{
}
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;
castSpellThisTurn = other.castSpellThisTurn;
ghost = other.ghost;
ghostPending = other.ghostPending;
movedThisRound = other.movedThisRound;
summoned = other.summoned;
waiting = other.waiting;
waitedThisTurn = other.waitedThisTurn;
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<int32_t>(creatureId().toEnum());
}
CreatureID CUnitState::creatureId() const
{
return unitType()->getId();
}
int32_t CUnitState::creatureLevel() const
{
return static_cast<int32_t>(unitType()->getLevel());
}
bool CUnitState::doubleWide() const
{
return unitType()->isDoubleWide();
}
int32_t CUnitState::creatureCost() const
{
return unitType()->getRecruitCost(EGameResID::GOLD);
}
int32_t CUnitState::creatureIconIndex() const
{
return unitType()->getIconIndex();
}
FactionID CUnitState::getFactionID() const
{
return unitType()->getFactionID();
}
int32_t CUnitState::getCasterUnitId() const
{
return static_cast<int32_t>(unitId());
}
const CGHeroInstance * CUnitState::getHeroCaster() const
{
return nullptr;
}
int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool) const
{
int32_t skill = valOfBonuses(Selector::typeSubtype(BonusType::SPELLCASTER, BonusSubtypeID(spell->getId())));
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;
}
int32_t CUnitState::getEffectLevel(const spells::Spell * spell) const
{
return getSpellSchoolLevel(spell);
}
int32_t CUnitState::getEffectPower(const spells::Spell * spell) const
{
return valOfBonuses(BonusType::CREATURE_SPELL_POWER) * getCount() / 100;
}
int32_t CUnitState::getEnchantPower(const spells::Spell * spell) const
{
int32_t res = valOfBonuses(BonusType::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<int64_t>(getCount()) * valOfBonuses(BonusType::SPECIFIC_SPELL_POWER, BonusSubtypeID(spell->getId()));
}
PlayerColor CUnitState::getCasterOwner() 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<const Unit *> & attacked, MetaString & text) const
{
text.appendLocalString(EMetaText::GENERAL_TXT, 565);//The %s casts %s
//todo: use text 566 for single creature
getCasterName(text);
text.replaceName(spell->getId());
}
int32_t CUnitState::manaLimit() const
{
return 0; //TODO: creature casting with mana mode (for mods)
}
bool CUnitState::ableToRetaliate() const
{
return alive()
&& counterAttacks.canUse();
}
bool CUnitState::alive() const
{
return health.getCount() > 0;
}
bool CUnitState::isGhost() const
{
return ghost;
}
bool CUnitState::isFrozen() const
{
return hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::STONE_GAZE))), Selector::all);
}
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) && !castSpellThisTurn;//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()(BonusType::STACKS_SPEED).And(Selector::turns(turn)));
}
uint8_t CUnitState::getRangedFullDamageDistance() const
{
if(!isShooter())
return 0;
uint8_t rangedFullDamageDistance = GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE;
// overwrite full ranged damage distance with the value set in Additional info field of LIMITED_SHOOTING_RANGE bonus
if(this->hasBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)))
{
auto bonus = this->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE));
if(bonus != nullptr && bonus->additionalInfo != CAddInfo::NONE)
rangedFullDamageDistance = bonus->additionalInfo[0];
}
return rangedFullDamageDistance;
}
uint8_t CUnitState::getShootingRangeDistance() const
{
if(!isShooter())
return 0;
uint8_t shootingRangeDistance = GameConstants::BATTLE_SHOOTING_RANGE_DISTANCE;
// overwrite full ranged damage distance with the value set in Additional info field of LIMITED_SHOOTING_RANGE bonus
if(this->hasBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)))
{
auto bonus = this->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE));
if(bonus != nullptr)
shootingRangeDistance = bonus->val;
}
return shootingRangeDistance;
}
bool CUnitState::canMove(int turn) const
{
return alive() && !hasBonus(Selector::type()(BonusType::NOT_ACTIVE).And(Selector::turns(turn))); //eg. Ammo Cart or blinded creature
}
bool CUnitState::defended(int turn) const
{
return !turn && defending;
}
bool CUnitState::moved(int turn) const
{
if(!turn && !waiting)
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;
}
BattlePhases::Type CUnitState::battleQueuePhase(int turn) const
{
if(turn <= 0 && waited()) //consider waiting state only for ongoing round
{
if(hadMorale)
return BattlePhases::WAIT_MORALE;
else
return BattlePhases::WAIT;
}
else if(creatureIndex() == CreatureID::CATAPULT || isTurret()) //catapult and turrets are first
{
return BattlePhases::SIEGE;
}
else
{
return BattlePhases::NORMAL;
}
}
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 = static_cast<double>(inFrenzy->totalValue()) / 100;
frenzyPower *= static_cast<double>(ranged ? defence.getRangedValue() : defence.getMeleeValue());
ret += static_cast<int>(frenzyPower);
}
vstd::amax(ret, 0);
return ret;
}
int CUnitState::getDefense(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<Unit> CUnitState::acquire() const
{
auto ret = std::make_shared<CUnitStateDetached>(this, this);
ret->localInit(env);
*ret = *this;
return ret;
}
std::shared_ptr<CUnitState> CUnitState::acquireState() const
{
auto ret = std::make_shared<CUnitStateDetached>(this, this);
ret->localInit(env);
*ret = *this;
return ret;
}
void CUnitState::serializeJson(JsonSerializeFormat & handler)
{
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("castSpellThisTurn", castSpellThisTurn);
handler.serializeBool("ghost", ghost);
handler.serializeBool("ghostPending", ghostPending);
handler.serializeBool("moved", movedThisRound);
handler.serializeBool("summoned", summoned);
handler.serializeBool("waiting", waiting);
handler.serializeBool("waitedThisTurn", waitedThisTurn);
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;
castSpellThisTurn = false;
ghost = false;
ghostPending = false;
movedThisRound = false;
summoned = false;
waiting = false;
waitedThisTurn = 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 = 0;
health.reset();
}
}
else
{
health.damage(amount);
}
bool disintegrate = hasBonusOfType(BonusType::DISINTEGRATE);
if(health.available() <= 0 && (cloned || summoned || disintegrate))
ghostPending = true;
}
HealInfo 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
return health.heal(amount, level, power);
return {};
}
void CUnitState::afterAttack(bool ranged, bool counter)
{
if(counter)
counterAttacks.use();
if(ranged)
shots.use();
}
void CUnitState::afterNewRound()
{
defending = false;
waiting = false;
waitedThisTurn = false;
movedThisRound = false;
hadMorale = false;
castSpellThisTurn = 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_):
unit(unit_),
bonus(bonus_)
{
}
TConstBonusListPtr CUnitStateDetached::getAllBonuses(const CSelector & selector, const CSelector & limit, const std::string & cachingStr) const
{
return bonus->getAllBonuses(selector, limit, 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();
}
BattleSide 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(ServerCallback * 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<CUnitStateDetached *>(this)->casts.use(spellCost);
}
}
VCMI_LIB_NAMESPACE_END