1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-18 17:40:48 +02:00
vcmi/lib/mapObjects/CGHeroInstance.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

1867 lines
51 KiB
C++

/*
* CGHeroInstance.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 "CGHeroInstance.h"
#include <vcmi/ServerCallback.h>
#include <vcmi/spells/Spell.h>
#include <vstd/RNG.h>
#include "../texts/CGeneralTextHandler.h"
#include "../ArtifactUtils.h"
#include "../CHeroHandler.h"
#include "../TerrainHandler.h"
#include "../RoadHandler.h"
#include "../IGameSettings.h"
#include "../CSoundBase.h"
#include "../spells/CSpellHandler.h"
#include "../CSkillHandler.h"
#include "../IGameCallback.h"
#include "../gameState/CGameState.h"
#include "../CCreatureHandler.h"
#include "../mapping/CMap.h"
#include "../StartInfo.h"
#include "CGTownInstance.h"
#include "../entities/faction/CTownHandler.h"
#include "../battle/CBattleInfoEssentials.h"
#include "../campaign/CampaignState.h"
#include "../json/JsonBonus.h"
#include "../pathfinder/TurnInfo.h"
#include "../serializer/JsonSerializeFormat.h"
#include "../mapObjectConstructors/AObjectTypeHandler.h"
#include "../mapObjectConstructors/CObjectClassesHandler.h"
#include "../mapObjects/MiscObjects.h"
#include "../modding/ModScope.h"
#include "../networkPacks/PacksForClient.h"
#include "../networkPacks/PacksForClientBattle.h"
#include "../constants/StringConstants.h"
#include "../battle/Unit.h"
#include "CConfigHandler.h"
VCMI_LIB_NAMESPACE_BEGIN
void CGHeroPlaceholder::serializeJsonOptions(JsonSerializeFormat & handler)
{
serializeJsonOwner(handler);
bool isHeroType = heroType.has_value();
handler.serializeBool("placeholderType", isHeroType, false);
if(!handler.saving)
{
if(isHeroType)
heroType = HeroTypeID::NONE;
else
powerRank = 0;
}
if(isHeroType)
handler.serializeId("heroType", heroType.value(), HeroTypeID::NONE);
else
handler.serializeInt("powerRank", powerRank.value());
}
static int lowestSpeed(const CGHeroInstance * chi)
{
static const CSelector selectorSTACKS_SPEED = Selector::type()(BonusType::STACKS_SPEED);
static const std::string keySTACKS_SPEED = "type_" + std::to_string(static_cast<si32>(BonusType::STACKS_SPEED));
if(!chi->stacksCount())
{
if(chi->commander && chi->commander->alive)
{
return chi->commander->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED);
}
logGlobal->error("Hero %d (%s) has no army!", chi->id.getNum(), chi->getNameTranslated());
return 20;
}
auto i = chi->Slots().begin();
//TODO? should speed modifiers (eg from artifacts) affect hero movement?
int ret = (i++)->second->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED);
for(; i != chi->Slots().end(); i++)
ret = std::min(ret, i->second->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED));
return ret;
}
ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const
{
int64_t ret = GameConstants::BASE_MOVEMENT_COST;
//if there is road both on dest and src tiles - use src road movement cost
if(dest.roadType->getId() != Road::NO_ROAD && from.roadType->getId() != Road::NO_ROAD)
{
ret = from.roadType->movementCost;
}
else if(ti->nativeTerrain != from.terType->getId() &&//the terrain is not native
ti->nativeTerrain != ETerrainId::ANY_TERRAIN && //no special creature bonus
!ti->hasBonusOfType(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(from.terType->getId()))) //no special movement bonus
{
ret = VLC->terrainTypeHandler->getById(from.terType->getId())->moveCost;
ret -= ti->valOfBonuses(BonusType::ROUGH_TERRAIN_DISCOUNT);
if(ret < GameConstants::BASE_MOVEMENT_COST)
ret = GameConstants::BASE_MOVEMENT_COST;
}
return static_cast<ui32>(ret);
}
FactionID CGHeroInstance::getFactionID() const
{
return getHeroClass()->faction;
}
const IBonusBearer* CGHeroInstance::getBonusBearer() const
{
return this;
}
TerrainId CGHeroInstance::getNativeTerrain() const
{
// NOTE: in H3 neutral stacks will ignore terrain penalty only if placed as topmost stack(s) in hero army.
// This is clearly bug in H3 however intended behaviour is not clear.
// Current VCMI behaviour will ignore neutrals in calculations so army in VCMI
// will always have best penalty without any influence from player-defined stacks order
// and army that consist solely from neutral will always be considered to be on native terrain
TerrainId nativeTerrain = ETerrainId::ANY_TERRAIN;
for(const auto & stack : stacks)
{
TerrainId stackNativeTerrain = stack.second->getNativeTerrain(); //consider terrain bonuses e.g. Lodestar.
if(stackNativeTerrain == ETerrainId::NONE)
continue;
if(nativeTerrain == ETerrainId::ANY_TERRAIN)
nativeTerrain = stackNativeTerrain;
else if(nativeTerrain != stackNativeTerrain)
return ETerrainId::NONE;
}
return nativeTerrain;
}
bool CGHeroInstance::isCoastVisitable() const
{
return true;
}
bool CGHeroInstance::isBlockedVisitable() const
{
return true;
}
BattleField CGHeroInstance::getBattlefield() const
{
return BattleField::NONE;
}
ui8 CGHeroInstance::getSecSkillLevel(const SecondarySkill & skill) const
{
for(const auto & elem : secSkills)
if(elem.first == skill)
return elem.second;
return 0;
}
void CGHeroInstance::setSecSkillLevel(const SecondarySkill & which, int val, bool abs)
{
if(getSecSkillLevel(which) == 0)
{
secSkills.emplace_back(which, val);
updateSkillBonus(which, val);
}
else
{
for (auto & elem : secSkills)
{
if(elem.first == which)
{
if(abs)
elem.second = val;
else
elem.second += val;
if(elem.second > 3) //workaround to avoid crashes when same sec skill is given more than once
{
logGlobal->warn("Skill %d increased over limit! Decreasing to Expert.", static_cast<int>(which.toEnum()));
elem.second = 3;
}
updateSkillBonus(which, elem.second); //when we know final value
}
}
}
}
int3 CGHeroInstance::convertToVisitablePos(const int3 & position) const
{
return position - getVisitableOffset();
}
int3 CGHeroInstance::convertFromVisitablePos(const int3 & position) const
{
return position + getVisitableOffset();
}
bool CGHeroInstance::canLearnSkill() const
{
return secSkills.size() < GameConstants::SKILL_PER_HERO;
}
bool CGHeroInstance::canLearnSkill(const SecondarySkill & which) const
{
if ( !canLearnSkill())
return false;
if (!cb->isAllowed(which))
return false;
if (getSecSkillLevel(which) > 0)
return false;
if (getHeroClass()->secSkillProbability.count(which) == 0)
return false;
if (getHeroClass()->secSkillProbability.at(which) == 0)
return false;
return true;
}
int CGHeroInstance::movementPointsRemaining() const
{
return movement;
}
void CGHeroInstance::setMovementPoints(int points)
{
if(getBonusBearer()->hasBonusOfType(BonusType::UNLIMITED_MOVEMENT))
movement = 1000000;
else
movement = std::max(0, points);
}
int CGHeroInstance::movementPointsLimit(bool onLand) const
{
TurnInfo ti(this);
return movementPointsLimitCached(onLand, &ti);
}
int CGHeroInstance::getLowestCreatureSpeed() const
{
return lowestCreatureSpeed;
}
void CGHeroInstance::updateArmyMovementBonus(bool onLand, const TurnInfo * ti) const
{
auto realLowestSpeed = lowestSpeed(this);
if(lowestCreatureSpeed != realLowestSpeed)
{
lowestCreatureSpeed = realLowestSpeed;
//Let updaters run again
treeHasChanged();
ti->updateHeroBonuses(BonusType::MOVEMENT, Selector::subtype()(onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea));
}
}
int CGHeroInstance::movementPointsLimitCached(bool onLand, const TurnInfo * ti) const
{
updateArmyMovementBonus(onLand, ti);
return ti->valOfBonuses(BonusType::MOVEMENT, onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea);
}
CGHeroInstance::CGHeroInstance(IGameCallback * cb)
: CArmedInstance(cb),
tacticFormationEnabled(false),
inTownGarrison(false),
moveDir(4),
mana(UNINITIALIZED_MANA),
movement(UNINITIALIZED_MOVEMENT),
level(1),
exp(UNINITIALIZED_EXPERIENCE),
gender(EHeroGender::DEFAULT),
lowestCreatureSpeed(0)
{
setNodeType(HERO);
ID = Obj::HERO;
secSkills.emplace_back(SecondarySkill::NONE, -1);
}
PlayerColor CGHeroInstance::getOwner() const
{
return tempOwner;
}
const CHeroClass * CGHeroInstance::getHeroClass() const
{
return getHeroType()->heroClass;
}
HeroClassID CGHeroInstance::getHeroClassID() const
{
return getHeroType()->heroClass->getId();
}
const CHero * CGHeroInstance::getHeroType() const
{
return getHeroTypeID().toHeroType();
}
HeroTypeID CGHeroInstance::getHeroTypeID() const
{
if (ID == Obj::RANDOM_HERO)
return HeroTypeID::NONE;
return HeroTypeID(getObjTypeIndex().getNum());
}
void CGHeroInstance::setHeroType(HeroTypeID heroType)
{
subID = heroType;
}
void CGHeroInstance::initHero(vstd::RNG & rand, const HeroTypeID & SUBID)
{
subID = SUBID.getNum();
initHero(rand);
}
void CGHeroInstance::initHero(vstd::RNG & rand)
{
assert(validTypes(true));
if (ID == Obj::HERO)
appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, getHeroClass()->getIndex())->getTemplates().front();
if(!vstd::contains(spells, SpellID::PRESET))
{
// hero starts with default spells
for(const auto & spellID : getHeroType()->spells)
spells.insert(spellID);
}
else //remove placeholder
spells -= SpellID::PRESET;
if(!vstd::contains(spells, SpellID::SPELLBOOK_PRESET))
{
// hero starts with default spellbook presence status
if(!getArt(ArtifactPosition::SPELLBOOK) && getHeroType()->haveSpellBook)
{
auto artifact = ArtifactUtils::createArtifact(ArtifactID::SPELLBOOK);
putArtifact(ArtifactPosition::SPELLBOOK, artifact);
}
}
else
spells -= SpellID::SPELLBOOK_PRESET;
if(!getArt(ArtifactPosition::MACH4))
{
auto artifact = ArtifactUtils::createArtifact(ArtifactID::CATAPULT);
putArtifact(ArtifactPosition::MACH4, artifact); //everyone has a catapult
}
if(!hasBonus(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)))
{
for(int g=0; g<GameConstants::PRIMARY_SKILLS; ++g)
{
pushPrimSkill(static_cast<PrimarySkill>(g), getHeroClass()->primarySkillInitial[g]);
}
}
if(secSkills.size() == 1 && secSkills[0] == std::pair<SecondarySkill,ui8>(SecondarySkill::NONE, -1)) //set secondary skills to default
secSkills = getHeroType()->secSkillsInit;
if (gender == EHeroGender::DEFAULT)
gender = getHeroType()->gender;
setFormation(EArmyFormation::LOOSE);
if (!stacksCount()) //standard army//initial army
{
initArmy(rand);
}
assert(validTypes());
if (patrol.patrolling)
patrol.initialPos = visitablePos();
if(exp == UNINITIALIZED_EXPERIENCE)
{
initExp(rand);
}
else
{
levelUpAutomatically(rand);
}
// load base hero bonuses, TODO: per-map loading of base hero bonuses
// must be done separately from global bonuses since recruitable heroes in taverns
// are not attached to global bonus node but need access to some global bonuses
// e.g. MANA_PER_KNOWLEDGE_PERCENTAGE for correct preview and initial state after recruit for(const auto & ob : VLC->modh->heroBaseBonuses)
// or MOVEMENT to compute initial movement before recruiting is finished
const JsonNode & baseBonuses = cb->getSettings().getValue(EGameSettings::BONUSES_PER_HERO);
for(const auto & b : baseBonuses.Struct())
{
auto bonus = JsonUtils::parseBonus(b.second);
bonus->source = BonusSource::HERO_BASE_SKILL;
bonus->sid = BonusSourceID(id);
bonus->duration = BonusDuration::PERMANENT;
addNewBonus(bonus);
}
if (cb->getSettings().getBoolean(EGameSettings::MODULE_COMMANDERS) && !commander && getHeroClass()->commander.hasValue())
{
commander = new CCommanderInstance(getHeroClass()->commander);
commander->setArmyObj (castToArmyObj()); //TODO: separate function for setting commanders
commander->giveStackExp (exp); //after our exp is set
}
skillsInfo = SecondarySkillsInfo();
//copy active (probably growing) bonuses from hero prototype to hero object
for(const std::shared_ptr<Bonus> & b : getHeroType()->specialty)
addNewBonus(b);
//initialize bonuses
recreateSecondarySkillsBonuses();
movement = movementPointsLimit(true);
mana = manaLimit(); //after all bonuses are taken into account, make sure this line is the last one
}
void CGHeroInstance::initArmy(vstd::RNG & rand, IArmyDescriptor * dst)
{
if(!dst)
dst = this;
int warMachinesGiven = 0;
auto stacksCountChances = cb->getSettings().getVector(EGameSettings::HEROES_STARTING_STACKS_CHANCES);
int stacksCountInitRandomNumber = rand.nextInt(1, 100);
size_t maxStacksCount = std::min(stacksCountChances.size(), getHeroType()->initialArmy.size());
for(int stackNo=0; stackNo < maxStacksCount; stackNo++)
{
if (stacksCountInitRandomNumber > stacksCountChances[stackNo])
continue;
auto & stack = getHeroType()->initialArmy[stackNo];
int count = rand.nextInt(stack.minAmount, stack.maxAmount);
if(stack.creature == CreatureID::NONE)
{
logGlobal->error("Hero %s has invalid creature in initial army", getNameTranslated());
continue;
}
const CCreature * creature = stack.creature.toCreature();
if(creature->warMachine != ArtifactID::NONE) //war machine
{
warMachinesGiven++;
if(dst != this)
continue;
ArtifactID aid = creature->warMachine;
const CArtifact * art = aid.toArtifact();
if(art != nullptr && !art->getPossibleSlots().at(ArtBearer::HERO).empty())
{
//TODO: should we try another possible slots?
ArtifactPosition slot = art->getPossibleSlots().at(ArtBearer::HERO).front();
if(!getArt(slot))
{
auto artifact = ArtifactUtils::createArtifact(aid);
putArtifact(slot, artifact);
}
else
logGlobal->warn("Hero %s already has artifact at %d, omitting giving artifact %d", getNameTranslated(), slot.toEnum(), aid.toEnum());
}
else
{
logGlobal->error("Hero %s has invalid war machine in initial army", getNameTranslated());
}
}
else
{
dst->setCreature(SlotID(stackNo-warMachinesGiven), stack.creature, count);
}
}
}
CGHeroInstance::~CGHeroInstance()
{
commander.dellNull();
}
bool CGHeroInstance::needsLastStack() const
{
return true;
}
void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const
{
if(h == this) return; //exclude potential self-visiting
if (ID == Obj::HERO)
{
if( cb->gameState()->getPlayerRelations(tempOwner, h->tempOwner) != PlayerRelations::ENEMIES)
{
//exchange
cb->heroExchange(h->id, id);
}
else //battle
{
if(visitedTown) //we're in town
visitedTown->onHeroVisit(h); //town will handle attacking
else
cb->startBattle(h, this);
}
}
else if(ID == Obj::PRISON)
{
if (cb->getHeroCount(h->tempOwner, false) < cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))//free hero slot
{
//update hero parameters
SetMovePoints smp;
smp.hid = id;
cb->setManaPoints (id, manaLimit());
ObjectInstanceID boatId;
const auto boatPos = visitablePos();
if (cb->gameState()->map->getTile(boatPos).isWater())
{
smp.val = movementPointsLimit(false);
if (!boat)
{
//Create a new boat for hero
cb->createBoat(boatPos, getBoatType(), h->getOwner());
boatId = cb->getTopObj(boatPos)->id;
}
}
else
{
smp.val = movementPointsLimit(true);
}
cb->giveHero(id, h->tempOwner, boatId); //recreates def and adds hero to player
cb->setObjPropertyID(id, ObjProperty::ID, Obj(Obj::HERO)); //set ID to 34 AFTER hero gets correct flag color
cb->setMovePoints (&smp);
h->showInfoDialog(102);
}
else //already 8 wandering heroes
{
h->showInfoDialog(103);
}
}
}
std::string CGHeroInstance::getObjectName() const
{
if(ID != Obj::PRISON)
{
std::string hoverName = VLC->generaltexth->allTexts[15];
boost::algorithm::replace_first(hoverName,"%s",getNameTranslated());
boost::algorithm::replace_first(hoverName,"%s", getClassNameTranslated());
return hoverName;
}
else
return VLC->objtypeh->getObjectName(ID, 0);
}
std::string CGHeroInstance::getHoverText(PlayerColor player) const
{
std::string hoverText = CArmedInstance::getHoverText(player) + getMovementPointsTextIfOwner(player);
return hoverText;
}
std::string CGHeroInstance::getMovementPointsTextIfOwner(PlayerColor player) const
{
std::string output = "";
if(player == getOwner())
{
output += " " + VLC->generaltexth->translate("vcmi.adventureMap.movementPointsHeroInfo");
boost::replace_first(output, "%POINTS", std::to_string(movementPointsLimit(!boat)));
boost::replace_first(output, "%REMAINING", std::to_string(movementPointsRemaining()));
}
return output;
}
ui8 CGHeroInstance::maxlevelsToMagicSchool() const
{
return getHeroClass()->isMagicHero() ? 3 : 4;
}
ui8 CGHeroInstance::maxlevelsToWisdom() const
{
return getHeroClass()->isMagicHero() ? 3 : 6;
}
CGHeroInstance::SecondarySkillsInfo::SecondarySkillsInfo():
magicSchoolCounter(1),
wisdomCounter(1)
{}
void CGHeroInstance::SecondarySkillsInfo::resetMagicSchoolCounter()
{
magicSchoolCounter = 0;
}
void CGHeroInstance::SecondarySkillsInfo::resetWisdomCounter()
{
wisdomCounter = 0;
}
void CGHeroInstance::pickRandomObject(vstd::RNG & rand)
{
assert(ID == Obj::HERO || ID == Obj::PRISON || ID == Obj::RANDOM_HERO);
if (ID == Obj::RANDOM_HERO)
{
ID = Obj::HERO;
subID = cb->gameState()->pickNextHeroType(getOwner());
randomizeArmy(getHeroClass()->faction);
}
auto oldSubID = subID;
// to find object handler we must use heroClass->id
// after setType subID used to store unique hero identify id. Check issue 2277 for details
// exclude prisons which should use appearance as set in map, via map editor or RMG
if (ID != Obj::PRISON)
setType(ID, getHeroClass()->getIndex());
this->subID = oldSubID;
}
void CGHeroInstance::recreateSecondarySkillsBonuses()
{
auto secondarySkillsBonuses = getBonuses(Selector::sourceType()(BonusSource::SECONDARY_SKILL));
for(const auto & bonus : *secondarySkillsBonuses)
removeBonus(bonus);
for(const auto & skill_info : secSkills)
if(skill_info.second > 0)
updateSkillBonus(SecondarySkill(skill_info.first), skill_info.second);
}
void CGHeroInstance::updateSkillBonus(const SecondarySkill & which, int val)
{
removeBonuses(Selector::source(BonusSource::SECONDARY_SKILL, BonusSourceID(which)));
auto skillBonus = (*VLC->skillh)[which]->at(val).effects;
for(const auto & b : skillBonus)
addNewBonus(std::make_shared<Bonus>(*b));
}
void CGHeroInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier)
{
if(what == ObjProperty::PRIMARY_STACK_COUNT)
setStackCount(SlotID(0), identifier.getNum());
}
double CGHeroInstance::getFightingStrength() const
{
return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::ATTACK)) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::DEFENSE)));
}
double CGHeroInstance::getMagicStrength() const
{
return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER)));
}
double CGHeroInstance::getHeroStrength() const
{
return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrength(), 2.0));
}
ui64 CGHeroInstance::getTotalStrength() const
{
double ret = getFightingStrength() * getArmyStrength();
return static_cast<ui64>(ret);
}
TExpType CGHeroInstance::calculateXp(TExpType exp) const
{
return static_cast<TExpType>(exp * (valOfBonuses(BonusType::HERO_EXPERIENCE_GAIN_PERCENT)) / 100.0);
}
int32_t CGHeroInstance::getCasterUnitId() const
{
return id.getNum();
}
int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool) const
{
int32_t skill = -1; //skill level
spell->forEachSchool([&, this](const SpellSchool & cnf, bool & stop)
{
int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(cnf)); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies)
if(thisSchool > skill)
{
skill = thisSchool;
if(outSelectedSchool)
*outSelectedSchool = cnf;
}
});
vstd::amax(skill, valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(SpellSchool::ANY))); //any school bonus
vstd::amax(skill, valOfBonuses(BonusType::SPELL, BonusSubtypeID(spell->getId()))); //given by artifact or other effect
vstd::amax(skill, 0); //in case we don't know any school
vstd::amin(skill, 3);
return skill;
}
int64_t CGHeroInstance::getSpellBonus(const spells::Spell * spell, int64_t base, const battle::Unit * affectedStack) const
{
//applying sorcery secondary skill
if(spell->isMagical())
base = static_cast<int64_t>(base * (valOfBonuses(BonusType::SPELL_DAMAGE, BonusSubtypeID(SpellSchool::ANY))) / 100.0);
base = static_cast<int64_t>(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, BonusSubtypeID(spell->getId()))) / 100.0);
int maxSchoolBonus = 0;
spell->forEachSchool([&maxSchoolBonus, this](const SpellSchool & cnf, bool & stop)
{
vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, BonusSubtypeID(cnf)));
});
base = static_cast<int64_t>(base * (100 + maxSchoolBonus) / 100.0);
if(affectedStack && affectedStack->creatureLevel() > 0) //Hero specials like Solmyr, Deemer
base = static_cast<int64_t>(base * static_cast<double>(100 + valOfBonuses(BonusType::SPECIAL_SPELL_LEV, BonusSubtypeID(spell->getId())) / affectedStack->creatureLevel()) / 100.0);
return base;
}
int64_t CGHeroInstance::getSpecificSpellBonus(const spells::Spell * spell, int64_t base) const
{
base = static_cast<int64_t>(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, BonusSubtypeID(spell->getId()))) / 100.0);
return base;
}
int32_t CGHeroInstance::getEffectLevel(const spells::Spell * spell) const
{
return getSpellSchoolLevel(spell);
}
int32_t CGHeroInstance::getEffectPower(const spells::Spell * spell) const
{
return getPrimSkillLevel(PrimarySkill::SPELL_POWER);
}
int32_t CGHeroInstance::getEnchantPower(const spells::Spell * spell) const
{
int32_t spellpower = getPrimSkillLevel(PrimarySkill::SPELL_POWER);
int32_t durationCommon = valOfBonuses(BonusType::SPELL_DURATION, BonusSubtypeID());
int32_t durationSpecific = valOfBonuses(BonusType::SPELL_DURATION, BonusSubtypeID(spell->getId()));
return spellpower + durationCommon + durationSpecific;
}
int64_t CGHeroInstance::getEffectValue(const spells::Spell * spell) const
{
return 0;
}
PlayerColor CGHeroInstance::getCasterOwner() const
{
return tempOwner;
}
void CGHeroInstance::getCasterName(MetaString & text) const
{
//FIXME: use local name, MetaString need access to gamestate as hero name is part of map object
text.replaceRawString(getNameTranslated());
}
void CGHeroInstance::getCastDescription(const spells::Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const
{
const bool singleTarget = attacked.size() == 1;
const int textIndex = singleTarget ? 195 : 196;
text.appendLocalString(EMetaText::GENERAL_TXT, textIndex);
getCasterName(text);
text.replaceName(spell->getId());
if(singleTarget)
attacked.at(0)->addNameReplacement(text, true);
}
const CGHeroInstance * CGHeroInstance::getHeroCaster() const
{
return this;
}
void CGHeroInstance::spendMana(ServerCallback * server, const int spellCost) const
{
if(spellCost != 0)
{
SetMana sm;
sm.absolute = false;
sm.hid = id;
sm.val = -spellCost;
server->apply(sm);
}
}
bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const
{
const bool isAllowed = cb->isAllowed(spell->getId());
const bool inSpellBook = vstd::contains(spells, spell->getId()) && hasSpellbook();
const bool specificBonus = hasBonusOfType(BonusType::SPELL, BonusSubtypeID(spell->getId()));
bool schoolBonus = false;
spell->forEachSchool([this, &schoolBonus](const SpellSchool & cnf, bool & stop)
{
if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, BonusSubtypeID(cnf)))
{
schoolBonus = stop = true;
}
});
const bool levelBonus = hasBonusOfType(BonusType::SPELLS_OF_LEVEL, BonusCustomSubtype::spellLevel(spell->getLevel()));
if(spell->isSpecial())
{
if(inSpellBook)
{//hero has this spell in spellbook
logGlobal->error("Special spell %s in spellbook.", spell->getNameTranslated());
}
return specificBonus;
}
else if(!isAllowed)
{
if(inSpellBook)
{
//hero has this spell in spellbook
//it is normal if set in map editor, but trace it to possible debug of magic guild
logGlobal->trace("Banned spell %s in spellbook.", spell->getNameTranslated());
}
return inSpellBook || specificBonus || schoolBonus || levelBonus;
}
else
{
return inSpellBook || schoolBonus || specificBonus || levelBonus;
}
}
bool CGHeroInstance::canLearnSpell(const spells::Spell * spell, bool allowBanned) const
{
if(!hasSpellbook())
return false;
if(spell->getLevel() > maxSpellLevel()) //not enough wisdom
return false;
if(vstd::contains(spells, spell->getId()))//already known
return false;
if(spell->isSpecial())
{
logGlobal->warn("Hero %s try to learn special spell %s", nodeName(), spell->getNameTranslated());
return false;//special spells can not be learned
}
if(spell->isCreatureAbility())
{
logGlobal->warn("Hero %s try to learn creature spell %s", nodeName(), spell->getNameTranslated());
return false;//creature abilities can not be learned
}
if(!allowBanned && !cb->isAllowed(spell->getId()))
{
logGlobal->warn("Hero %s try to learn banned spell %s", nodeName(), spell->getNameTranslated());
return false;//banned spells should not be learned
}
return true;
}
/**
* Calculates what creatures and how many to be raised from a battle.
* @param battleResult The results of the battle.
* @return Returns a pair with the first value indicating the ID of the creature
* type and second value the amount. Both values are returned as -1 if necromancy
* could not be applied.
*/
CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &battleResult) const
{
bool hasImprovedNecromancy = hasBonusOfType(BonusType::IMPROVED_NECROMANCY);
// need skill or cloak of undead king - lesser artifacts don't work without skill
if (hasImprovedNecromancy)
{
double necromancySkill = valOfBonuses(BonusType::UNDEAD_RAISE_PERCENTAGE) / 100.0;
const ui8 necromancyLevel = valOfBonuses(BonusType::IMPROVED_NECROMANCY);
vstd::amin(necromancySkill, 1.0); //it's impossible to raise more creatures than all...
const std::map<CreatureID,si32> &casualties = battleResult.casualties[CBattleInfoEssentials::otherSide(battleResult.winner)];
// figure out what to raise - pick strongest creature meeting requirements
CreatureID creatureTypeRaised = CreatureID::NONE; //now we always have IMPROVED_NECROMANCY, no need for hardcode
int requiredCasualtyLevel = 1;
TConstBonusListPtr improvedNecromancy = getBonuses(Selector::type()(BonusType::IMPROVED_NECROMANCY));
if(!improvedNecromancy->empty())
{
int maxCasualtyLevel = 1;
for(const auto & casualty : casualties)
vstd::amax(maxCasualtyLevel, VLC->creatures()->getById(casualty.first)->getLevel());
// pick best bonus available
std::shared_ptr<Bonus> topPick;
for(const std::shared_ptr<Bonus> & newPick : *improvedNecromancy)
{
// addInfo[0] = required necromancy skill, addInfo[1] = required casualty level
if(newPick->additionalInfo[0] > necromancyLevel || newPick->additionalInfo[1] > maxCasualtyLevel)
continue;
if(!topPick)
{
topPick = newPick;
}
else
{
auto quality = [](const std::shared_ptr<Bonus> & pick) -> std::tuple<int, int, int>
{
const auto * c = pick->subtype.as<CreatureID>().toCreature();
return std::tuple<int, int, int> {c->getLevel(), static_cast<int>(c->getFullRecruitCost().marketValue()), -pick->additionalInfo[1]};
};
if(quality(topPick) < quality(newPick))
topPick = newPick;
}
}
if(topPick)
{
creatureTypeRaised = topPick->subtype.as<CreatureID>();
requiredCasualtyLevel = std::max(topPick->additionalInfo[1], 1);
}
}
assert(creatureTypeRaised != CreatureID::NONE);
// raise upgraded creature (at 2/3 rate) if no space available otherwise
if(getSlotFor(creatureTypeRaised) == SlotID())
{
for(const CreatureID & upgraded : creatureTypeRaised.toCreature()->upgrades)
{
if(getSlotFor(upgraded) != SlotID())
{
creatureTypeRaised = upgraded;
necromancySkill *= 2/3.0;
break;
}
}
}
// calculate number of creatures raised - low level units contribute at 50% rate
const double raisedUnitHealth = creatureTypeRaised.toCreature()->getMaxHealth();
double raisedUnits = 0;
for(const auto & casualty : casualties)
{
const CCreature * c = casualty.first.toCreature();
double raisedFromCasualty = std::min(c->getMaxHealth() / raisedUnitHealth, 1.0) * casualty.second * necromancySkill;
if(c->getLevel() < requiredCasualtyLevel)
raisedFromCasualty *= 0.5;
raisedUnits += raisedFromCasualty;
}
return CStackBasicDescriptor(creatureTypeRaised, std::max(static_cast<int>(raisedUnits), 1));
}
return CStackBasicDescriptor();
}
/**
* Show the necromancy dialog with information about units raised.
* @param raisedStack Pair where the first element represents ID of the raised creature
* and the second element the amount.
*/
void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const
{
InfoWindow iw;
iw.type = EInfoWindowMode::AUTO;
iw.soundID = soundBase::pickup01 + rand.nextInt(6);
iw.player = tempOwner;
iw.components.emplace_back(ComponentType::CREATURE, raisedStack.getId(), raisedStack.count);
if (raisedStack.count > 1) // Practicing the dark arts of necromancy, ... (plural)
{
iw.text.appendLocalString(EMetaText::GENERAL_TXT, 145);
iw.text.replaceNumber(raisedStack.count);
}
else // Practicing the dark arts of necromancy, ... (singular)
{
iw.text.appendLocalString(EMetaText::GENERAL_TXT, 146);
}
iw.text.replaceName(raisedStack);
cb->showInfoDialog(&iw);
}
/*
int3 CGHeroInstance::getSightCenter() const
{
return getPosition(false);
}*/
int CGHeroInstance::getSightRadius() const
{
return valOfBonuses(BonusType::SIGHT_RADIUS); // scouting gives SIGHT_RADIUS bonus
}
si32 CGHeroInstance::manaRegain() const
{
if (hasBonusOfType(BonusType::FULL_MANA_REGENERATION))
return manaLimit();
return valOfBonuses(BonusType::MANA_REGENERATION);
}
si32 CGHeroInstance::getManaNewTurn() const
{
if(visitedTown && visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1))
{
//if hero starts turn in town with mage guild - restore all mana
return std::max(mana, manaLimit());
}
si32 res = mana + manaRegain();
res = std::min(res, manaLimit());
res = std::max(res, mana);
res = std::max(res, 0);
return res;
}
// /**
// * Places an artifact in hero's backpack. If it's a big artifact equips it
// * or discards it if it cannot be equipped.
// */
// void CGHeroInstance::giveArtifact (ui32 aid) //use only for fixed artifacts
// {
// CArtifact * const artifact = VLC->arth->objects[aid]; //pointer to constant object
// CArtifactInstance *ai = CArtifactInstance::createNewArtifactInstance(artifact);
// ai->putAt(this, ai->firstAvailableSlot(this));
// }
BoatId CGHeroInstance::getBoatType() const
{
return BoatId(VLC->townh->getById(getHeroClass()->faction)->getBoatType());
}
void CGHeroInstance::getOutOffsets(std::vector<int3> &offsets) const
{
offsets = {
{0, -1, 0},
{+1, -1, 0},
{+1, 0, 0},
{+1, +1, 0},
{0, +1, 0},
{-1, +1, 0},
{-1, 0, 0},
{-1, -1, 0},
};
}
const IObjectInterface * CGHeroInstance::getObject() const
{
return this;
}
int32_t CGHeroInstance::getSpellCost(const spells::Spell * sp) const
{
return sp->getCost(getSpellSchoolLevel(sp));
}
void CGHeroInstance::pushPrimSkill( PrimarySkill which, int val )
{
auto sel = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which))
.And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL));
if(hasBonus(sel))
removeBonuses(sel);
addNewBonus(std::make_shared<Bonus>(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, BonusSourceID(id), BonusSubtypeID(which)));
}
EAlignment CGHeroInstance::getAlignment() const
{
return getHeroClass()->getAlignment();
}
void CGHeroInstance::initExp(vstd::RNG & rand)
{
exp = rand.nextInt(40, 89);
}
std::string CGHeroInstance::nodeName() const
{
return "Hero " + getNameTextID();
}
si32 CGHeroInstance::manaLimit() const
{
return si32(getPrimSkillLevel(PrimarySkill::KNOWLEDGE)
* (valOfBonuses(BonusType::MANA_PER_KNOWLEDGE_PERCENTAGE))) / 100;
}
HeroTypeID CGHeroInstance::getPortraitSource() const
{
if (customPortraitSource.isValid())
return customPortraitSource;
else
return getHeroTypeID();
}
int32_t CGHeroInstance::getIconIndex() const
{
return getPortraitSource().toEntity(VLC)->getIconIndex();
}
std::string CGHeroInstance::getNameTranslated() const
{
return VLC->generaltexth->translate(getNameTextID());
}
std::string CGHeroInstance::getClassNameTranslated() const
{
return VLC->generaltexth->translate(getClassNameTextID());
}
std::string CGHeroInstance::getClassNameTextID() const
{
if (isCampaignGem())
return "core.genrltxt.735";
return getHeroClass()->getNameTextID();
}
std::string CGHeroInstance::getNameTextID() const
{
if (!nameCustomTextId.empty())
return nameCustomTextId;
if (getHeroTypeID().hasValue())
return getHeroType()->getNameTextID();
// FIXME: called by logging from some specialties (mods?) before type is set on deserialization
// assert(0);
return "";
}
std::string CGHeroInstance::getBiographyTranslated() const
{
return VLC->generaltexth->translate(getBiographyTextID());
}
std::string CGHeroInstance::getBiographyTextID() const
{
if (!biographyCustomTextId.empty())
return biographyCustomTextId;
if (getHeroTypeID().hasValue())
return getHeroType()->getBiographyTextID();
return ""; //for random hero
}
CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(const ArtifactPosition & pos, CArtifactInstance * art)
{
assert(art->canBePutAt(this, pos));
if(ArtifactUtils::isSlotEquipment(pos))
attachTo(*art);
return CArtifactSet::putArtifact(pos, art);
}
void CGHeroInstance::removeArtifact(const ArtifactPosition & pos)
{
auto art = getArt(pos);
assert(art);
CArtifactSet::removeArtifact(pos);
if(ArtifactUtils::isSlotEquipment(pos))
detachFrom(*art);
}
bool CGHeroInstance::hasSpellbook() const
{
return getArt(ArtifactPosition::SPELLBOOK);
}
void CGHeroInstance::addSpellToSpellbook(const SpellID & spell)
{
spells.insert(spell);
}
void CGHeroInstance::removeSpellFromSpellbook(const SpellID & spell)
{
spells.erase(spell);
}
bool CGHeroInstance::spellbookContainsSpell(const SpellID & spell) const
{
return vstd::contains(spells, spell);
}
void CGHeroInstance::removeSpellbook()
{
spells.clear();
if(hasSpellbook())
{
cb->removeArtifact(ArtifactLocation(this->id, ArtifactPosition::SPELLBOOK));
}
}
const std::set<SpellID> & CGHeroInstance::getSpellsInSpellbook() const
{
return spells;
}
int CGHeroInstance::maxSpellLevel() const
{
return std::min(GameConstants::SPELL_LEVELS, valOfBonuses(Selector::type()(BonusType::MAX_LEARNABLE_SPELL_LEVEL)));
}
void CGHeroInstance::attachToBoat(CGBoat* newBoat)
{
assert(newBoat);
boat = newBoat;
attachTo(const_cast<CGBoat&>(*boat));
const_cast<CGBoat*>(boat)->hero = this;
}
void CGHeroInstance::deserializationFix()
{
artDeserializationFix(this);
boatDeserializationFix();
}
void CGHeroInstance::boatDeserializationFix()
{
if (boat)
attachTo(const_cast<CGBoat&>(*boat));
}
CBonusSystemNode * CGHeroInstance::whereShouldBeAttachedOnSiege(const bool isBattleOutsideTown) const
{
if(!visitedTown)
return nullptr;
return isBattleOutsideTown ? (CBonusSystemNode *)(& visitedTown->townAndVis)
: (CBonusSystemNode *)(visitedTown.get());
}
CBonusSystemNode * CGHeroInstance::whereShouldBeAttachedOnSiege(CGameState * gs)
{
if(visitedTown)
return whereShouldBeAttachedOnSiege(visitedTown->isBattleOutsideTown(this));
return &CArmedInstance::whereShouldBeAttached(gs);
}
CBonusSystemNode & CGHeroInstance::whereShouldBeAttached(CGameState * gs)
{
if(visitedTown)
{
if(inTownGarrison)
return *visitedTown;
else
return visitedTown->townAndVis;
}
else
return CArmedInstance::whereShouldBeAttached(gs);
}
int CGHeroInstance::movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark, const TurnInfo * ti) const
{
std::unique_ptr<TurnInfo> turnInfoLocal;
if(!ti)
{
turnInfoLocal = std::make_unique<TurnInfo>(this);
ti = turnInfoLocal.get();
}
if(!ti->hasBonusOfType(BonusType::FREE_SHIP_BOARDING))
return 0; // take all MPs by default
auto boatLayer = boat ? boat->layer : EPathfindingLayer::SAIL;
int mp1 = ti->getMaxMovePoints(disembark ? EPathfindingLayer::LAND : boatLayer);
int mp2 = ti->getMaxMovePoints(disembark ? boatLayer : EPathfindingLayer::LAND);
int ret = static_cast<int>((MPsBefore - basicCost) * static_cast<double>(mp1) / mp2);
return ret;
}
EDiggingStatus CGHeroInstance::diggingStatus() const
{
if(static_cast<int>(movement) < movementPointsLimit(true))
return EDiggingStatus::LACK_OF_MOVEMENT;
if(!ArtifactID(ArtifactID::GRAIL).toArtifact()->canBePutAt(this))
return EDiggingStatus::BACKPACK_IS_FULL;
return cb->getTileDigStatus(visitablePos());
}
ArtBearer::ArtBearer CGHeroInstance::bearerType() const
{
return ArtBearer::HERO;
}
std::vector<SecondarySkill> CGHeroInstance::getLevelUpProposedSecondarySkills(vstd::RNG & rand) const
{
auto getObligatorySkills = [](CSkill::Obligatory obl){
std::set<SecondarySkill> obligatory;
for(auto i = 0; i < VLC->skillh->size(); i++)
if((*VLC->skillh)[SecondarySkill(i)]->obligatory(obl))
obligatory.insert(i); //Always return all obligatory skills
return obligatory;
};
auto intersect = [](const std::set<SecondarySkill> & left, const std::set<SecondarySkill> & right)
{
std::set<SecondarySkill> intersect;
std::set_intersection(left.begin(), left.end(), right.begin(), right.end(),
std::inserter(intersect, intersect.begin()));
return intersect;
};
std::set<SecondarySkill> wisdomList = getObligatorySkills(CSkill::Obligatory::MAJOR);
std::set<SecondarySkill> schoolList = getObligatorySkills(CSkill::Obligatory::MINOR);
std::set<SecondarySkill> basicAndAdv;
std::set<SecondarySkill> none;
for(int i = 0; i < VLC->skillh->size(); i++)
if (canLearnSkill(SecondarySkill(i)))
none.insert(SecondarySkill(i));
for(const auto & elem : secSkills)
{
if(elem.second < MasteryLevel::EXPERT)
basicAndAdv.insert(elem.first);
none.erase(elem.first);
}
bool wantsWisdom = skillsInfo.wisdomCounter + 1 >= maxlevelsToWisdom();
bool wantsSchool = skillsInfo.magicSchoolCounter + 1 >= maxlevelsToMagicSchool();
std::vector<SecondarySkill> skills;
auto chooseSkill = [&](std::set<SecondarySkill> & options)
{
bool selectWisdom = wantsWisdom && !intersect(options, wisdomList).empty();
bool selectSchool = wantsSchool && !intersect(options, schoolList).empty();
SecondarySkill selection;
if (selectWisdom)
selection = getHeroClass()->chooseSecSkill(intersect(options, wisdomList), rand);
else if (selectSchool)
selection = getHeroClass()->chooseSecSkill(intersect(options, schoolList), rand);
else
selection = getHeroClass()->chooseSecSkill(options, rand);
skills.push_back(selection);
options.erase(selection);
if (wisdomList.count(selection))
wisdomList.clear();
if (schoolList.count(selection))
schoolList.clear();
};
if (!basicAndAdv.empty())
chooseSkill(basicAndAdv);
if (canLearnSkill() && !none.empty())
chooseSkill(none);
if (!basicAndAdv.empty() && skills.size() < 2)
chooseSkill(basicAndAdv);
if (canLearnSkill() && !none.empty() && skills.size() < 2)
chooseSkill(none);
return skills;
}
PrimarySkill CGHeroInstance::nextPrimarySkill(vstd::RNG & rand) const
{
assert(gainsLevel());
const auto isLowLevelHero = level < GameConstants::HERO_HIGH_LEVEL;
const auto & skillChances = isLowLevelHero ? getHeroClass()->primarySkillLowLevel : getHeroClass()->primarySkillHighLevel;
if (isCampaignYog())
{
// Yog can only receive Attack or Defence on level-up
std::vector<int> yogChances = { skillChances[0], skillChances[1]};
return static_cast<PrimarySkill>(RandomGeneratorUtil::nextItemWeighted(yogChances, rand));
}
return static_cast<PrimarySkill>(RandomGeneratorUtil::nextItemWeighted(skillChances, rand));
}
std::optional<SecondarySkill> CGHeroInstance::nextSecondarySkill(vstd::RNG & rand) const
{
assert(gainsLevel());
std::optional<SecondarySkill> chosenSecondarySkill;
const auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(rand);
if(!proposedSecondarySkills.empty())
{
std::vector<SecondarySkill> learnedSecondarySkills;
for(const auto & secondarySkill : proposedSecondarySkills)
{
if(getSecSkillLevel(secondarySkill) > 0)
{
learnedSecondarySkills.push_back(secondarySkill);
}
}
if(learnedSecondarySkills.empty())
{
// there are only new skills to learn, so choose anyone of them
chosenSecondarySkill = std::make_optional(*RandomGeneratorUtil::nextItem(proposedSecondarySkills, rand));
}
else
{
// preferably upgrade a already learned secondary skill
chosenSecondarySkill = std::make_optional(*RandomGeneratorUtil::nextItem(learnedSecondarySkills, rand));
}
}
return chosenSecondarySkill;
}
void CGHeroInstance::setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8 abs)
{
if(primarySkill < PrimarySkill::EXPERIENCE)
{
auto skill = getLocalBonus(Selector::type()(BonusType::PRIMARY_SKILL)
.And(Selector::subtype()(BonusSubtypeID(primarySkill)))
.And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)));
assert(skill);
if(abs)
{
skill->val = static_cast<si32>(value);
}
else
{
skill->val += static_cast<si32>(value);
}
CBonusSystemNode::treeHasChanged();
}
else if(primarySkill == PrimarySkill::EXPERIENCE)
{
if(abs)
{
exp = value;
}
else
{
exp += value;
}
}
}
bool CGHeroInstance::gainsLevel() const
{
return level < VLC->heroh->maxSupportedLevel() && exp >= static_cast<TExpType>(VLC->heroh->reqExp(level+1));
}
void CGHeroInstance::levelUp(const std::vector<SecondarySkill> & skills)
{
++level;
//deterministic secondary skills
++skillsInfo.magicSchoolCounter;
++skillsInfo.wisdomCounter;
for(const auto & skill : skills)
{
if((*VLC->skillh)[skill]->obligatory(CSkill::Obligatory::MAJOR))
skillsInfo.resetWisdomCounter();
if((*VLC->skillh)[skill]->obligatory(CSkill::Obligatory::MINOR))
skillsInfo.resetMagicSchoolCounter();
}
//update specialty and other bonuses that scale with level
treeHasChanged();
}
void CGHeroInstance::levelUpAutomatically(vstd::RNG & rand)
{
while(gainsLevel())
{
const auto primarySkill = nextPrimarySkill(rand);
setPrimarySkill(primarySkill, 1, false);
auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(rand);
const auto secondarySkill = nextSecondarySkill(rand);
if(secondarySkill)
{
setSecSkillLevel(*secondarySkill, 1, false);
}
//TODO why has the secondary skills to be passed to the method?
levelUp(proposedSecondarySkills);
}
}
bool CGHeroInstance::hasVisions(const CGObjectInstance * target, BonusSubtypeID subtype) const
{
//VISIONS spell support
const int visionsMultiplier = valOfBonuses(BonusType::VISIONS, subtype);
int visionsRange = visionsMultiplier * getPrimSkillLevel(PrimarySkill::SPELL_POWER);
if (visionsMultiplier > 0)
vstd::amax(visionsRange, 3); //minimum range is 3 tiles, but only if VISIONS bonus present
const int distance = static_cast<int>(target->anchorPos().dist2d(visitablePos()));
//logGlobal->debug(boost::str(boost::format("Visions: dist %d, mult %d, range %d") % distance % visionsMultiplier % visionsRange));
return (distance < visionsRange) && (target->anchorPos().z == anchorPos().z);
}
std::string CGHeroInstance::getHeroTypeName() const
{
if(ID == Obj::HERO || ID == Obj::PRISON)
return getHeroType()->getJsonKey();
return "";
}
void CGHeroInstance::afterAddToMap(CMap * map)
{
if(ID != Obj::PRISON)
map->heroesOnMap.emplace_back(this);
}
void CGHeroInstance::afterRemoveFromMap(CMap* map)
{
if (ID == Obj::PRISON)
vstd::erase_if_present(map->heroesOnMap, this);
}
void CGHeroInstance::setHeroTypeName(const std::string & identifier)
{
if(ID == Obj::HERO || ID == Obj::PRISON)
{
auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier);
if(rawId)
subID = rawId.value();
else
{
throw std::runtime_error("Couldn't resolve hero identifier " + identifier);
}
}
}
void CGHeroInstance::updateFrom(const JsonNode & data)
{
CGObjectInstance::updateFrom(data);
}
void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
{
handler.serializeString("biography", biographyCustomTextId);
handler.serializeInt("experience", exp, 0);
if(!handler.saving && exp != UNINITIALIZED_EXPERIENCE) //do not gain levels if experience is not initialized
{
while (gainsLevel())
{
++level;
}
}
handler.serializeString("name", nameCustomTextId);
handler.serializeInt("gender", gender, 0);
handler.serializeId("portrait", customPortraitSource, HeroTypeID::NONE);
//primary skills
if(handler.saving)
{
const bool haveSkills = hasBonus(Selector::type()(BonusType::PRIMARY_SKILL).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)));
if(haveSkills)
{
auto primarySkills = handler.enterStruct("primarySkills");
for(auto i = PrimarySkill::BEGIN; i < PrimarySkill::END; ++i)
{
int value = valOfBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(i)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)));
handler.serializeInt(NPrimarySkill::names[i.getNum()], value, 0);
}
}
}
else
{
auto primarySkills = handler.enterStruct("primarySkills");
if(handler.getCurrent().getType() == JsonNode::JsonType::DATA_STRUCT)
{
for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i)
{
int value = 0;
handler.serializeInt(NPrimarySkill::names[i], value, 0);
pushPrimSkill(static_cast<PrimarySkill>(i), value);
}
}
}
//secondary skills
if(handler.saving)
{
//does hero have default skills?
bool defaultSkills = false;
bool normalSkills = false;
for(const auto & p : secSkills)
{
if(p.first == SecondarySkill(SecondarySkill::NONE))
defaultSkills = true;
else
normalSkills = true;
}
if(defaultSkills && normalSkills)
logGlobal->error("Mixed default and normal secondary skills");
//in json default skills means no field/null
if(!defaultSkills)
{
//enter array here as handler initialize it
auto secondarySkills = handler.enterArray("secondarySkills");
secondarySkills.syncSize(secSkills, JsonNode::JsonType::DATA_VECTOR);
for(size_t skillIndex = 0; skillIndex < secondarySkills.size(); ++skillIndex)
{
JsonArraySerializer inner = secondarySkills.enterArray(skillIndex);
SecondarySkill skillId = secSkills.at(skillIndex).first;
handler.serializeId("skill", skillId);
std::string skillLevel = NSecondarySkill::levels.at(secSkills.at(skillIndex).second);
handler.serializeString("level", skillLevel);
}
}
}
else
{
auto secondarySkills = handler.getCurrent()["secondarySkills"];
secSkills.clear();
if(secondarySkills.getType() == JsonNode::JsonType::DATA_NULL)
{
secSkills.emplace_back(SecondarySkill::NONE, -1);
}
else
{
auto addSkill = [this](const std::string & skillId, const std::string & levelId)
{
const int rawId = SecondarySkill::decode(skillId);
if(rawId < 0)
{
logGlobal->error("Invalid secondary skill %s", skillId);
return;
}
const int level = vstd::find_pos(NSecondarySkill::levels, levelId);
if(level < 0)
{
logGlobal->error("Invalid secondary skill level%s", levelId);
return;
}
secSkills.emplace_back(SecondarySkill(rawId), level);
};
if(secondarySkills.getType() == JsonNode::JsonType::DATA_VECTOR)
{
for(const auto & p : secondarySkills.Vector())
{
auto skillMap = p.Struct();
addSkill(skillMap["skill"].String(), skillMap["level"].String());
}
}
else if(secondarySkills.getType() == JsonNode::JsonType::DATA_STRUCT)
{
for(const auto & p : secondarySkills.Struct())
{
addSkill(p.first, p.second.String());
};
}
}
}
handler.serializeIdArray("spellBook", spells);
if(handler.saving)
CArtifactSet::serializeJsonArtifacts(handler, "artifacts");
}
void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler)
{
serializeCommonOptions(handler);
serializeJsonOwner(handler);
if(ID == Obj::HERO || ID == Obj::PRISON)
{
std::string typeName;
if(handler.saving)
typeName = getHeroTypeName();
handler.serializeString("type", typeName);
if(!handler.saving)
setHeroTypeName(typeName);
}
if(!handler.saving)
{
if(!appearance)
{
// crossoverDeserialize
appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, getHeroClassID())->getTemplates().front();
}
}
CArmedInstance::serializeJsonOptions(handler);
{
static constexpr int NO_PATROLING = -1;
int rawPatrolRadius = NO_PATROLING;
if(handler.saving)
{
rawPatrolRadius = patrol.patrolling ? patrol.patrolRadius : NO_PATROLING;
}
handler.serializeInt("patrolRadius", rawPatrolRadius, NO_PATROLING);
if(!handler.saving)
{
patrol.patrolling = (rawPatrolRadius > NO_PATROLING);
patrol.initialPos = visitablePos();
patrol.patrolRadius = (rawPatrolRadius > NO_PATROLING) ? rawPatrolRadius : 0;
}
}
}
void CGHeroInstance::serializeJsonDefinition(JsonSerializeFormat & handler)
{
serializeCommonOptions(handler);
}
bool CGHeroInstance::isMissionCritical() const
{
for(const TriggeredEvent & event : cb->getMapHeader()->triggeredEvents)
{
if (event.effect.type != EventEffect::DEFEAT)
continue;
auto const & testFunctor = [&](const EventCondition & condition)
{
if ((condition.condition == EventCondition::CONTROL) && condition.objectID != ObjectInstanceID::NONE)
return (id != condition.objectID);
if (condition.condition == EventCondition::HAVE_ARTIFACT)
{
if(hasArt(condition.objectType.as<ArtifactID>()))
return true;
}
if(condition.condition == EventCondition::IS_HUMAN)
return true;
return false;
};
if(event.trigger.test(testFunctor))
return true;
}
return false;
}
void CGHeroInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const
{
TConstBonusListPtr lista = getBonuses(Selector::typeSubtype(BonusType::SPECIAL_UPGRADE, BonusSubtypeID(stack.type->getId())));
for(const auto & it : *lista)
{
auto nid = CreatureID(it->additionalInfo[0]);
if (nid != stack.type->getId()) //in very specific case the upgrade is available by default (?)
{
info.newID.push_back(nid);
info.cost.push_back(nid.toCreature()->getFullRecruitCost() - stack.type->getFullRecruitCost());
}
}
}
bool CGHeroInstance::isCampaignYog() const
{
const StartInfo *si = cb->getStartInfo();
// it would be nice to find a way to move this hack to config/mapOverrides.json
if(!si || !si->campState)
return false;
std::string campaign = si->campState->getFilename();
if (!boost::starts_with(campaign, "DATA/YOG")) // "Birth of a Barbarian"
return false;
if (getHeroTypeID() != HeroTypeID::SOLMYR) // Yog (based on Solmyr)
return false;
return true;
}
bool CGHeroInstance::isCampaignGem() const
{
const StartInfo *si = cb->getStartInfo();
// it would be nice to find a way to move this hack to config/mapOverrides.json
if(!si || !si->campState)
return false;
std::string campaign = si->campState->getFilename();
if (!boost::starts_with(campaign, "DATA/GEM") && !boost::starts_with(campaign, "DATA/FINAL")) // "New Beginning" and "Unholy Alliance"
return false;
if (getHeroTypeID() != HeroTypeID::GEM) // Yog (based on Solmyr)
return false;
return true;
}
ResourceSet CGHeroInstance::dailyIncome() const
{
ResourceSet income;
for (GameResID k : GameResID::ALL_RESOURCES())
income[k] += valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k));
const auto & playerSettings = cb->getPlayerSettings(getOwner());
income.applyHandicap(playerSettings->handicap.percentIncome);
return income;
}
std::vector<CreatureID> CGHeroInstance::providedCreatures() const
{
return {};
}
const IOwnableObject * CGHeroInstance::asOwnable() const
{
return this;
}
VCMI_LIB_NAMESPACE_END