1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-26 22:57:00 +02:00
vcmi/lib/CCreatureHandler.cpp
2024-04-27 18:41:21 +02:00

1380 lines
40 KiB
C++

/*
* 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::CreatureQuantityId, std::string> 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<int>(std::min(res.size(),cost.size()));
for(int i=0;i<resAmnt;i++)
if(cost[i])
ret = std::min(ret, (res[i] / cost[i]));
return ret;
}
CCreature::CCreature()
{
setNodeType(CBonusSystemNode::CREATURE);
fightValue = AIValue = growth = hordeGrowth = ammMin = ammMax = 0;
}
void CCreature::addBonus(int val, BonusType type)
{
addBonus(val, type, BonusSubtypeID());
}
void CCreature::addBonus(int val, BonusType type, BonusSubtypeID subtype)
{
auto selector = Selector::typeSubtype(type, subtype).And(Selector::source(BonusSource::CREATURE_ABILITY, BonusSourceID(getId())));
BonusList & exported = getExportedBonusList();
BonusList existing;
exported.getBonuses(existing, selector, Selector::all);
if(existing.empty())
{
auto added = std::make_shared<Bonus>(BonusDuration::PERMANENT, type, BonusSource::CREATURE_ABILITY, val, BonusSourceID(getId()), subtype, BonusValueType::BASE_NUMBER);
addNewBonus(added);
}
else
{
std::shared_ptr<Bonus> 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<ui8>(skillLevel.Float()));
}
++i;
}
for (auto ability : config["abilityRequirements"].Vector())
{
std::pair <std::shared_ptr<Bonus>, std::pair <ui8, ui8> > a;
a.first = JsonUtils::parseBonus (ability["ability"].Vector());
a.second.first = static_cast<ui8>(ability["skills"].Vector()[0].Float());
a.second.second = static_cast<ui8>(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<std::string, JsonNode> 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<JsonNode> CCreatureHandler::loadLegacyData()
{
size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_CREATURE);
objects.resize(dataSize);
std::vector<JsonNode> 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; i<dataSize; i++)
{
//loop till non-empty line
while (parser.isNextEntryEmpty())
parser.endLine();
JsonNode data;
data["name"]["singular"].String() = parser.readString();
if (namesCount == 3)
parser.readString();
data["name"]["plural"].String() = parser.readString();
for(int v=0; v<7; ++v)
data["cost"][GameConstants::RESOURCE_NAMES[v]].Float() = parser.readNumber();
data["fightValue"].Float() = parser.readNumber();
data["aiValue"].Float() = parser.readNumber();
data["growth"].Float() = parser.readNumber();
data["horde"].Float() = parser.readNumber();
data["hitPoints"].Float() = parser.readNumber();
data["speed"].Float() = parser.readNumber();
data["attack"].Float() = parser.readNumber();
data["defense"].Float() = parser.readNumber();
data["damage"]["min"].Float() = parser.readNumber();
data["damage"]["max"].Float() = parser.readNumber();
if (float shots = parser.readNumber())
data["shots"].Float() = shots;
if (float spells = parser.readNumber())
data["spellPoints"].Float() = spells;
data["advMapAmount"]["min"].Float() = parser.readNumber();
data["advMapAmount"]["max"].Float() = parser.readNumber();
// unused - ability text, not used since we no longer have original creature window
parser.readString();
loadBonuses(data, parser.readString()); //Attributes
h3Data.push_back(data);
}
loadAnimationInfo(h3Data);
return h3Data;
}
CCreature * CCreatureHandler::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 * cre = new CCreature();
if(node["hasDoubleWeek"].Bool())
{
doubledCreatures.insert(CreatureID(index));
}
cre->idNumber = 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", [=](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())
VLC->objtypeh->removeSubObject(Obj::MONSTER, cre->getId().num);
});
return cre;
}
const std::vector<std::string> & CCreatureHandler::getTypeNames() const
{
static const std::vector<std::string> 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<ui32>(expBonParser.readNumber());
expRanks[i].push_back(expRanks[i].back() + static_cast<ui32>(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<Bonus> b) {
auto limiter = std::make_shared<CreatureLevelLimiter>();
b->addLimiter(limiter);
globalEffects.addNewBonus(b);
};
auto addBonusForTier = [&](int tier, std::shared_ptr<Bonus> 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<int>::max() : tier + 1;
auto limiter = std::make_shared<CreatureLevelLimiter>(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<JsonNode> &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<int>(offsets["upperX"].Float());
cre->animation.upperRightMissleOffsetY = static_cast<int>(offsets["upperY"].Float());
cre->animation.rightMissleOffsetX = static_cast<int>(offsets["middleX"].Float());
cre->animation.rightMissleOffsetY = static_cast<int>(offsets["middleY"].Float());
cre->animation.lowerRightMissleOffsetX = static_cast<int>(offsets["lowerX"].Float());
cre->animation.lowerRightMissleOffsetY = static_cast<int>(offsets["lowerY"].Float());
cre->animation.attackClimaxFrame = static_cast<int>(missile["attackClimaxFrame"].Float());
cre->animation.missleFrameAngles = missile["frameAngles"].convertTo<std::vector<double> >();
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();
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>(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<int>(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<si32>(parser.readNumber());
if (curVal == 1)
{
b.limiter.reset (new RankRangeLimiter(i));
bl.push_back(std::make_shared<Bonus>(b));
break; //never turned off it seems
}
}
}
else
{
lastVal = static_cast<si32>(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<si32>(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<Bonus>(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<CreatureID> allowed;
for(const auto & creature : objects)
{
if(creature->special)
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