diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index bac669ae7..66a86592d 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -393,7 +393,7 @@ void ArmyManager::update() } } - for(auto army : totalArmy) + for(auto & army : totalArmy) { army.second.creature = army.first.toCreature(); army.second.power = evaluateStackPower(army.second.creature, army.second.count); diff --git a/config/difficulty.json b/config/difficulty.json new file mode 100644 index 000000000..2ee96a7ad --- /dev/null +++ b/config/difficulty.json @@ -0,0 +1,70 @@ +//Configured difficulty +{ + "human": + { + "pawn": + { + "resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "knight": + { + "resources": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "rook": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "queen": + { + "resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "king": + { + "resources": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + } + }, + "ai": + { + "pawn": + { + "resources": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "knight": + { + "resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "rook": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "queen": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "king": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + } + }, +} + diff --git a/config/mapOverrides.json b/config/mapOverrides.json index c275c9ee7..9a520fe7c 100644 --- a/config/mapOverrides.json +++ b/config/mapOverrides.json @@ -75,6 +75,46 @@ "victoryIconIndex" : 11, "victoryString" : "core.vcdesc.0" }, + "data/evil2:0" : { // A Gryphon's Heart + "defeatIconIndex" : 2, + "defeatString" : "core.lcdesc.3", + "triggeredEvents" : { + "specialDefeat" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "daysPassed", { "value" : 84 } ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "core.genrltxt.254" + }, + "specialVictory" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "transport", { "position" : [ 16, 23, 0 ], "type" : 84 } ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.293", + "type" : "victory" + }, + "message" : "core.genrltxt.292" + }, + "standardDefeat" : { + "condition" : [ "daysWithoutTown", { "value" : 7 } ], + "effect" : { + "messageToSend" : "core.genrltxt.8", + "type" : "defeat" + }, + "message" : "core.genrltxt.7" + } + }, + "victoryIconIndex" : 10, + "victoryString" : "core.vcdesc.11" + }, "data/secret1:0" : { // The Grail "defeatIconIndex" : 2, "defeatString" : "core.lcdesc.3", diff --git a/config/startres.json b/config/startres.json deleted file mode 100644 index dc96714e1..000000000 --- a/config/startres.json +++ /dev/null @@ -1,31 +0,0 @@ -// Starting resources, ordered by difficulty level (0 to 4) -{ - "difficulty": - [ - { - "human": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, - "ai": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 } - }, - - { - "human": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 }, - "ai": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 } - }, - - { - "human": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } - }, - - { - "human": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } - }, - - { - "human": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } - } - ] -} - diff --git a/docs/modders/Difficulty.md b/docs/modders/Difficulty.md new file mode 100644 index 000000000..75c44d768 --- /dev/null +++ b/docs/modders/Difficulty.md @@ -0,0 +1,67 @@ +< [Documentation](../Readme.md) / [Modding](Readme.md) / Difficulty + + +Since VCMI 1.4.0 there are more capabilities to configure difficulty parameters. +It means, that modders can give different bonuses to AI or human players depending on selected difficulty + +Difficulty configuration is located in [config/difficulty.json](../config/difficulty.json) file and can be overriden by mods. + +## Format summary + +``` javascript +{ + "human": //parameters impacting human players only + { + "pawn": //parameters for specific difficulty + { + //starting resources + "resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, + //bonuses will be given to player globaly + "globalBonuses": [], + //bonuses will be given to player every battle + "battleBonuses": [] + }, + "knight": {}, + "rook": {}, + "queen": {}, + "king": {}, + }, + "ai": //parameters impacting AI players only + { + "pawn": {}, //parameters for specific difficulty + "knight": {}, + "rook": {}, + "queen": {}, + "king": {}, + } +} +``` + +## Bonuses + +It's possible to specify bonuses of two types: `globalBonuses` and `battleBonuses`. + +Both are arrays containing any amount of bonuses, each can be described as usual bonus. See details in [bonus documenation](Bonus_Format.md). + +`globalBonuses` are given to player on the begining and depending on bonus configuration, it can behave diffierently. + +`battleBonuses` are given to player during the battles, but *only for battles with neutral forces*. So it won't be provided to player for PvP battles and battles versus AI heroes/castles/garrisons. To avoid cumulative effects or unexpected behavior it's recommended to specify bonus `duration` as `ONE_BATTLE`. + +For both types of bonuses, `source` should be specified as `OTHER`. + +## Example + +```js +{ //will give 150% extra health to all players' creatures if specified in "battleBonuses" array + "type" : "STACK_HEALTH", + "val" : 150, + "valueType" : "PERCENT_TO_ALL", + "duration" : "ONE_BATTLE", + "sourceType" : "OTHER" +}, +``` + +## Compatibility + +Starting from VCMI 1.4 `startres.json` is not available anymore and will be ignored if present in any mod. +Thus, `Resourceful AI` mod of version 1.2 won't work anymore. \ No newline at end of file diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index 8061901fa..b59de08fa 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -40,6 +40,7 @@ PlayerState::PlayerState(PlayerState && other) noexcept: std::swap(towns, other.towns); std::swap(dwellings, other.dwellings); std::swap(quests, other.quests); + std::swap(battleBonuses, other.battleBonuses); } PlayerState::~PlayerState() = default; diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index 560c3d636..66c70619a 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -37,6 +37,7 @@ public: std::vector > towns; std::vector > dwellings; //used for town growth std::vector quests; //store info about all received quests + std::vector battleBonuses; //additional bonuses to be added during battle with neutrals bool cheated; bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory @@ -84,6 +85,7 @@ public: h & status; h & daysWithoutCastle; h & cheated; + h & battleBonuses; h & enteredLosingCheatCode; h & enteredWinningCheatCode; h & static_cast(*this); diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index 86a1badfe..3c9eac22c 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -388,12 +388,11 @@ void MetaString::jsonDeserialize(const JsonNode & source) void MetaString::serializeJson(JsonSerializeFormat & handler) { - JsonNode attr; if(handler.saving) - jsonSerialize(attr); - handler.serializeRaw("attributes", attr, std::nullopt); + jsonSerialize(const_cast(handler.getCurrent())); + if(!handler.saving) - jsonDeserialize(attr); + jsonDeserialize(handler.getCurrent()); } VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index c8714652d..b8876b946 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1573,7 +1573,7 @@ int32_t CBattleInfoCallback::battleGetSpellCost(const spells::Spell * sp, const } } - return ret - manaReduction + manaIncrease; + return std::max(0, ret - manaReduction + manaIncrease); } bool CBattleInfoCallback::battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const diff --git a/lib/constants/StringConstants.h b/lib/constants/StringConstants.h index cf8d12a6a..d7b40fd92 100644 --- a/lib/constants/StringConstants.h +++ b/lib/constants/StringConstants.h @@ -27,6 +27,8 @@ namespace GameConstants }; const std::string ALIGNMENT_NAMES [3] = {"good", "evil", "neutral"}; + + const std::string DIFFICULTY_NAMES [5] = {"pawn", "knight", "rook", "queen", "king"}; } namespace NPrimarySkill diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index d51e36c66..f5657821c 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -29,6 +29,7 @@ #include "../VCMI_Lib.h" #include "../battle/BattleInfo.h" #include "../campaign/CampaignState.h" +#include "../constants/StringConstants.h" #include "../filesystem/ResourcePath.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" @@ -455,7 +456,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog initRandomFactionsForPlayers(); randomizeMapObjects(); placeStartingHeroes(); - initStartingResources(); + initDifficulty(); initHeroes(); initStartingBonus(); initTowns(); @@ -657,6 +658,41 @@ void CGameState::initGlobalBonuses() VLC->creh->loadCrExpBon(globalEffects); } +void CGameState::initDifficulty() +{ + logGlobal->debug("\tLoading difficulty settings"); + const JsonNode config = JsonUtils::assembleFromFiles("config/difficulty.json"); + + const JsonNode & difficultyAI(config["ai"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); + const JsonNode & difficultyHuman(config["human"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); + + auto setDifficulty = [](PlayerState & state, const JsonNode & json) + { + //set starting resources + state.resources = TResources(json["resources"]); + + //set global bonuses + for(auto & jsonBonus : json["globalBonuses"].Vector()) + if(auto bonus = JsonUtils::parseBonus(jsonBonus)) + state.addNewBonus(bonus); + + //set battle bonuses + for(auto & jsonBonus : json["battleBonuses"].Vector()) + if(auto bonus = JsonUtils::parseBonus(jsonBonus)) + state.battleBonuses.push_back(*bonus); + + }; + + for (auto & elem : players) + { + PlayerState &p = elem.second; + setDifficulty(p, p.human ? difficultyHuman : difficultyAI); + } + + if (campaign) + campaign->initStartingResources(); +} + void CGameState::initGrailPosition() { logGlobal->debug("\tPicking grail position"); @@ -813,30 +849,6 @@ void CGameState::removeHeroPlaceholders() } } -void CGameState::initStartingResources() -{ - logGlobal->debug("\tSetting up resources"); - const JsonNode config(JsonPath::builtin("config/startres.json")); - const JsonVector &vector = config["difficulty"].Vector(); - const JsonNode &level = vector[scenarioOps->difficulty]; - - TResources startresAI(level["ai"]); - TResources startresHuman(level["human"]); - - for (auto & elem : players) - { - PlayerState &p = elem.second; - - if (p.human) - p.resources = startresHuman; - else - p.resources = startresAI; - } - - if (campaign) - campaign->initStartingResources(); -} - void CGameState::initHeroes() { for(auto hero : map->heroesOnMap) //heroes instances initialization diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 63123b5d9..8ba75e2f9 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -192,7 +192,7 @@ private: void placeStartingHeroes(); void placeStartingHero(const PlayerColor & playerColor, const HeroTypeID & heroTypeId, int3 townPos); void removeHeroPlaceholders(); - void initStartingResources(); + void initDifficulty(); void initHeroes(); void placeHeroesInTowns(); void initFogOfWar(); diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 7da445fca..a2891c2cb 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -103,6 +103,16 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const void CGCreature::onHeroVisit( const CGHeroInstance * h ) const { + //show message + if(!message.empty()) + { + InfoWindow iw; + iw.player = h->tempOwner; + iw.text.appendRawString(message); + iw.type = EInfoWindowMode::MODAL; + cb->showInfoDialog(&iw); + } + int action = takenAction(h); switch( action ) //decide what we do... { diff --git a/lib/mapping/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index 4be3af4d4..c0a7d5f1f 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/lib/mapping/MapFeaturesH3M.cpp @@ -49,7 +49,7 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesROE() result.factionsCount = 8; result.heroesCount = 128; - result.heroesPortraitsCount = 128; + result.heroesPortraitsCount = 130; // +General Kendal, +Catherine (portrait-only in RoE) result.artifactsCount = 127; result.resourcesCount = 7; result.creaturesCount = 118; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 5c95fffae..6f4a0a38e 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -44,6 +44,7 @@ static std::string convertMapName(std::string input) { boost::algorithm::to_lower(input); boost::algorithm::trim(input); + boost::algorithm::erase_all(input, "."); size_t slashPos = input.find_last_of('/'); @@ -345,27 +346,11 @@ void CMapLoaderH3M::readVictoryLossConditions() bool allowNormalVictory = reader->readBool(); bool appliesToAI = reader->readBool(); - if(allowNormalVictory) - { - size_t playersOnMap = boost::range::count_if( - mapHeader->players, - [](const PlayerInfo & info) - { - return info.canAnyonePlay(); - } - ); - - if(playersOnMap == 1) - { - logGlobal->warn("Map %s: Only one player exists, but normal victory allowed!", mapName); - allowNormalVictory = false; // makes sense? Not much. Works as H3? Yes! - } - } - switch(vicCondition) { case EVictoryConditionType::ARTIFACT: { + assert(allowNormalVictory == true); // not selectable in editor EventCondition cond(EventCondition::HAVE_ARTIFACT); cond.objectType = reader->readArtifact(); @@ -404,6 +389,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::BUILDCITY: { + assert(appliesToAI == true); // not selectable in editor EventExpression::OperatorAll oper; EventCondition cond(EventCondition::HAVE_BUILDING); cond.position = reader->readInt3(); @@ -421,6 +407,8 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::BUILDGRAIL: { + assert(allowNormalVictory == true); // not selectable in editor + assert(appliesToAI == true); // not selectable in editor EventCondition cond(EventCondition::HAVE_BUILDING); cond.objectType = BuildingID::GRAIL; cond.position = reader->readInt3(); @@ -436,6 +424,10 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::BEATHERO: { + if (!allowNormalVictory) + logGlobal->warn("Map %s: Has 'beat hero' as victory condition, but 'allow normal victory' not set. Ignoring", mapName); + allowNormalVictory = true; // H3 behavior + assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DESTROY); cond.objectType = Obj::HERO; cond.position = reader->readInt3(); @@ -462,6 +454,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::BEATMONSTER: { + assert(appliesToAI == true); // not selectable in editor EventCondition cond(EventCondition::DESTROY); cond.objectType = Obj::MONSTER; cond.position = reader->readInt3(); @@ -500,6 +493,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::TRANSPORTITEM: { + assert(allowNormalVictory == true); // not selectable in editor EventCondition cond(EventCondition::TRANSPORT); cond.objectType = reader->readUInt8(); cond.position = reader->readInt3(); @@ -513,6 +507,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::HOTA_ELIMINATE_ALL_MONSTERS: { + assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DESTROY); cond.objectType = Obj::MONSTER; @@ -526,6 +521,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::HOTA_SURVIVE_FOR_DAYS: { + assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DAYS_PASSED); cond.value = reader->readUInt32(); @@ -541,6 +537,24 @@ void CMapLoaderH3M::readVictoryLossConditions() assert(0); } + if(allowNormalVictory) + { + size_t playersOnMap = boost::range::count_if( + mapHeader->players, + [](const PlayerInfo & info) + { + return info.canAnyonePlay(); + } + ); + + assert(playersOnMap > 1); + if(playersOnMap == 1) + { + logGlobal->warn("Map %s: Only one player exists, but normal victory allowed!", mapName); + allowNormalVictory = false; // makes sense? Not much. Works as H3? Yes! + } + } + // if condition is human-only turn it into following construction: AllOf(human, condition) if(!appliesToAI) { @@ -883,7 +897,7 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) if(!hero->artifactsWorn.empty() || !hero->artifactsInBackpack.empty()) { - logGlobal->warn("Hero %s at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getNameTranslated(), hero->pos.toString()); + logGlobal->debug("Hero %s at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getNameTranslated(), hero->pos.toString()); hero->artifactsInBackpack.clear(); while(!hero->artifactsWorn.empty()) @@ -1551,6 +1565,9 @@ void CMapLoaderH3M::readObjects() newObject->appearance = objectTemplate; assert(objectInstanceID == ObjectInstanceID((si32)map->objects.size())); + if (newObject->isVisitable() && !map->isInTheMap(newObject->visitablePos())) + logGlobal->error("Map '%s': Object at %s - outside of map borders!", mapName, mapPosition.toString()); + { //TODO: define valid typeName and subtypeName for H3M maps //boost::format fmt("%s_%d"); @@ -1703,7 +1720,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec if(!object->secSkills.empty()) { if(object->secSkills[0].first != SecondarySkill::DEFAULT) - logGlobal->warn("Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID); + logGlobal->debug("Map '%s': Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", mapName, object->getNameTextID(), object->subID); object->secSkills.clear(); } @@ -1775,7 +1792,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec auto ps = object->getAllBonuses(Selector::type()(BonusType::PRIMARY_SKILL).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)), nullptr); if(ps->size()) { - logGlobal->warn("Hero %s subID=%d has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTranslated(), object->subID ); + logGlobal->debug("Hero %s subID=%d has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTranslated(), object->subID ); for(const auto & b : *ps) object->removeBonus(b); } @@ -1907,8 +1924,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con auto rVal = reader->readUInt32(); assert(rId < features.resourcesCount); - assert((rVal & 0x00ffffff) == rVal); - + reward.resources[rId] = rVal; break; } @@ -2172,7 +2188,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt uint8_t alignment = reader->readUInt8(); - if(alignment != PlayerColor::NEUTRAL.getNum()) + if(alignment != 255) { if(alignment < PlayerColor::PLAYER_LIMIT.getNum()) { diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index c8aed2682..42c76415b 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -870,6 +870,11 @@ void CMapPatcher::readPatchData() { JsonDeserializer handler(mapObjectResolver.get(), input); readTriggeredEvents(handler); + + handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); + handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex); + handler.serializeStruct("victoryString", mapHeader->victoryMessage); + handler.serializeStruct("defeatString", mapHeader->defeatMessage); } ///CMapLoaderJson diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index 05c35020c..9935519d8 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -53,11 +53,11 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) for (auto entryTemplate : mapping["templates"].Struct()) { - std::string h3mName = boost::to_lower_copy(entryTemplate.second.String()); - std::string vcmiName = boost::to_lower_copy(entryTemplate.first); + AnimationPath h3mName = AnimationPath::builtinTODO(entryTemplate.second.String()); + AnimationPath vcmiName = AnimationPath::builtinTODO(entryTemplate.first); - if (!CResourceHandler::get()->existsResource(AnimationPath::builtinTODO("SPRITES/" + vcmiName))) - logMod->warn("Template animation file %s was not found!", vcmiName); + if (!CResourceHandler::get()->existsResource(vcmiName.addPrefix("SPRITES/"))) + logMod->warn("Template animation file %s was not found!", vcmiName.getOriginalName()); mappingObjectTemplate[h3mName] = vcmiName; } @@ -108,10 +108,10 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) void MapIdentifiersH3M::remapTemplate(ObjectTemplate & objectTemplate) { - std::string name = boost::to_lower_copy(objectTemplate.animationFile.getName()); + auto name = objectTemplate.animationFile; if (mappingObjectTemplate.count(name)) - objectTemplate.animationFile = AnimationPath::builtinTODO(mappingObjectTemplate.at(name)); + objectTemplate.animationFile = mappingObjectTemplate.at(name); ObjectTypeIdentifier objectType{ objectTemplate.id, objectTemplate.subid}; diff --git a/lib/mapping/MapIdentifiersH3M.h b/lib/mapping/MapIdentifiersH3M.h index 71cea67dc..581a80fda 100644 --- a/lib/mapping/MapIdentifiersH3M.h +++ b/lib/mapping/MapIdentifiersH3M.h @@ -11,6 +11,7 @@ #pragma once #include "../GameConstants.h" +#include "../filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -43,7 +44,7 @@ class MapIdentifiersH3M std::map mappingArtifact; std::map mappingSecondarySkill; - std::map mappingObjectTemplate; + std::map mappingObjectTemplate; std::map mappingObjectIndex; template diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index f7328b9de..306279104 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -113,7 +113,12 @@ int32_t MapReaderH3M::readHeroPortrait() if(result.getNum() == features.heroIdentifierInvalid) return int32_t(-1); - assert(result.getNum() < features.heroesPortraitsCount); + if (result.getNum() >= features.heroesPortraitsCount) + { + logGlobal->warn("Map contains invalid hero portrait ID %d. Will be reset!", result.getNum() ); + return int32_t(-1); + } + return remapper.remapPortrrait(result); } @@ -199,7 +204,12 @@ PlayerColor MapReaderH3M::readPlayer() if (value == 255) return PlayerColor::NEUTRAL; - assert(value < PlayerColor::PLAYER_LIMIT_I); + if (value >= PlayerColor::PLAYER_LIMIT_I) + { + logGlobal->warn("Map contains invalid player ID %d. Will be reset!", value ); + return PlayerColor::NEUTRAL; + } + return PlayerColor(value); } @@ -210,7 +220,12 @@ PlayerColor MapReaderH3M::readPlayer32() if (value == 255) return PlayerColor::NEUTRAL; - assert(value < PlayerColor::PLAYER_LIMIT_I); + if (value >= PlayerColor::PLAYER_LIMIT_I) + { + logGlobal->warn("Map contains invalid player ID %d. Will be reset!", value ); + return PlayerColor::NEUTRAL; + } + return PlayerColor(value); } diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index ea03a3433..9a443b0c9 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -24,6 +24,7 @@ #include "../../lib/gameState/CGameState.h" #include "../../lib/mapping/CMap.h" #include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/CPlayerState.h" BattleProcessor::BattleProcessor(CGameHandler * gameHandler) : gameHandler(gameHandler) @@ -113,6 +114,19 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm const auto * battle = gameHandler->gameState()->getBattle(battleID); assert(battle); + + //add battle bonuses based from player state only when attacks neutral creatures + const auto * attackerInfo = gameHandler->getPlayerState(army1->getOwner(), false); + if(attackerInfo && !army2->getOwner().isValidPlayer()) + { + for(auto bonus : attackerInfo->battleBonuses) + { + GiveBonus giveBonus(GiveBonus::ETarget::HERO); + giveBonus.id = hero1->id.getNum(); + giveBonus.bonus = bonus; + gameHandler->sendAndApply(&giveBonus); + } + } auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color));