/* * CCreatureHandler.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 "CCreatureHandler.h" #include "CGeneralTextHandler.h" #include "ResourceSet.h" #include "filesystem/Filesystem.h" #include "VCMI_Lib.h" #include "CRandomGenerator.h" #include "CTownHandler.h" #include "GameSettings.h" #include "constants/StringConstants.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" #include "json/JsonBonus.h" #include "serializer/JsonDeserializer.h" #include "serializer/JsonUpdater.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" #include "modding/CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN const std::map CCreature::creatureQuantityRanges = { {CCreature::CreatureQuantityId::FEW, "1-4"}, {CCreature::CreatureQuantityId::SEVERAL, "5-9"}, {CCreature::CreatureQuantityId::PACK, "10-19"}, {CCreature::CreatureQuantityId::LOTS, "20-49"}, {CCreature::CreatureQuantityId::HORDE, "50-99"}, {CCreature::CreatureQuantityId::THRONG, "100-249"}, {CCreature::CreatureQuantityId::SWARM, "250-499"}, {CCreature::CreatureQuantityId::ZOUNDS, "500-999"}, {CCreature::CreatureQuantityId::LEGION, "1000+"} }; int32_t CCreature::getIndex() const { return idNumber.toEnum(); } int32_t CCreature::getIconIndex() const { return iconIndex; } std::string CCreature::getJsonKey() const { return modScope + ':' + identifier; } void CCreature::registerIcons(const IconRegistar & cb) const { cb(getIconIndex(), 0, "CPRSMALL", smallIconName); cb(getIconIndex(), 0, "TWCRPORT", largeIconName); } CreatureID CCreature::getId() const { return idNumber; } const IBonusBearer * CCreature::getBonusBearer() const { return this; } int32_t CCreature::getAdvMapAmountMin() const { return ammMin; } int32_t CCreature::getAdvMapAmountMax() const { return ammMax; } int32_t CCreature::getAIValue() const { return AIValue; } int32_t CCreature::getFightValue() const { return fightValue; } int32_t CCreature::getLevel() const { return level; } int32_t CCreature::getGrowth() const { return growth; } int32_t CCreature::getHorde() const { return hordeGrowth; } FactionID CCreature::getFaction() const { return FactionID(faction); } int32_t CCreature::getBaseAttack() const { static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDefense() const { static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDamageMin() const { static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDamageMax() const { static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseHitPoints() const { static const auto SELECTOR = Selector::type()(BonusType::STACK_HEALTH).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseSpellPoints() const { static const auto SELECTOR = Selector::type()(BonusType::CASTS).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseSpeed() const { static const auto SELECTOR = Selector::type()(BonusType::STACKS_SPEED).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseShots() const { static const auto SELECTOR = Selector::type()(BonusType::SHOTS).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getRecruitCost(GameResID resIndex) const { if(resIndex.getNum() >= 0 && resIndex.getNum() < cost.size()) return cost[resIndex]; else return 0; } TResources CCreature::getFullRecruitCost() const { return cost; } bool CCreature::hasUpgrades() const { return !upgrades.empty(); } std::string CCreature::getNameTranslated() const { return getNameSingularTranslated(); } std::string CCreature::getNamePluralTranslated() const { return VLC->generaltexth->translate(getNamePluralTextID()); } std::string CCreature::getNameSingularTranslated() const { return VLC->generaltexth->translate(getNameSingularTextID()); } std::string CCreature::getNameTextID() const { return getNameSingularTextID(); } std::string CCreature::getDescriptionTranslated() const { return VLC->generaltexth->translate(getDescriptionTextID()); } std::string CCreature::getNamePluralTextID() const { return TextIdentifier("creatures", modScope, identifier, "name", "plural" ).get(); } std::string CCreature::getNameSingularTextID() const { return TextIdentifier("creatures", modScope, identifier, "name", "singular" ).get(); } std::string CCreature::getDescriptionTextID() const { return TextIdentifier("creatures", modScope, identifier, "description").get(); } CCreature::CreatureQuantityId CCreature::getQuantityID(const int & quantity) { if (quantity<5) return CCreature::CreatureQuantityId::FEW; if (quantity<10) return CCreature::CreatureQuantityId::SEVERAL; if (quantity<20) return CCreature::CreatureQuantityId::PACK; if (quantity<50) return CCreature::CreatureQuantityId::LOTS; if (quantity<100) return CCreature::CreatureQuantityId::HORDE; if (quantity<250) return CCreature::CreatureQuantityId::THRONG; if (quantity<500) return CCreature::CreatureQuantityId::SWARM; if (quantity<1000) return CCreature::CreatureQuantityId::ZOUNDS; return CCreature::CreatureQuantityId::LEGION; } std::string CCreature::getQuantityRangeStringForId(const CCreature::CreatureQuantityId & quantityId) { if(creatureQuantityRanges.find(quantityId) != creatureQuantityRanges.end()) return creatureQuantityRanges.at(quantityId); logGlobal->error("Wrong quantityId: %d", (int)quantityId); assert(0); return "[ERROR]"; } int CCreature::estimateCreatureCount(ui32 countID) { static const int creature_count[] = { 0, 3, 8, 15, 35, 75, 175, 375, 750, 2500 }; if(countID > 9) { logGlobal->error("Wrong countID %d!", countID); return 0; } else return creature_count[countID]; } bool CCreature::isDoubleWide() const { return doubleWide; } /** * Determines if the creature is of a good alignment. * @return true if the creture is good, false otherwise. */ bool CCreature::isGood () const { return VLC->factions()->getById(faction)->getAlignment() == EAlignment::GOOD; } /** * Determines if the creature is of an evil alignment. * @return true if the creature is evil, false otherwise. */ bool CCreature::isEvil () const { return VLC->factions()->getById(faction)->getAlignment() == EAlignment::EVIL; } si32 CCreature::maxAmount(const TResources &res) const //how many creatures can be bought { int ret = 2147483645; int resAmnt = static_cast(std::min(res.size(),cost.size())); for(int i=0;i(BonusDuration::PERMANENT, type, BonusSource::CREATURE_ABILITY, val, BonusSourceID(getId()), subtype, BonusValueType::BASE_NUMBER); addNewBonus(added); } else { std::shared_ptr b = existing[0]; b->val = val; } } bool CCreature::isMyUpgrade(const CCreature *anotherCre) const { //TODO upgrade of upgrade? return vstd::contains(upgrades, anotherCre->getId()); } bool CCreature::valid() const { return this == (*VLC->creh)[idNumber]; } std::string CCreature::nodeName() const { return "\"" + getNamePluralTextID() + "\""; } void CCreature::updateFrom(const JsonNode & data) { JsonUpdater handler(nullptr, data); { auto configScope = handler.enterStruct("config"); const JsonNode & configNode = handler.getCurrent(); serializeJson(handler); if(!configNode["hitPoints"].isNull()) addBonus(configNode["hitPoints"].Integer(), BonusType::STACK_HEALTH); if(!configNode["speed"].isNull()) addBonus(configNode["speed"].Integer(), BonusType::STACKS_SPEED); if(!configNode["attack"].isNull()) addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)); if(!configNode["defense"].isNull()) addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)); if(!configNode["damage"]["min"].isNull()) addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); if(!configNode["damage"]["max"].isNull()) addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); if(!configNode["shots"].isNull()) addBonus(configNode["shots"].Integer(), BonusType::SHOTS); if(!configNode["spellPoints"].isNull()) addBonus(configNode["spellPoints"].Integer(), BonusType::CASTS); } handler.serializeBonuses("bonuses", this); } void CCreature::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("fightValue", fightValue); handler.serializeInt("aiValue", AIValue); handler.serializeInt("growth", growth); handler.serializeInt("horde", hordeGrowth);// Needed at least until configurable buildings { auto advMapNode = handler.enterStruct("advMapAmount"); handler.serializeInt("min", ammMin); handler.serializeInt("max", ammMax); } if(handler.updating) { cost.serializeJson(handler, "cost"); handler.serializeId("faction", faction); } handler.serializeInt("level", level); handler.serializeBool("doubleWide", doubleWide); if(!handler.saving) { if(ammMin > ammMax) { logMod->error("Invalid creature '%s' configuration, advMapAmount.min > advMapAmount.max", identifier); std::swap(ammMin, ammMax); } } } CCreatureHandler::CCreatureHandler() : expAfterUpgrade(0) { loadCommanders(); } void CCreatureHandler::loadCommanders() { auto configResource = JsonPath::builtin("config/commanders.json"); std::string modSource = VLC->modh->findResourceOrigin(configResource); JsonNode data(configResource); data.setModScope(modSource); const JsonNode & config = data; // switch to const data accessors for (auto bonus : config["bonusPerLevel"].Vector()) { commanderLevelPremy.push_back(JsonUtils::parseBonus(bonus.Vector())); } int i = 0; for (auto skill : config["skillLevels"].Vector()) { skillLevels.emplace_back(); for (auto skillLevel : skill["levels"].Vector()) { skillLevels[i].push_back(static_cast(skillLevel.Float())); } ++i; } for (auto ability : config["abilityRequirements"].Vector()) { std::pair , std::pair > a; a.first = JsonUtils::parseBonus (ability["ability"].Vector()); a.second.first = static_cast(ability["skills"].Vector()[0].Float()); a.second.second = static_cast(ability["skills"].Vector()[1].Float()); skillRequirements.push_back (a); } } void CCreatureHandler::loadBonuses(JsonNode & creature, std::string bonuses) const { auto makeBonusNode = [&](const std::string & type, double val = 0) -> JsonNode { JsonNode ret; ret["type"].String() = type; ret["val"].Float() = val; return ret; }; static const std::map abilityMap = { {"FLYING_ARMY", makeBonusNode("FLYING")}, {"SHOOTING_ARMY", makeBonusNode("SHOOTER")}, {"SIEGE_WEAPON", makeBonusNode("SIEGE_WEAPON")}, {"const_free_attack", makeBonusNode("BLOCKS_RETALIATION")}, {"IS_UNDEAD", makeBonusNode("UNDEAD")}, {"const_no_melee_penalty", makeBonusNode("NO_MELEE_PENALTY")}, {"const_jousting", makeBonusNode("JOUSTING", 5)}, {"KING_1", makeBonusNode("KING")}, // Slayer with no expertise {"KING_2", makeBonusNode("KING", 2)}, // Advanced Slayer or better {"KING_3", makeBonusNode("KING", 3)}, // Expert Slayer only {"const_no_wall_penalty", makeBonusNode("NO_WALL_PENALTY")}, {"MULTI_HEADED", makeBonusNode("ATTACKS_ALL_ADJACENT")}, {"IMMUNE_TO_MIND_SPELLS", makeBonusNode("MIND_IMMUNITY")}, {"HAS_EXTENDED_ATTACK", makeBonusNode("TWO_HEX_ATTACK_BREATH")} }; auto hasAbility = [&](const std::string & name) -> bool { return boost::algorithm::find_first(bonuses, name); }; for(const auto & a : abilityMap) { if(hasAbility(a.first)) creature["abilities"][a.first] = a.second; } if(hasAbility("DOUBLE_WIDE")) creature["doubleWide"].Bool() = true; if(hasAbility("const_raises_morale")) { JsonNode node = makeBonusNode("MORALE"); node["val"].Float() = 1; node["propagator"].String() = "HERO"; creature["abilities"]["const_raises_morale"] = node; } } std::vector CCreatureHandler::loadLegacyData() { size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_CREATURE); objects.resize(dataSize); std::vector h3Data; h3Data.reserve(dataSize); CLegacyConfigParser parser(TextPath::builtin("DATA/CRTRAITS.TXT")); parser.endLine(); // header // this file is a bit different in some of Russian localisations: //ENG: Singular Plural Wood ... //RUS: Singular Plural Plural2 Wood ... // Try to detect which version this is by header // TODO: use 3rd name? Stand for "whose", e.g. pikemans' size_t namesCount = 2; { if ( parser.readString() != "Singular" || parser.readString() != "Plural" ) throw std::runtime_error("Incorrect format of CrTraits.txt"); if (parser.readString() == "Plural2") namesCount = 3; parser.endLine(); } for (size_t i=0; iidNumber = CreatureID(index); cre->iconIndex = cre->getIndex() + 2; cre->identifier = identifier; cre->modScope = scope; JsonDeserializer handler(nullptr, node); cre->serializeJson(handler); cre->cost = ResourceSet(node["cost"]); VLC->generaltexth->registerString(scope, cre->getNameSingularTextID(), node["name"]["singular"].String()); VLC->generaltexth->registerString(scope, cre->getNamePluralTextID(), node["name"]["plural"].String()); VLC->generaltexth->registerString(scope, cre->getDescriptionTextID(), node["description"].String()); cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH); cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED); cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)); cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)); int minDamage = node["damage"]["min"].Integer(); int maxDamage = node["damage"]["max"].Integer(); if (minDamage <= maxDamage) { cre->addBonus(minDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); cre->addBonus(maxDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); } else { logMod->error("Mod %s: creature %s has minimal damage (%d) greater than maximal damage (%d)!", scope, identifier, minDamage, maxDamage); cre->addBonus(maxDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); cre->addBonus(minDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); } if(!node["shots"].isNull()) cre->addBonus(node["shots"].Integer(), BonusType::SHOTS); loadStackExperience(cre, node["stackExperience"]); loadJsonAnimation(cre, node["graphics"]); loadCreatureJson(cre, node); for(const auto & extraName : node["extraNames"].Vector()) { for(const auto & type_name : getTypeNames()) registerObject(scope, type_name, extraName.String(), cre->getIndex()); } JsonNode advMapFile = node["graphics"]["map"]; JsonNode advMapMask = node["graphics"]["mapMask"]; VLC->identifiers()->requestIdentifier(scope, "object", "monster", [cre, scope, advMapFile, advMapMask](si32 index) { JsonNode conf; conf.setModScope(scope); VLC->objtypeh->loadSubObject(cre->identifier, conf, Obj::MONSTER, cre->getId().num); if (!advMapFile.isNull()) { JsonNode templ; templ["animation"] = advMapFile; if (!advMapMask.isNull()) templ["mask"] = advMapMask; templ.setModScope(scope); // if creature has custom advMapFile, reset any potentially imported H3M templates and use provided file instead VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->clearTemplates(); VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->addTemplate(templ); } // object does not have any templates - this is not usable object (e.g. pseudo-creature like Arrow Tower) if (VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->getTemplates().empty()) { assert(cre->special); if (!cre->special) logMod->error("Creature %s does not have valid map object but is not marked as special!", cre->getJsonKey()); VLC->objtypeh->removeSubObject(Obj::MONSTER, cre->getId().num); } }); return cre; } const std::vector & CCreatureHandler::getTypeNames() const { static const std::vector typeNames = { "creature" }; return typeNames; } void CCreatureHandler::loadCrExpMod() { if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience values { //Calculate rank exp values, formula appears complicated bu no parsing needed expRanks.resize(8); int dif = 0; int it = 8000; //ignore name of this variable expRanks[0].push_back(it); for (int j = 1; j < 10; ++j) //used for tiers 8-10, and all other probably { expRanks[0].push_back(expRanks[0][j-1] + it + dif); dif += it/5; } for (int i = 1; i < 8; ++i) //used for tiers 1-7 { dif = 0; it = 1000 * i; expRanks[i].push_back(it); for (int j = 1; j < 10; ++j) { expRanks[i].push_back(expRanks[i][j-1] + it + dif); dif += it/5; } } CLegacyConfigParser expBonParser(TextPath::builtin("DATA/CREXPMOD.TXT")); expBonParser.endLine(); //header maxExpPerBattle.resize(8); for (int i = 1; i < 8; ++i) { expBonParser.readString(); //index expBonParser.readString(); //float multiplier -> hardcoded expBonParser.readString(); //ignore upgrade mod? ->hardcoded expBonParser.readString(); //already calculated maxExpPerBattle[i] = static_cast(expBonParser.readNumber()); expRanks[i].push_back(expRanks[i].back() + static_cast(expBonParser.readNumber())); expBonParser.endLine(); } //exp for tier >7, rank 11 expRanks[0].push_back(147000); expAfterUpgrade = 75; //percent maxExpPerBattle[0] = maxExpPerBattle[7]; } } void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) { if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience bonuses { logGlobal->debug("\tLoading stack experience bonuses"); auto addBonusForAllCreatures = [&](std::shared_ptr b) { auto limiter = std::make_shared(); b->addLimiter(limiter); globalEffects.addNewBonus(b); }; auto addBonusForTier = [&](int tier, std::shared_ptr b) { assert(vstd::iswithin(tier, 1, 7)); //bonuses from level 7 are given to high-level creatures too auto max = tier == GameConstants::CREATURES_PER_TOWN ? std::numeric_limits::max() : tier + 1; auto limiter = std::make_shared(tier, max); b->addLimiter(limiter); globalEffects.addNewBonus(b); }; CLegacyConfigParser parser(TextPath::builtin("DATA/CREXPBON.TXT")); Bonus b; //prototype with some default properties b.source = BonusSource::STACK_EXPERIENCE; b.duration = BonusDuration::PERMANENT; b.valType = BonusValueType::ADDITIVE_VALUE; b.effectRange = BonusLimitEffect::NO_LIMIT; b.additionalInfo = 0; b.turnsRemain = 0; BonusList bl; parser.endLine(); parser.readString(); //ignore index loadStackExp(b, bl, parser); for(const auto & b : bl) addBonusForAllCreatures(b); //health bonus is common for all parser.endLine(); for (int i = 1; i < 7; ++i) { for (int j = 0; j < 4; ++j) //four modifiers common for tiers { parser.readString(); //ignore index bl.clear(); loadStackExp(b, bl, parser); for(const auto & b : bl) addBonusForTier(i, b); parser.endLine(); } } for (int j = 0; j < 4; ++j) //tier 7 { parser.readString(); //ignore index bl.clear(); loadStackExp(b, bl, parser); for(const auto & b : bl) addBonusForTier(7, b); parser.endLine(); } do //parse everything that's left { CreatureID sid = parser.readNumber(); //id = this particular creature ID b.sid = BonusSourceID(sid); bl.clear(); loadStackExp(b, bl, parser); for(const auto & b : bl) objects[sid.getNum()]->addNewBonus(b); //add directly to CCreature Node } while (parser.endLine()); }//end of Stack Experience } void CCreatureHandler::loadAnimationInfo(std::vector &h3Data) const { CLegacyConfigParser parser(TextPath::builtin("DATA/CRANIM.TXT")); parser.endLine(); // header parser.endLine(); for(int dd = 0; dd < VLC->settings()->getInteger(EGameSettings::TEXTS_CREATURE); ++dd) { while (parser.isNextEntryEmpty() && parser.endLine()) // skip empty lines ; loadUnitAnimInfo(h3Data[dd]["graphics"], parser); parser.endLine(); } } void CCreatureHandler::loadUnitAnimInfo(JsonNode & graphics, CLegacyConfigParser & parser) const { graphics["timeBetweenFidgets"].Float() = parser.readNumber(); JsonNode & animationTime = graphics["animationTime"]; animationTime["walk"].Float() = parser.readNumber(); animationTime["attack"].Float() = parser.readNumber(); parser.readNumber(); // unused value "Flight animation time" - H3 actually uses "Walk animation time" even for flying creatures animationTime["idle"].Float() = 10.0; JsonNode & missile = graphics["missile"]; JsonNode & offsets = missile["offset"]; offsets["upperX"].Float() = parser.readNumber(); offsets["upperY"].Float() = parser.readNumber(); offsets["middleX"].Float() = parser.readNumber(); offsets["middleY"].Float() = parser.readNumber(); offsets["lowerX"].Float() = parser.readNumber(); offsets["lowerY"].Float() = parser.readNumber(); for(int i=0; i<12; i++) { JsonNode entry; entry.Float() = parser.readNumber(); missile["frameAngles"].Vector().push_back(entry); } // Unused property "troopCountLocationOffset" parser.readNumber(); missile["attackClimaxFrame"].Float() = parser.readNumber(); // assume that creature is not a shooter and should not have whole missile field if (missile["frameAngles"].Vector()[0].Integer() == 0 && missile["attackClimaxFrame"].Integer() == 0) graphics.Struct().erase("missile"); } void CCreatureHandler::loadJsonAnimation(CCreature * cre, const JsonNode & graphics) const { cre->animation.timeBetweenFidgets = graphics["timeBetweenFidgets"].Float(); const JsonNode & animationTime = graphics["animationTime"]; cre->animation.walkAnimationTime = animationTime["walk"].Float(); cre->animation.idleAnimationTime = animationTime["idle"].Float(); cre->animation.attackAnimationTime = animationTime["attack"].Float(); const JsonNode & missile = graphics["missile"]; const JsonNode & offsets = missile["offset"]; cre->animation.upperRightMissleOffsetX = static_cast(offsets["upperX"].Float()); cre->animation.upperRightMissleOffsetY = static_cast(offsets["upperY"].Float()); cre->animation.rightMissleOffsetX = static_cast(offsets["middleX"].Float()); cre->animation.rightMissleOffsetY = static_cast(offsets["middleY"].Float()); cre->animation.lowerRightMissleOffsetX = static_cast(offsets["lowerX"].Float()); cre->animation.lowerRightMissleOffsetY = static_cast(offsets["lowerY"].Float()); cre->animation.attackClimaxFrame = static_cast(missile["attackClimaxFrame"].Float()); cre->animation.missleFrameAngles = missile["frameAngles"].convertTo >(); cre->smallIconName = graphics["iconSmall"].String(); cre->largeIconName = graphics["iconLarge"].String(); } void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & config) const { creature->animDefName = AnimationPath::fromJson(config["graphics"]["animation"]); //FIXME: MOD COMPATIBILITY if (config["abilities"].getType() == JsonNode::JsonType::DATA_STRUCT) { for(const auto & ability : config["abilities"].Struct()) { if (!ability.second.isNull()) { auto b = JsonUtils::parseBonus(ability.second); b->source = BonusSource::CREATURE_ABILITY; b->sid = BonusSourceID(creature->getId()); b->duration = BonusDuration::PERMANENT; creature->addNewBonus(b); } } } else { for(const JsonNode &ability : config["abilities"].Vector()) { if(ability.getType() == JsonNode::JsonType::DATA_VECTOR) { logMod->error("Ignored outdated creature ability format in %s", creature->getJsonKey()); } else { auto b = JsonUtils::parseBonus(ability); b->source = BonusSource::CREATURE_ABILITY; b->sid = BonusSourceID(creature->getId()); b->duration = BonusDuration::PERMANENT; creature->addNewBonus(b); } } } VLC->identifiers()->requestIdentifier("faction", config["faction"], [=](si32 faction) { creature->faction = FactionID(faction); }); for(const JsonNode &value : config["upgrades"].Vector()) { VLC->identifiers()->requestIdentifier("creature", value, [=](si32 identifier) { creature->upgrades.insert(CreatureID(identifier)); }); } creature->animation.projectileImageName = AnimationPath::fromJson(config["graphics"]["missile"]["projectile"]); for(const JsonNode & value : config["graphics"]["missile"]["ray"].Vector()) { CCreature::CreatureAnimation::RayColor color; color.start.r = value["start"].Vector()[0].Integer(); color.start.g = value["start"].Vector()[1].Integer(); color.start.b = value["start"].Vector()[2].Integer(); color.start.a = value["start"].Vector()[3].Integer(); color.end.r = value["end"].Vector()[0].Integer(); color.end.g = value["end"].Vector()[1].Integer(); color.end.b = value["end"].Vector()[2].Integer(); color.end.a = value["end"].Vector()[3].Integer(); creature->animation.projectileRay.push_back(color); } creature->special = config["special"].Bool() || config["disabled"].Bool(); creature->excludeFromRandomization = config["excludeFromRandomization"].Bool(); const JsonNode & sounds = config["sound"]; creature->sounds.attack = AudioPath::fromJson(sounds["attack"]); creature->sounds.defend = AudioPath::fromJson(sounds["defend"]); creature->sounds.killed = AudioPath::fromJson(sounds["killed"]); creature->sounds.move = AudioPath::fromJson(sounds["move"]); creature->sounds.shoot = AudioPath::fromJson(sounds["shoot"]); creature->sounds.wince = AudioPath::fromJson(sounds["wince"]); creature->sounds.startMoving = AudioPath::fromJson(sounds["startMoving"]); creature->sounds.endMoving = AudioPath::fromJson(sounds["endMoving"]); } void CCreatureHandler::loadStackExperience(CCreature * creature, const JsonNode & input) const { for (const JsonNode &exp : input.Vector()) { const JsonVector &values = exp["values"].Vector(); int lowerLimit = 1;//, upperLimit = 255; if (values[0].getType() == JsonNode::JsonType::DATA_BOOL) { for (const JsonNode &val : values) { if(val.Bool()) { // parse each bonus separately // we can not create copies since identifiers resolution does not tracks copies // leading to unset identifier values in copies auto bonus = JsonUtils::parseBonus (exp["bonus"]); bonus->source = BonusSource::STACK_EXPERIENCE; bonus->duration = BonusDuration::PERMANENT; bonus->limiter = std::make_shared(RankRangeLimiter(lowerLimit)); creature->addNewBonus (bonus); break; //TODO: allow bonuses to turn off? } ++lowerLimit; } } else { int lastVal = 0; for (const JsonNode &val : values) { if (val.Integer() != lastVal) { JsonNode bonusInput = exp["bonus"]; bonusInput["val"].Float() = val.Integer() - lastVal; auto bonus = JsonUtils::parseBonus (bonusInput); bonus->source = BonusSource::STACK_EXPERIENCE; bonus->duration = BonusDuration::PERMANENT; bonus->limiter.reset (new RankRangeLimiter(lowerLimit)); creature->addNewBonus (bonus); } lastVal = static_cast(val.Float()); ++lowerLimit; } } } } void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigParser & parser) const//help function for parsing CREXPBON.txt { bool enable = false; //some bonuses are activated with values 2 or 1 std::string buf = parser.readString(); std::string mod = parser.readString(); switch (buf[0]) { case 'H': b.type = BonusType::STACK_HEALTH; b.valType = BonusValueType::PERCENT_TO_BASE; break; case 'A': b.type = BonusType::PRIMARY_SKILL; b.subtype = BonusSubtypeID(PrimarySkill::ATTACK); break; case 'D': b.type = BonusType::PRIMARY_SKILL; b.subtype = BonusSubtypeID(PrimarySkill::DEFENSE); break; case 'M': //Max damage b.type = BonusType::CREATURE_DAMAGE; b.subtype = BonusCustomSubtype::creatureDamageMax; break; case 'm': //Min damage b.type = BonusType::CREATURE_DAMAGE; b.subtype = BonusCustomSubtype::creatureDamageMin; break; case 'S': b.type = BonusType::STACKS_SPEED; break; case 'O': b.type = BonusType::SHOTS; break; case 'b': b.type = BonusType::ENEMY_DEFENCE_REDUCTION; break; case 'C': b.type = BonusType::CHANGES_SPELL_COST_FOR_ALLY; break; case 'd': b.type = BonusType::DEFENSIVE_STANCE; break; case 'e': b.type = BonusType::DOUBLE_DAMAGE_CHANCE; break; case 'E': b.type = BonusType::DEATH_STARE; b.subtype = BonusCustomSubtype::deathStareGorgon; break; case 'F': b.type = BonusType::FEAR; break; case 'g': b.type = BonusType::SPELL_DAMAGE_REDUCTION; b.subtype = BonusSubtypeID(SpellSchool::ANY); break; case 'P': b.type = BonusType::CASTS; break; case 'R': b.type = BonusType::ADDITIONAL_RETALIATION; break; case 'W': b.type = BonusType::MAGIC_RESISTANCE; break; case 'f': //on-off skill enable = true; //sometimes format is: 2 -> 0, 1 -> 1 switch (mod[0]) { case 'A': b.type = BonusType::ATTACKS_ALL_ADJACENT; break; case 'b': b.type = BonusType::RETURN_AFTER_STRIKE; break; case 'B': b.type = BonusType::TWO_HEX_ATTACK_BREATH; break; case 'c': b.type = BonusType::JOUSTING; b.val = 5; break; case 'D': b.type = BonusType::ADDITIONAL_ATTACK; break; case 'f': b.type = BonusType::FEARLESS; break; case 'F': b.type = BonusType::FLYING; break; case 'm': b.type = BonusType::MORALE; b.val = 1; b.valType = BonusValueType::INDEPENDENT_MAX; break; case 'M': b.type = BonusType::NO_MORALE; break; case 'p': //Mind spells case 'P': b.type = BonusType::MIND_IMMUNITY; break; case 'r': b.type = BonusType::REBIRTH; //on/off? makes sense? b.subtype = BonusCustomSubtype::rebirthRegular; b.val = 20; //arbitrary value break; case 'R': b.type = BonusType::BLOCKS_RETALIATION; break; case 's': b.type = BonusType::FREE_SHOOTING; break; case 'u': b.type = BonusType::SPELL_RESISTANCE_AURA; break; case 'U': b.type = BonusType::UNDEAD; break; default: logGlobal->trace("Not parsed bonus %s %s", buf, mod); return; break; } break; case 'w': //specific spell immunities, enabled/disabled enable = true; switch (mod[0]) { case 'B': //Blind b.type = BonusType::SPELL_IMMUNITY; b.subtype = BonusSubtypeID(SpellID(SpellID::BLIND)); b.additionalInfo = 0;//normal immunity break; case 'H': //Hypnotize b.type = BonusType::SPELL_IMMUNITY; b.subtype = BonusSubtypeID(SpellID(SpellID::HYPNOTIZE)); b.additionalInfo = 0;//normal immunity break; case 'I': //Implosion b.type = BonusType::SPELL_IMMUNITY; b.subtype = BonusSubtypeID(SpellID(SpellID::IMPLOSION)); b.additionalInfo = 0;//normal immunity break; case 'K': //Berserk b.type = BonusType::SPELL_IMMUNITY; b.subtype = BonusSubtypeID(SpellID(SpellID::BERSERK)); b.additionalInfo = 0;//normal immunity break; case 'M': //Meteor Shower b.type = BonusType::SPELL_IMMUNITY; b.subtype = BonusSubtypeID(SpellID(SpellID::METEOR_SHOWER)); b.additionalInfo = 0;//normal immunity break; case 'N': //dispell beneficial spells b.type = BonusType::SPELL_IMMUNITY; b.subtype = BonusSubtypeID(SpellID(SpellID::DISPEL_HELPFUL_SPELLS)); b.additionalInfo = 0;//normal immunity break; case 'R': //Armageddon b.type = BonusType::SPELL_IMMUNITY; b.subtype = BonusSubtypeID(SpellID(SpellID::ARMAGEDDON)); b.additionalInfo = 0;//normal immunity break; case 'S': //Slow b.type = BonusType::SPELL_IMMUNITY; b.subtype = BonusSubtypeID(SpellID(SpellID::SLOW)); b.additionalInfo = 0;//normal immunity break; case '6': case '7': case '8': case '9': b.type = BonusType::LEVEL_SPELL_IMMUNITY; b.val = std::atoi(mod.c_str()) - 5; break; case ':': b.type = BonusType::LEVEL_SPELL_IMMUNITY; b.val = GameConstants::SPELL_LEVELS; //in case someone adds higher level spells? break; case 'F': b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; b.subtype = BonusSubtypeID(SpellSchool::FIRE); break; case 'O': b.type = BonusType::SPELL_DAMAGE_REDUCTION; b.subtype = BonusSubtypeID(SpellSchool::FIRE); b.val = 100; //Full damage immunity break; case 'f': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; b.subtype = BonusSubtypeID(SpellSchool::FIRE); break; case 'C': b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; b.subtype = BonusSubtypeID(SpellSchool::WATER); break; case 'W': b.type = BonusType::SPELL_DAMAGE_REDUCTION; b.subtype = BonusSubtypeID(SpellSchool::WATER); b.val = 100; //Full damage immunity break; case 'w': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; b.subtype = BonusSubtypeID(SpellSchool::WATER); break; case 'E': b.type = BonusType::SPELL_DAMAGE_REDUCTION; b.subtype = BonusSubtypeID(SpellSchool::EARTH); b.val = 100; //Full damage immunity break; case 'e': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; b.subtype = BonusSubtypeID(SpellSchool::EARTH); break; case 'A': b.type = BonusType::SPELL_DAMAGE_REDUCTION; b.subtype = BonusSubtypeID(SpellSchool::AIR); b.val = 100; //Full damage immunity break; case 'a': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; b.subtype = BonusSubtypeID(SpellSchool::AIR); break; case 'D': b.type = BonusType::SPELL_DAMAGE_REDUCTION; b.subtype = BonusSubtypeID(SpellSchool::ANY); b.val = 100; //Full damage immunity break; case '0': b.type = BonusType::RECEPTIVE; break; case 'm': b.type = BonusType::MIND_IMMUNITY; break; default: logGlobal->trace("Not parsed bonus %s %s", buf, mod); return; } break; case 'i': enable = true; b.type = BonusType::NO_DISTANCE_PENALTY; break; case 'o': enable = true; b.type = BonusType::NO_WALL_PENALTY; break; case 'a': case 'c': case 'K': case 'k': b.type = BonusType::SPELL_AFTER_ATTACK; b.subtype = BonusSubtypeID(SpellID(stringToNumber(mod))); break; case 'h': b.type = BonusType::HATE; b.subtype = BonusSubtypeID(CreatureID(stringToNumber(mod))); break; case 'p': case 'J': b.type = BonusType::SPELL_BEFORE_ATTACK; b.subtype = BonusSubtypeID(SpellID(stringToNumber(mod))); b.additionalInfo = 3; //always expert? break; case 'r': b.type = BonusType::HP_REGENERATION; b.val = stringToNumber(mod); break; case 's': b.type = BonusType::ENCHANTED; b.subtype = BonusSubtypeID(SpellID(stringToNumber(mod))); b.valType = BonusValueType::INDEPENDENT_MAX; break; default: logGlobal->trace("Not parsed bonus %s %s", buf, mod); return; break; } switch (mod[0]) { case '+': case '=': //should we allow percent values to stack or pick highest? b.valType = BonusValueType::ADDITIVE_VALUE; break; } //limiters, range si32 lastVal; si32 curVal; si32 lastLev = 0; if (enable) //0 and 2 means non-active, 1 - active { if (b.type != BonusType::REBIRTH) b.val = 0; //on-off ability, no value specified parser.readNumber(); // 0 level is never active for (int i = 1; i < 11; ++i) { curVal = static_cast(parser.readNumber()); if (curVal == 1) { b.limiter.reset (new RankRangeLimiter(i)); bl.push_back(std::make_shared(b)); break; //never turned off it seems } } } else { lastVal = static_cast(parser.readNumber()); if (b.type == BonusType::HATE) lastVal *= 10; //odd fix //FIXME: value for zero level should be stored in our config files (independent of stack exp) for (int i = 1; i < 11; ++i) { curVal = static_cast(parser.readNumber()); if (b.type == BonusType::HATE) curVal *= 10; //odd fix if (curVal > lastVal) //threshold, add new bonus { b.val = curVal - lastVal; lastVal = curVal; b.limiter.reset (new RankRangeLimiter(i)); bl.push_back(std::make_shared(b)); lastLev = i; //start new range from here, i = previous rank } else if (curVal < lastVal) { b.val = lastVal; b.limiter.reset (new RankRangeLimiter(lastLev, i)); } } } } int CCreatureHandler::stringToNumber(std::string & s) const { boost::algorithm::replace_first(s,"#",""); //drop hash character return std::atoi(s.c_str()); } CCreatureHandler::~CCreatureHandler() { for(auto & p : skillRequirements) p.first = nullptr; } CreatureID CCreatureHandler::pickRandomMonster(CRandomGenerator & rand, int tier) const { std::vector allowed; for(const auto & creature : objects) { if(creature->special) continue; if(creature->excludeFromRandomization) continue; if (creature->level == tier || tier == -1) allowed.push_back(creature->getId()); } if(allowed.empty()) { logGlobal->warn("Cannot pick a random creature of tier %d!", tier); return CreatureID::NONE; } return *RandomGeneratorUtil::nextItem(allowed, rand); } void CCreatureHandler::afterLoadFinalization() { } VCMI_LIB_NAMESPACE_END