/* * CHeroHandler.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 "CHeroHandler.h" #include "CGeneralTextHandler.h" #include "filesystem/Filesystem.h" #include "VCMI_Lib.h" #include "JsonNode.h" #include "StringConstants.h" #include "battle/BattleHex.h" #include "CCreatureHandler.h" #include "GameSettings.h" #include "CModHandler.h" #include "CTownHandler.h" #include "mapObjects/CObjectHandler.h" //for hero specialty #include "CSkillHandler.h" #include "mapObjects/CObjectClassesHandler.h" #include "BattleFieldHandler.h" VCMI_LIB_NAMESPACE_BEGIN CHero::CHero() = default; CHero::~CHero() = default; int32_t CHero::getIndex() const { return ID.getNum(); } int32_t CHero::getIconIndex() const { return imageIndex; } std::string CHero::getJsonKey() const { return modScope + ':' + identifier;; } HeroTypeID CHero::getId() const { return ID; } std::string CHero::getNameTranslated() const { return VLC->generaltexth->translate(getNameTextID()); } std::string CHero::getBiographyTranslated() const { return VLC->generaltexth->translate(getBiographyTextID()); } std::string CHero::getSpecialtyNameTranslated() const { return VLC->generaltexth->translate(getSpecialtyNameTextID()); } std::string CHero::getSpecialtyDescriptionTranslated() const { return VLC->generaltexth->translate(getSpecialtyDescriptionTextID()); } std::string CHero::getSpecialtyTooltipTranslated() const { return VLC->generaltexth->translate(getSpecialtyTooltipTextID()); } std::string CHero::getNameTextID() const { return TextIdentifier("hero", modScope, identifier, "name").get(); } std::string CHero::getBiographyTextID() const { return TextIdentifier("hero", modScope, identifier, "biography").get(); } std::string CHero::getSpecialtyNameTextID() const { return TextIdentifier("hero", modScope, identifier, "specialty", "name").get(); } std::string CHero::getSpecialtyDescriptionTextID() const { return TextIdentifier("hero", modScope, identifier, "specialty", "description").get(); } std::string CHero::getSpecialtyTooltipTextID() const { return TextIdentifier("hero", modScope, identifier, "specialty", "tooltip").get(); } void CHero::registerIcons(const IconRegistar & cb) const { cb(getIconIndex(), 0, "UN32", iconSpecSmall); cb(getIconIndex(), 0, "UN44", iconSpecLarge); cb(getIconIndex(), 0, "PORTRAITSLARGE", portraitLarge); cb(getIconIndex(), 0, "PORTRAITSSMALL", portraitSmall); } void CHero::updateFrom(const JsonNode & data) { //todo: CHero::updateFrom } void CHero::serializeJson(JsonSerializeFormat & handler) { } SecondarySkill CHeroClass::chooseSecSkill(const std::set & possibles, CRandomGenerator & rand) const //picks secondary skill out from given possibilities { int totalProb = 0; for(const auto & possible : possibles) { totalProb += secSkillProbability[possible]; } if (totalProb != 0) // may trigger if set contains only banned skills (0 probability) { auto ran = rand.nextInt(totalProb - 1); for(const auto & possible : possibles) { ran -= secSkillProbability[possible]; if(ran < 0) { return possible; } } } // FIXME: select randomly? How H3 handles such rare situation? return *possibles.begin(); } bool CHeroClass::isMagicHero() const { return affinity == MAGIC; } EAlignment::EAlignment CHeroClass::getAlignment() const { return ((*VLC->townh)[faction]->alignment); } int32_t CHeroClass::getIndex() const { return id.getNum(); } int32_t CHeroClass::getIconIndex() const { return getIndex(); } std::string CHeroClass::getJsonKey() const { return modScope + ':' + identifier;; } HeroClassID CHeroClass::getId() const { return id; } void CHeroClass::registerIcons(const IconRegistar & cb) const { } std::string CHeroClass::getNameTranslated() const { return VLC->generaltexth->translate(getNameTextID()); } std::string CHeroClass::getNameTextID() const { return TextIdentifier("heroClass", modScope, identifier, "name").get(); } void CHeroClass::updateFrom(const JsonNode & data) { //TODO: CHeroClass::updateFrom } void CHeroClass::serializeJson(JsonSerializeFormat & handler) { } CHeroClass::CHeroClass(): faction(0), affinity(0), defaultTavernChance(0), commander(nullptr) { } void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill::PrimarySkill pSkill) const { const auto & skillName = PrimarySkill::names[pSkill]; auto currentPrimarySkillValue = static_cast(node["primarySkills"][skillName].Integer()); //minimal value is 0 for attack and defense and 1 for spell power and knowledge auto primarySkillLegalMinimum = (pSkill == PrimarySkill::ATTACK || pSkill == PrimarySkill::DEFENSE) ? 0 : 1; if(currentPrimarySkillValue < primarySkillLegalMinimum) { logMod->error("Hero class '%s' has incorrect initial value '%d' for skill '%s'. Value '%d' will be used instead.", heroClass->getNameTranslated(), currentPrimarySkillValue, skillName, primarySkillLegalMinimum); currentPrimarySkillValue = primarySkillLegalMinimum; } heroClass->primarySkillInitial.push_back(currentPrimarySkillValue); heroClass->primarySkillLowLevel.push_back(static_cast(node["lowLevelChance"][skillName].Float())); heroClass->primarySkillHighLevel.push_back(static_cast(node["highLevelChance"][skillName].Float())); } const std::vector & CHeroClassHandler::getTypeNames() const { static const std::vector typeNames = { "heroClass" }; return typeNames; } CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) { assert(identifier.find(':') == std::string::npos); assert(!scope.empty()); std::string affinityStr[2] = { "might", "magic" }; auto * heroClass = new CHeroClass(); heroClass->id = HeroClassID(index); heroClass->identifier = identifier; heroClass->modScope = scope; heroClass->imageBattleFemale = node["animation"]["battle"]["female"].String(); heroClass->imageBattleMale = node["animation"]["battle"]["male"].String(); //MODS COMPATIBILITY FOR 0.96 heroClass->imageMapFemale = node["animation"]["map"]["female"].String(); heroClass->imageMapMale = node["animation"]["map"]["male"].String(); VLC->generaltexth->registerString(scope, heroClass->getNameTextID(), node["name"].String()); heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); fillPrimarySkillData(node, heroClass, PrimarySkill::ATTACK); fillPrimarySkillData(node, heroClass, PrimarySkill::DEFENSE); fillPrimarySkillData(node, heroClass, PrimarySkill::SPELL_POWER); fillPrimarySkillData(node, heroClass, PrimarySkill::KNOWLEDGE); auto percentSumm = std::accumulate(heroClass->primarySkillLowLevel.begin(), heroClass->primarySkillLowLevel.end(), 0); if(percentSumm != 100) logMod->error("Hero class %s has wrong lowLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); percentSumm = std::accumulate(heroClass->primarySkillHighLevel.begin(), heroClass->primarySkillHighLevel.end(), 0); if(percentSumm != 100) logMod->error("Hero class %s has wrong highLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); for(auto skillPair : node["secondarySkills"].Struct()) { int probability = static_cast(skillPair.second.Integer()); VLC->modh->identifiers.requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID) { if(heroClass->secSkillProbability.size() <= skillID) heroClass->secSkillProbability.resize(skillID + 1, -1); // -1 = override with default later heroClass->secSkillProbability[skillID] = probability; }); } VLC->modh->identifiers.requestIdentifier ("creature", node["commander"], [=](si32 commanderID) { heroClass->commander = VLC->creh->objects[commanderID]; }); heroClass->defaultTavernChance = static_cast(node["defaultTavern"].Float()); for(const auto & tavern : node["tavern"].Struct()) { int value = static_cast(tavern.second.Float()); VLC->modh->identifiers.requestIdentifier(tavern.second.meta, "faction", tavern.first, [=](si32 factionID) { heroClass->selectionProbability[factionID] = value; }); } VLC->modh->identifiers.requestIdentifier("faction", node["faction"], [=](si32 factionID) { heroClass->faction = factionID; }); VLC->modh->identifiers.requestIdentifier(scope, "object", "hero", [=](si32 index) { JsonNode classConf = node["mapObject"]; classConf["heroClass"].String() = identifier; classConf.setMeta(scope); VLC->objtypeh->loadSubObject(identifier, classConf, index, heroClass->getIndex()); }); return heroClass; } std::vector CHeroClassHandler::loadLegacyData() { size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_HERO_CLASS); objects.resize(dataSize); std::vector h3Data; h3Data.reserve(dataSize); CLegacyConfigParser parser("DATA/HCTRAITS.TXT"); parser.endLine(); // header parser.endLine(); for (size_t i=0; i set selection probability if it was not set before in tavern entries for(CHeroClass * heroClass : objects) { for(CFaction * faction : VLC->townh->objects) { if (!faction->town) continue; if (heroClass->selectionProbability.count(faction->getIndex())) continue; auto chance = static_cast(heroClass->defaultTavernChance * faction->town->defaultTavernChance); heroClass->selectionProbability[faction->getIndex()] = static_cast(sqrt(chance) + 0.5); //FIXME: replace with std::round once MVS supports it } // set default probabilities for gaining secondary skills where not loaded previously heroClass->secSkillProbability.resize(VLC->skillh->size(), -1); for(int skillID = 0; skillID < VLC->skillh->size(); skillID++) { if(heroClass->secSkillProbability[skillID] < 0) { const CSkill * skill = (*VLC->skillh)[SecondarySkill(skillID)]; logMod->trace("%s: no probability for %s, using default", heroClass->identifier, skill->getJsonKey()); heroClass->secSkillProbability[skillID] = skill->gainChance[heroClass->affinity]; } } } for(CHeroClass * hc : objects) { if (!hc->imageMapMale.empty()) { JsonNode templ; templ["animation"].String() = hc->imageMapMale; VLC->objtypeh->getHandlerFor(Obj::HERO, hc->getIndex())->addTemplate(templ); } } } std::vector CHeroClassHandler::getDefaultAllowed() const { return std::vector(size(), true); } CHeroClassHandler::~CHeroClassHandler() = default; CHeroHandler::~CHeroHandler() = default; CHeroHandler::CHeroHandler() { loadExperience(); } const std::vector & CHeroHandler::getTypeNames() const { static const std::vector typeNames = { "hero" }; return typeNames; } CHero * CHeroHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) { assert(identifier.find(':') == std::string::npos); assert(!scope.empty()); auto * hero = new CHero(); hero->ID = HeroTypeID(index); hero->identifier = identifier; hero->modScope = scope; hero->sex = node["female"].Bool(); hero->special = node["special"].Bool(); VLC->generaltexth->registerString(scope, hero->getNameTextID(), node["texts"]["name"].String()); VLC->generaltexth->registerString(scope, hero->getBiographyTextID(), node["texts"]["biography"].String()); VLC->generaltexth->registerString(scope, hero->getSpecialtyNameTextID(), node["texts"]["specialty"]["name"].String()); VLC->generaltexth->registerString(scope, hero->getSpecialtyTooltipTextID(), node["texts"]["specialty"]["tooltip"].String()); VLC->generaltexth->registerString(scope, hero->getSpecialtyDescriptionTextID(), node["texts"]["specialty"]["description"].String()); hero->iconSpecSmall = node["images"]["specialtySmall"].String(); hero->iconSpecLarge = node["images"]["specialtyLarge"].String(); hero->portraitSmall = node["images"]["small"].String(); hero->portraitLarge = node["images"]["large"].String(); hero->battleImage = node["battleImage"].String(); loadHeroArmy(hero, node); loadHeroSkills(hero, node); loadHeroSpecialty(hero, node); VLC->modh->identifiers.requestIdentifier("heroClass", node["class"], [=](si32 classID) { hero->heroClass = classes[HeroClassID(classID)]; }); return hero; } void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const { assert(node["army"].Vector().size() <= 3); // anything bigger is useless - army initialization uses up to 3 slots hero->initialArmy.resize(node["army"].Vector().size()); for (size_t i=0; i< hero->initialArmy.size(); i++) { const JsonNode & source = node["army"].Vector()[i]; hero->initialArmy[i].minAmount = static_cast(source["min"].Float()); hero->initialArmy[i].maxAmount = static_cast(source["max"].Float()); assert(hero->initialArmy[i].minAmount <= hero->initialArmy[i].maxAmount); VLC->modh->identifiers.requestIdentifier("creature", source["creature"], [=](si32 creature) { hero->initialArmy[i].creature = CreatureID(creature); }); } } void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) const { for(const JsonNode &set : node["skills"].Vector()) { int skillLevel = static_cast(boost::range::find(NSecondarySkill::levels, set["level"].String()) - std::begin(NSecondarySkill::levels)); if (skillLevel < SecSkillLevel::LEVELS_SIZE) { size_t currentIndex = hero->secSkillsInit.size(); hero->secSkillsInit.emplace_back(SecondarySkill(-1), skillLevel); VLC->modh->identifiers.requestIdentifier("skill", set["skill"], [=](si32 id) { hero->secSkillsInit[currentIndex].first = SecondarySkill(id); }); } else { logMod->error("Unknown skill level: %s", set["level"].String()); } } // spellbook is considered present if hero have "spellbook" entry even when this is an empty set (0 spells) hero->haveSpellBook = !node["spellbook"].isNull(); for(const JsonNode & spell : node["spellbook"].Vector()) { VLC->modh->identifiers.requestIdentifier("spell", spell, [=](si32 spellID) { hero->spells.insert(SpellID(spellID)); }); } } // add standard creature specialty to result void AddSpecialtyForCreature(int creatureID, std::shared_ptr bonus, std::vector> &result) { const CCreature &specBaseCreature = *VLC->creh->objects[creatureID]; //base creature in which we have specialty bonus->limiter.reset(new CCreatureTypeLimiter(specBaseCreature, true)); bonus->type = Bonus::STACKS_SPEED; bonus->valType = Bonus::ADDITIVE_VALUE; bonus->val = 1; result.push_back(bonus); // attack and defense may differ for upgraded creatures => separate bonuses std::vector specTargets; specTargets.push_back(creatureID); specTargets.insert(specTargets.end(), specBaseCreature.upgrades.begin(), specBaseCreature.upgrades.end()); for(int cid : specTargets) { const CCreature &specCreature = *VLC->creh->objects[cid]; bonus = std::make_shared(*bonus); bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); bonus->type = Bonus::PRIMARY_SKILL; bonus->val = 0; int stepSize = specCreature.level ? specCreature.level : 5; bonus->subtype = PrimarySkill::ATTACK; bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getAttack(false), stepSize)); result.push_back(bonus); bonus = std::make_shared(*bonus); bonus->subtype = PrimarySkill::DEFENSE; bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefense(false), stepSize)); result.push_back(bonus); } } // convert deprecated format std::vector> SpecialtyInfoToBonuses(const SSpecialtyInfo & spec, int sid) { std::vector> result; std::shared_ptr bonus = std::make_shared(); bonus->duration = Bonus::PERMANENT; bonus->source = Bonus::HERO_SPECIAL; bonus->sid = sid; bonus->val = spec.val; switch (spec.type) { case 1: //creature specialty AddSpecialtyForCreature(spec.additionalinfo, bonus, result); break; case 2: //secondary skill { auto params = BonusParams("SECONDARY_SKILL_PREMY", "", spec.subtype); bonus->type = params.type; if(params.subtypeRelevant) bonus->subtype = params.subtype; bonus->valType = Bonus::PERCENT_TO_TARGET_TYPE; bonus->targetSourceType = Bonus::SECONDARY_SKILL; bonus->updater.reset(new TimesHeroLevelUpdater()); result.push_back(bonus); break; } case 3: //spell damage bonus, level dependent but calculated elsewhere bonus->type = Bonus::SPECIAL_SPELL_LEV; bonus->subtype = spec.subtype; bonus->updater.reset(new TimesHeroLevelUpdater()); result.push_back(bonus); break; case 4: //creature stat boost switch (spec.subtype) { case 1: bonus->type = Bonus::PRIMARY_SKILL; bonus->subtype = PrimarySkill::ATTACK; break; case 2: bonus->type = Bonus::PRIMARY_SKILL; bonus->subtype = PrimarySkill::DEFENSE; break; case 3: bonus->type = Bonus::CREATURE_DAMAGE; bonus->subtype = 0; //both min and max break; case 4: bonus->type = Bonus::STACK_HEALTH; break; case 5: bonus->type = Bonus::STACKS_SPEED; break; default: logMod->warn("Unknown subtype for specialty 4"); return result; } bonus->valType = Bonus::ADDITIVE_VALUE; bonus->limiter.reset(new CCreatureTypeLimiter(*VLC->creh->objects[spec.additionalinfo], true)); result.push_back(bonus); break; case 5: //spell damage bonus in percent bonus->type = Bonus::SPECIFIC_SPELL_DAMAGE; bonus->valType = Bonus::BASE_NUMBER; //current spell system is screwed bonus->subtype = spec.subtype; //spell id result.push_back(bonus); break; case 6: //damage bonus for bless (Adela) { auto limiter = std::make_shared(Bonus::GENERAL_DAMAGE_PREMY,Bonus::SPELL_EFFECT); limiter->sid = spec.subtype; //spell id if you ever wanted to use it otherwise limiter->isSourceIDRelevant = true; bonus->type = Bonus::GENERAL_DAMAGE_PREMY; bonus->updater.reset(new TimesHeroLevelUpdater()); bonus->addLimiter(limiter); result.push_back(bonus); break; } case 7: //maxed mastery for spell bonus->type = Bonus::SPELL; bonus->subtype = spec.subtype; //spell id bonus->val = 3; //to match MAXED_SPELL bonus->valType = Bonus::INDEPENDENT_MAX; result.push_back(bonus); break; case 8: //peculiar spells - enchantments bonus->type = Bonus::SPECIAL_PECULIAR_ENCHANT; bonus->subtype = spec.subtype; //spell id bonus->additionalInfo = spec.additionalinfo; //0, 1 for Coronius result.push_back(bonus); break; case 9: //upgrade creatures { const auto &creatures = VLC->creh->objects; bonus->type = Bonus::SPECIAL_UPGRADE; bonus->subtype = spec.subtype; //base id bonus->additionalInfo = spec.additionalinfo; //target id result.push_back(bonus); //propagate for regular upgrades of base creature for(const auto & cre_id : creatures[spec.subtype]->upgrades) { std::shared_ptr upgradeUpgradedVersion = std::make_shared(*bonus); upgradeUpgradedVersion->subtype = cre_id; result.push_back(upgradeUpgradedVersion); } } break; case 10: //resource generation bonus->type = Bonus::GENERATE_RESOURCE; bonus->subtype = spec.subtype; result.push_back(bonus); break; case 11: //starting skill with mastery (Adrienne) logMod->warn("Secondary skill mastery is no longer supported as specialty."); break; case 12: //army speed bonus->type = Bonus::STACKS_SPEED; result.push_back(bonus); break; case 13: //Dragon bonuses (Mutare) bonus->type = Bonus::PRIMARY_SKILL; bonus->valType = Bonus::ADDITIVE_VALUE; switch(spec.subtype) { case 1: bonus->subtype = PrimarySkill::ATTACK; break; case 2: bonus->subtype = PrimarySkill::DEFENSE; break; } bonus->limiter.reset(new HasAnotherBonusLimiter(Bonus::DRAGON_NATURE)); result.push_back(bonus); break; default: logMod->warn("Unknown hero specialty %d", spec.type); break; } return result; } void CHeroHandler::beforeValidate(JsonNode & object) { //handle "base" specialty info JsonNode & specialtyNode = object["specialty"]; if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT) { const JsonNode & base = specialtyNode["base"]; if(!base.isNull()) { if(specialtyNode["bonuses"].isNull()) { logMod->warn("specialty has base without bonuses"); } else { JsonMap & bonuses = specialtyNode["bonuses"].Struct(); for(std::pair keyValue : bonuses) JsonUtils::inherit(bonuses[keyValue.first], base); } } } } void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) const { int sid = hero->ID.getNum(); auto prepSpec = [=](std::shared_ptr bonus) { bonus->duration = Bonus::PERMANENT; bonus->source = Bonus::HERO_SPECIAL; bonus->sid = sid; return bonus; }; //new format, using bonus system const JsonNode & specialtyNode = node["specialty"]; if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT) { //creature specialty - alias for simplicity if(!specialtyNode["creature"].isNull()) { VLC->modh->identifiers.requestIdentifier("creature", specialtyNode["creature"], [hero](si32 creature) { // use legacy format for delayed conversion (must have all creature data loaded, also for upgrades) SSpecialtyInfo spec{}; spec.type = 1; spec.additionalinfo = creature; hero->specDeprecated.push_back(spec); }); } if(!specialtyNode["bonuses"].isNull()) { //proper new format for(const auto & keyValue : specialtyNode["bonuses"].Struct()) hero->specialty.push_back(prepSpec(JsonUtils::parseBonus(keyValue.second))); } } else logMod->error("Unsupported speciality format for hero %s!", hero->getNameTranslated()); } void CHeroHandler::loadExperience() { expPerLevel.push_back(0); expPerLevel.push_back(1000); expPerLevel.push_back(2000); expPerLevel.push_back(3200); expPerLevel.push_back(4600); expPerLevel.push_back(6200); expPerLevel.push_back(8000); expPerLevel.push_back(10000); expPerLevel.push_back(12200); expPerLevel.push_back(14700); expPerLevel.push_back(17500); expPerLevel.push_back(20600); expPerLevel.push_back(24320); expPerLevel.push_back(28784); expPerLevel.push_back(34140); while (expPerLevel[expPerLevel.size() - 1] > expPerLevel[expPerLevel.size() - 2]) { auto i = expPerLevel.size() - 1; auto diff = expPerLevel[i] - expPerLevel[i-1]; diff += diff / 5; expPerLevel.push_back (expPerLevel[i] + diff); } expPerLevel.pop_back();//last value is broken } /// convert h3-style ID (e.g. Gobin Wolf Rider) to vcmi (e.g. goblinWolfRider) static std::string genRefName(std::string input) { boost::algorithm::replace_all(input, " ", ""); //remove spaces input[0] = std::tolower(input[0]); // to camelCase return input; } std::vector CHeroHandler::loadLegacyData() { size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_HERO); objects.resize(dataSize); std::vector h3Data; h3Data.reserve(dataSize); CLegacyConfigParser specParser("DATA/HEROSPEC.TXT"); CLegacyConfigParser bioParser("DATA/HEROBIOS.TXT"); CLegacyConfigParser parser("DATA/HOTRAITS.TXT"); parser.endLine(); //ignore header parser.endLine(); specParser.endLine(); //ignore header specParser.endLine(); for (int i=0; iimageIndex = static_cast(index) + GameConstants::HERO_PORTRAIT_SHIFT; // 2 special frames + some extra portraits objects.emplace_back(object); registerObject(scope, "hero", name, object->getIndex()); } void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) { auto * object = loadFromJson(scope, data, name, index); object->imageIndex = static_cast(index); assert(objects[index] == nullptr); // ensure that this id was not loaded before objects[index] = object; registerObject(scope, "hero", name, object->getIndex()); } void CHeroHandler::afterLoadFinalization() { for(auto & hero : objects) { for(const auto & bonus : hero->specialty) { bonus->sid = hero->getIndex(); } if(!hero->specDeprecated.empty()) { logMod->debug("Converting specialty format for hero %s(%s)", hero->getNameTranslated(), FactionID::encode(hero->heroClass->faction)); std::vector> convertedBonuses; for(const SSpecialtyInfo & spec : hero->specDeprecated) { for(const std::shared_ptr & b : SpecialtyInfoToBonuses(spec, hero->ID.getNum())) convertedBonuses.push_back(b); } hero->specDeprecated.clear(); // store and create json for logging std::vector specVec; std::vector specNames; for(const std::shared_ptr & bonus : convertedBonuses) { hero->specialty.push_back(bonus); specVec.push_back(bonus->toJsonNode()); // find fitting & unique bonus name std::string bonusName = bonus->nameForBonus(); if(vstd::contains(specNames, bonusName)) { int suffix = 2; while(vstd::contains(specNames, bonusName + std::to_string(suffix))) suffix++; bonusName += std::to_string(suffix); } specNames.push_back(bonusName); } // log new format for easy copy-and-paste JsonNode specNode(JsonNode::JsonType::DATA_STRUCT); if(specVec.size() > 1) { JsonNode base = JsonUtils::intersect(specVec); if(base.containsBaseData()) { specNode["base"] = base; for(JsonNode & node : specVec) node = JsonUtils::difference(node, base); } } // add json for bonuses specNode["bonuses"].Struct(); for(int i = 0; i < specVec.size(); i++) specNode["bonuses"][specNames[i]] = specVec[i]; logMod->trace("\"specialty\" : %s", specNode.toJson(true)); } } } ui32 CHeroHandler::level (ui64 experience) const { return static_cast(boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel)); } ui64 CHeroHandler::reqExp (ui32 level) const { if(!level) return 0; if (level <= expPerLevel.size()) { return expPerLevel[level-1]; } else { logGlobal->warn("A hero has reached unsupported amount of experience"); return expPerLevel[expPerLevel.size()-1]; } } std::vector CHeroHandler::getDefaultAllowed() const { // Look Data/HOTRAITS.txt for reference std::vector allowedHeroes; allowedHeroes.reserve(size()); for(const CHero * hero : objects) { allowedHeroes.push_back(!hero->special); } return allowedHeroes; } VCMI_LIB_NAMESPACE_END