Browse Source

Merge branch 'develop' into highscore_menu

Laserlicht 2 years ago
parent
commit
f3e1943aaf

+ 1 - 1
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);

+ 70 - 0
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": []
+		}
+	},
+}
+

+ 40 - 0
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",

+ 0 - 31
config/startres.json

@@ -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 }
-			}
-		]
-}
-

+ 67 - 0
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.

+ 1 - 0
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;

+ 2 - 0
lib/CPlayerState.h

@@ -37,6 +37,7 @@ public:
 	std::vector<ConstTransitivePtr<CGTownInstance> > towns;
 	std::vector<ConstTransitivePtr<CGDwelling> > dwellings; //used for town growth
 	std::vector<QuestInfo> quests; //store info about all received quests
+	std::vector<Bonus> 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<CBonusSystemNode&>(*this);

+ 3 - 4
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<JsonNode&>(handler.getCurrent()));
+
 	if(!handler.saving)
-		jsonDeserialize(attr);
+		jsonDeserialize(handler.getCurrent());
 }
 
 VCMI_LIB_NAMESPACE_END

+ 1 - 1
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

+ 2 - 0
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

+ 37 - 25
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

+ 1 - 1
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();

+ 10 - 0
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...
 	{

+ 1 - 1
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;

+ 39 - 23
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())
 			{

+ 5 - 0
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

+ 6 - 6
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};
 

+ 2 - 1
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<ArtifactID, ArtifactID> mappingArtifact;
 	std::map<SecondarySkill, SecondarySkill> mappingSecondarySkill;
 
-	std::map<std::string, std::string> mappingObjectTemplate;
+	std::map<AnimationPath, AnimationPath> mappingObjectTemplate;
 	std::map<ObjectTypeIdentifier, ObjectTypeIdentifier> mappingObjectIndex;
 
 	template<typename IdentifierID>

+ 18 - 3
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);
 }
 

+ 14 - 0
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<CBattleQuery>(gameHandler->queries->topQuery(battle->sides[0].color));