mirror of
				https://github.com/vcmi/vcmi.git
				synced 2025-10-31 00:07:39 +02:00 
			
		
		
		
	Merge branch 'develop' into highscore_menu
This commit is contained in:
		| @@ -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
									
								
								config/difficulty.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								config/difficulty.json
									
									
									
									
									
										Normal file
									
								
							| @@ -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": [] | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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
									
								
								docs/modders/Difficulty.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								docs/modders/Difficulty.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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. | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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... | ||||
| 	{ | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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()) | ||||
| 			{ | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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}; | ||||
|  | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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)); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user