Browse Source

Win/loss conditions based on logical expressions, yet another large
changeset:
- victory/defeat will be detected using triggered events
- vcmi will convert h3 conditions into set of triggered events
- it is possible to either change number of days without towns or even
remove this loss condition completely
- possibility of custom win/loss text and icons in pregame (no longer
connected to win/loss conditions)

Still missing:
- No interface to pass custom events/victory conditions into the game
- AI would benefit from improvemets (handle all victory conditions,
select best one to fulfill)
- You have X days till defeat message still hardcoded to 7 days

Ivan Savenko 11 years ago
parent
commit
0c5be52a42

+ 115 - 61
AI/VCAI/Goals.cpp

@@ -135,86 +135,140 @@ namespace Goals
 
 TSubgoal Win::whatToDoToAchieve()
 {
-	const VictoryCondition &vc = cb->getMapHeader()->victoryCondition;
-	EVictoryConditionType::EVictoryConditionType cond = vc.condition;
+	auto toBool = [=](const EventCondition &)
+	{
+		// TODO: proper implementation
+		// Right now even already fulfilled goals will be included into generated list
+		// Proper check should test if event condition is already fulfilled
+		// Easiest way to do this is to call CGameState::checkForVictory but this function should not be
+		// used on client side or in AI code
+		return false;
+	};
 
-	if(!vc.appliesToAI)
+	std::vector<EventCondition> goals;
+
+	for (const TriggeredEvent & event : cb->getMapHeader()->triggeredEvents)
 	{
-		//TODO deduce victory from human loss condition
-		cond = EVictoryConditionType::WINSTANDARD;
+		//TODO: try to eliminate human player(s) using loss conditions that have isHuman element
+
+		if (event.effect.type == EventEffect::VICTORY)
+		{
+			boost::range::copy(event.trigger.getFulfillmentCandidates(toBool), std::back_inserter(goals));
+		}
 	}
 
-	switch(cond)
+	//TODO: instead of returning first encountered goal AI should generate list of possible subgoals
+	for (const EventCondition & goal : goals)
 	{
-	case EVictoryConditionType::ARTIFACT:
-		return sptr (Goals::GetArtOfType(vc.objectId));
-	case EVictoryConditionType::BEATHERO:
-		return sptr (Goals::GetObj(vc.obj->id.getNum()));
-	case EVictoryConditionType::BEATMONSTER:
-		return sptr (Goals::GetObj(vc.obj->id.getNum()));
-	case EVictoryConditionType::BUILDCITY:
-		//TODO build castle/capitol
-		break;
-	case EVictoryConditionType::BUILDGRAIL:
+		switch(goal.condition)
 		{
-			if(auto h = ai->getHeroWithGrail())
+		case EventCondition::HAVE_ARTIFACT:
+			return sptr (Goals::GetArtOfType(goal.objectType));
+		case EventCondition::DESTROY:
 			{
-				//hero is in a town that can host Grail
-				if(h->visitedTown && !vstd::contains(h->visitedTown->forbiddenBuildings, BuildingID::GRAIL))
+				if (goal.object)
 				{
-					const CGTownInstance *t = h->visitedTown;
-					return sptr (Goals::BuildThis(BuildingID::GRAIL, t));
+					return sptr (Goals::GetObj(goal.object->id.getNum()));
 				}
 				else
 				{
-					auto towns = cb->getTownsInfo();
-					towns.erase(boost::remove_if(towns,
-										[](const CGTownInstance *t) -> bool
-										{
-											return vstd::contains(t->forbiddenBuildings, BuildingID::GRAIL);
-										}),
-								towns.end());
-					boost::sort(towns, isCloser);
-					if(towns.size())
+					// TODO: destroy all objects of type goal.objectType
+					// This situation represents "kill all creatures" condition from H3
+					break;
+				}
+			}
+		case EventCondition::HAVE_BUILDING:
+			{
+			// TODO build other buildings apart from Grail
+			// goal.objectType = buidingID to build
+			// goal.object = optional, town in which building should be built
+			// Represents "Improve town" condition from H3 (but unlike H3 it consists from 2 separate conditions)
+
+				if (goal.objectType == BuildingID::GRAIL)
+				{
+					if(auto h = ai->getHeroWithGrail())
 					{
-						return sptr (Goals::VisitTile(towns.front()->visitablePos()).sethero(h));
+						//hero is in a town that can host Grail
+						if(h->visitedTown && !vstd::contains(h->visitedTown->forbiddenBuildings, BuildingID::GRAIL))
+						{
+							const CGTownInstance *t = h->visitedTown;
+							return sptr (Goals::BuildThis(BuildingID::GRAIL, t));
+						}
+						else
+						{
+							auto towns = cb->getTownsInfo();
+							towns.erase(boost::remove_if(towns,
+												[](const CGTownInstance *t) -> bool
+												{
+													return vstd::contains(t->forbiddenBuildings, BuildingID::GRAIL);
+												}),
+										towns.end());
+							boost::sort(towns, isCloser);
+							if(towns.size())
+							{
+								return sptr (Goals::VisitTile(towns.front()->visitablePos()).sethero(h));
+							}
+						}
 					}
+					double ratio = 0;
+					// maybe make this check a bit more complex? For example:
+					// 0.75 -> dig randomly withing 3 tiles radius
+					// 0.85 -> radius now 2 tiles
+					// 0.95 -> 1 tile radius, position is fully known
+					// AFAIK H3 AI does something like this
+					int3 grailPos = cb->getGrailPos(ratio);
+					if(ratio > 0.99)
+					{
+						return sptr (Goals::DigAtTile(grailPos));
+					} //TODO: use FIND_OBJ
+					else if(const CGObjectInstance * obj = ai->getUnvisitedObj(objWithID<Obj::OBELISK>)) //there are unvisited Obelisks
+						return sptr (Goals::GetObj(obj->id.getNum()));
+					else
+						return sptr (Goals::Explore());
 				}
+				break;
 			}
-			double ratio = 0;
-			int3 grailPos = cb->getGrailPos(ratio);
-			if(ratio > 0.99)
+		case EventCondition::CONTROL:
 			{
-				return sptr (Goals::DigAtTile(grailPos));
-			} //TODO: use FIND_OBJ
-			else if(const CGObjectInstance * obj = ai->getUnvisitedObj(objWithID<Obj::OBELISK>)) //there are unvisited Obelisks
+				if (goal.object)
+				{
+					return sptr (Goals::GetObj(goal.object->id.getNum()));
+				}
+				else
+				{
+					//TODO: control all objects of type "goal.objectType"
+					// Represents H3 condition "Flag all mines"
+					break;
+				}
+			}
+
+		case EventCondition::HAVE_RESOURCES:
+			//TODO mines? piles? marketplace?
+			//save?
+			return sptr (Goals::CollectRes(static_cast<Res::ERes>(goal.objectType), goal.value));
+		case EventCondition::HAVE_CREATURES:
+			return sptr (Goals::GatherTroops(goal.objectType, goal.value));
+		case EventCondition::TRANSPORT:
 			{
-				return sptr (Goals::GetObj(obj->id.getNum()));
+				//TODO. merge with bring Grail to town? So AI will first dig grail, then transport it using this goal and builds it
+				// Represents "transport artifact" condition:
+				// goal.objectType = type of artifact
+				// goal.object = destination-town where artifact should be transported
+				break;
 			}
-			else
-				return sptr (Goals::Explore());
+		case EventCondition::STANDARD_WIN:
+			return sptr (Goals::Conquer());
+
+		// Conditions that likely don't need any implementation
+		case EventCondition::DAYS_PASSED:
+			break; // goal.value = number of days for condition to trigger
+		case EventCondition::DAYS_WITHOUT_TOWN:
+			break; // goal.value = number of days to trigger this
+		case EventCondition::IS_HUMAN:
+			break; // Should be only used in calculation of candidates (see toBool lambda)
+		default:
+			assert(0);
 		}
-		break;
-	case EVictoryConditionType::CAPTURECITY:
-		return sptr (Goals::GetObj(vc.obj->id.getNum()));
-	case EVictoryConditionType::GATHERRESOURCE:
-        return sptr (Goals::CollectRes(static_cast<Res::ERes>(vc.objectId), vc.count));
-		//TODO mines? piles? marketplace?
-		//save?
-		break;
-	case EVictoryConditionType::GATHERTROOP:
-		return sptr (Goals::GatherTroops(vc.objectId, vc.count));
-		break;
-	case EVictoryConditionType::TAKEDWELLINGS:
-		break;
-	case EVictoryConditionType::TAKEMINES:
-		break;
-	case EVictoryConditionType::TRANSPORTITEM:
-		break;
-	case EVictoryConditionType::WINSTANDARD:
-		return sptr (Goals::Conquer());
-	default:
-		assert(0);
 	}
 	return sptr (Goals::Invalid());
 }

+ 5 - 13
client/CPlayerInterface.cpp

@@ -2107,7 +2107,8 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
 
 	if(player == playerID)
 	{
-		if(victoryLossCheckResult.loss()) showInfoDialog(CGI->generaltexth->allTexts[95]);
+		if(victoryLossCheckResult.loss())
+			showInfoDialog(CGI->generaltexth->allTexts[95]);
 
 		if(LOCPLINT == this)
 		{
@@ -2154,18 +2155,9 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
 	{
 		if(victoryLossCheckResult.loss() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME) //enemy has lost
 		{
-			std::string txt;
-			if(victoryLossCheckResult == EVictoryLossCheckResult::LOSS_STANDARD_TOWNS_AND_TIME_OVER)
-			{
-				txt = CGI->generaltexth->allTexts[8]; // %s's heroes have abandoned him, and he is banished from this land.
-			}
-			else
-			{
-				txt = CGI->generaltexth->allTexts[5]; // %s has been vanquished!
-			}
-
-			boost::algorithm::replace_first(txt, "%s", CGI->generaltexth->capColors[player.getNum()]);
-			showInfoDialog(txt,std::vector<CComponent*>(1, new CComponent(CComponent::flag, player.getNum(), 0)));
+			std::string str = victoryLossCheckResult.messageToSelf;
+			boost::algorithm::replace_first(str, "%s", CGI->generaltexth->capColors[player.getNum()]);
+			showInfoDialog(str, std::vector<CComponent*>(1, new CComponent(CComponent::flag, player.getNum(), 0)));
 		}
 	}
 }

+ 9 - 29
client/CPreGame.cpp

@@ -1437,18 +1437,10 @@ void SelectionTab::printMaps(SDL_Surface *to)
 			blitAtLoc(format->ourImages[temp].bitmap, 88, 117 + line * 25, to);
 
 			//victory conditions
-			if (currentItem->mapHeader->victoryCondition.condition == EVictoryConditionType::WINSTANDARD)
-				temp = 11;
-			else
-				temp = currentItem->mapHeader->victoryCondition.condition;
-			blitAtLoc(CGP->victory->ourImages[temp].bitmap, 306, 117 + line * 25, to);
+			blitAtLoc(CGP->victory->ourImages[currentItem->mapHeader->victoryIconIndex].bitmap, 306, 117 + line * 25, to);
 
 			//loss conditions
-			if (currentItem->mapHeader->lossCondition.typeOfLossCon == ELossConditionType::LOSSSTANDARD)
-				temp=3;
-			else
-				temp=currentItem->mapHeader->lossCondition.typeOfLossCon;
-			blitAtLoc(CGP->loss->ourImages[temp].bitmap, 339, 117 + line * 25, to);
+			blitAtLoc(CGP->loss->ourImages[currentItem->mapHeader->defeatIconIndex].bitmap, 339, 117 + line * 25, to);
 		}
 		else //if campaign
 		{
@@ -2023,26 +2015,14 @@ void InfoCard::showAll(SDL_Surface * to)
 				CDefHandler * loss    = CGP ? CGP->loss    : CDefHandler::giveDef("SCNRLOSS.DEF");
 				CDefHandler * victory = CGP ? CGP->victory : CDefHandler::giveDef("SCNRVICT.DEF");
 
+				CMapHeader * header = SEL->current->mapHeader.get();
 				//victory conditions
-				temp = SEL->current->mapHeader->victoryCondition.condition+1;
-				if (temp>20) temp=0;
-				std::string sss = CGI->generaltexth->victoryConditions[temp];
-				if (temp && SEL->current->mapHeader->victoryCondition.allowNormalVictory) sss+= "/" + CGI->generaltexth->victoryConditions[0];
-				printAtLoc(sss, 60, 307, FONT_SMALL, Colors::WHITE, to);
-
-				temp = SEL->current->mapHeader->victoryCondition.condition;
-				if (temp>12) temp=11;
-				blitAtLoc(victory->ourImages[temp].bitmap, 24, 302, to); //victory cond descr
+				printAtLoc(header->victoryMessage, 60, 307, FONT_SMALL, Colors::WHITE, to);
+				blitAtLoc(victory->ourImages[header->victoryIconIndex].bitmap, 24, 302, to); //victory cond descr
 
 				//loss conditoins
-				temp = SEL->current->mapHeader->lossCondition.typeOfLossCon+1;
-				if (temp>20) temp=0;
-				sss = CGI->generaltexth->lossCondtions[temp];
-				printAtLoc(sss, 60, 366, FONT_SMALL, Colors::WHITE, to);
-
-				temp=SEL->current->mapHeader->lossCondition.typeOfLossCon;
-				if (temp>12) temp=3;
-				blitAtLoc(loss->ourImages[temp].bitmap, 24, 359, to); //loss cond
+				printAtLoc(header->defeatMessage, 60, 366, FONT_SMALL, Colors::WHITE, to);
+				blitAtLoc(loss->ourImages[header->defeatIconIndex].bitmap, 24, 359, to); //loss cond
 
 				if (!CGP)
 				{
@@ -2987,7 +2967,7 @@ bool mapSorter::operator()(const CMapInfo *aaa, const CMapInfo *bbb)
 			return (a->version<b->version);
 			break;
 		case _loscon: //by loss conditions
-			return (a->lossCondition.typeOfLossCon<b->lossCondition.typeOfLossCon);
+			return (a->defeatMessage < b->defeatMessage);
 			break;
 		case _playerAm: //by player amount
 			int playerAmntB,humenPlayersB,playerAmntA,humenPlayersA;
@@ -3008,7 +2988,7 @@ bool mapSorter::operator()(const CMapInfo *aaa, const CMapInfo *bbb)
 			return (a->width<b->width);
 			break;
 		case _viccon: //by victory conditions
-			return (a->victoryCondition.condition < b->victoryCondition.condition);
+			return (a->victoryMessage < b->victoryMessage);
 			break;
 		case _name: //by name
 			return boost::ilexicographical_compare(a->name, b->name);

+ 139 - 170
lib/CGameState.cpp

@@ -2225,141 +2225,151 @@ bool CGameState::checkForVisitableDir( const int3 & src, const TerrainTile *pom,
 
 EVictoryLossCheckResult CGameState::checkForVictoryAndLoss(PlayerColor player) const
 {
-	auto result = checkForVictory(player);
-	if (result == EVictoryLossCheckResult::NO_VICTORY_OR_LOSS) result = checkForLoss(player);
-	return result;
-}
+	const std::string & messageWonSelf = VLC->generaltexth->allTexts[659];
+	const std::string & messageWonOther = VLC->generaltexth->allTexts[5];
+	const std::string & messageLostSelf = VLC->generaltexth->allTexts[7];
+	const std::string & messageLostOther = VLC->generaltexth->allTexts[8];
+
+	auto evaluateEvent = [=](const EventCondition & condition)
+	{
+		return this->checkForVictory(player, condition);
+	};
 
-EVictoryLossCheckResult CGameState::checkForVictory( PlayerColor player ) const
-{
 	const PlayerState *p = CGameInfoCallback::getPlayer(player);
-	if(map->victoryCondition.condition == EVictoryConditionType::WINSTANDARD  ||  map->victoryCondition.allowNormalVictory
-		|| (!p->human && !map->victoryCondition.appliesToAI)) //if the special victory condition applies only to human, AI has the standard)
+
+	//cheater or tester, but has entered the code...
+	if (p->enteredWinningCheatCode)
+		return EVictoryLossCheckResult::victory(messageWonSelf, messageWonOther);
+
+	if (p->enteredLosingCheatCode)
+		return EVictoryLossCheckResult::defeat(messageLostSelf, messageLostOther);
+
+	for (const TriggeredEvent & event : map->triggeredEvents)
 	{
-		if(player == checkForStandardWin())
-			return EVictoryLossCheckResult::VICTORY_STANDARD;
+		if ((event.trigger.test(evaluateEvent)))
+		{
+			if (event.effect.type == EventEffect::VICTORY)
+				return EVictoryLossCheckResult::victory(event.onFulfill, event.effect.toOtherMessage);
+
+			if (event.effect.type == EventEffect::DEFEAT)
+				return EVictoryLossCheckResult::defeat(event.onFulfill, event.effect.toOtherMessage);
+		}
 	}
 
-	if (p->enteredWinningCheatCode)
-	{ //cheater or tester, but has entered the code...
-		if(map->victoryCondition.condition == EVictoryConditionType::WINSTANDARD)
-			return EVictoryLossCheckResult::VICTORY_STANDARD;
-		else
-			return EVictoryLossCheckResult::VICTORY_SPECIAL;
+	if (checkForStandardLoss(player))
+	{
+		return EVictoryLossCheckResult::defeat(messageLostSelf, messageLostOther);
 	}
+	return EVictoryLossCheckResult();
+}
 
-	if(p->human || map->victoryCondition.appliesToAI)
+bool CGameState::checkForVictory( PlayerColor player, const EventCondition & condition ) const
+{
+	const PlayerState *p = CGameInfoCallback::getPlayer(player);
+	switch (condition.condition)
 	{
- 		switch(map->victoryCondition.condition)
+		case EventCondition::STANDARD_WIN:
+		{
+			return player == checkForStandardWin();
+		}
+		case EventCondition::HAVE_ARTIFACT: //check if any hero has winning artifact
 		{
-		case EVictoryConditionType::ARTIFACT:
-			//check if any hero has winning artifact
 			for(auto & elem : p->heroes)
-                if(elem->hasArt(map->victoryCondition.objectId))
-					return EVictoryLossCheckResult::VICTORY_SPECIAL;
-
-			break;
-
-		case EVictoryConditionType::GATHERTROOP:
+				if(elem->hasArt(condition.objectType))
+					return true;
+			return false;
+		}
+		case EventCondition::HAVE_CREATURES:
+		{
+			//check if in players armies there is enough creatures
+			int total = 0; //creature counter
+			for(size_t i = 0; i < map->objects.size(); i++)
 			{
-				//check if in players armies there is enough creatures
-				int total = 0; //creature counter
-				for(size_t i = 0; i < map->objects.size(); i++)
+				const CArmedInstance *ai = nullptr;
+				if(map->objects[i]
+					&& map->objects[i]->tempOwner == player //object controlled by player
+					&&  (ai = dynamic_cast<const CArmedInstance*>(map->objects[i].get()))) //contains army
 				{
-					const CArmedInstance *ai = nullptr;
-					if(map->objects[i]
-						&& map->objects[i]->tempOwner == player //object controlled by player
-						&&  (ai = dynamic_cast<const CArmedInstance*>(map->objects[i].get()))) //contains army
-					{
-						for(auto & elem : ai->Slots()) //iterate through army
-                            if(elem.second->type->idNumber == map->victoryCondition.objectId) //it's searched creature
-								total += elem.second->count;
-					}
+					for(auto & elem : ai->Slots()) //iterate through army
+						if(elem.second->type->idNumber == condition.objectType) //it's searched creature
+							total += elem.second->count;
 				}
-
-				if(total >= map->victoryCondition.count)
-					return EVictoryLossCheckResult::VICTORY_SPECIAL;
-			}
-			break;
-
-		case EVictoryConditionType::GATHERRESOURCE:
-            if(p->resources[map->victoryCondition.objectId] >= map->victoryCondition.count)
-				return EVictoryLossCheckResult::VICTORY_SPECIAL;
-
-			break;
-
-		case EVictoryConditionType::BUILDCITY:
-			{
-				const CGTownInstance *t = static_cast<const CGTownInstance *>(map->victoryCondition.obj);
-                if(t->tempOwner == player && t->fortLevel()-1 >= map->victoryCondition.objectId && t->hallLevel()-1 >= map->victoryCondition.count)
-					return EVictoryLossCheckResult::VICTORY_SPECIAL;
 			}
-			break;
-
-		case EVictoryConditionType::BUILDGRAIL:
-			for(const CGTownInstance *t : map->towns)
-				if((t == map->victoryCondition.obj || !map->victoryCondition.obj)
-					&& t->tempOwner == player
-					&& t->hasBuilt(BuildingID::GRAIL))
-					return EVictoryLossCheckResult::VICTORY_SPECIAL;
-			break;
-
-		case EVictoryConditionType::BEATHERO:
-			if(map->victoryCondition.obj->tempOwner >= PlayerColor::PLAYER_LIMIT) //target hero not present on map
-				return EVictoryLossCheckResult::VICTORY_SPECIAL;
-			break;
-		case EVictoryConditionType::CAPTURECITY:
+			return total >= condition.value;
+		}
+		case EventCondition::HAVE_RESOURCES:
+		{
+			return p->resources[condition.objectType] >= condition.value;
+		}
+		case EventCondition::HAVE_BUILDING:
+		{
+			const CGTownInstance *t = static_cast<const CGTownInstance *>(condition.object);
+			return (t->tempOwner == player && t->hasBuilt(BuildingID(condition.objectType)));
+		}
+		case EventCondition::DESTROY:
+		{
+			if (condition.object) // mode A - destroy specific object of this type
 			{
-				if(map->victoryCondition.obj->tempOwner == player)
-					return EVictoryLossCheckResult::VICTORY_SPECIAL;
+				if (auto hero = dynamic_cast<const CGHeroInstance*>(condition.object))
+					return boost::range::find(gs->map->heroesOnMap, hero) == gs->map->heroesOnMap.end();
+				else
+					return getObj(condition.object->id) == nullptr;
 			}
-			break;
-		case EVictoryConditionType::BEATMONSTER:
-			if(!getObj(map->victoryCondition.obj->id)) //target monster not present on map
-				return EVictoryLossCheckResult::VICTORY_SPECIAL;
-			break;
-		case EVictoryConditionType::TAKEDWELLINGS:
-			for(auto & elem : map->objects)
+			else
 			{
-				if(elem && elem->tempOwner != player) //check not flagged objs
+				for(auto & elem : map->objects) // mode B - destroy all objects of this type
 				{
-					switch(elem->ID)
-					{
-					case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR2:
-					case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR4:
-					case Obj::RANDOM_DWELLING: case Obj::RANDOM_DWELLING_LVL: case Obj::RANDOM_DWELLING_FACTION:
-						return EVictoryLossCheckResult::NO_VICTORY_OR_LOSS; //found not flagged dwelling - player not won
-					}
+					if(elem && elem->ID == condition.objectType)
+						return false;
 				}
+				return true;
 			}
-			return EVictoryLossCheckResult::VICTORY_SPECIAL;
-		case EVictoryConditionType::TAKEMINES:
-			for(auto & elem : map->objects)
+		}
+		case EventCondition::CONTROL:
+		{
+			if (condition.object) // mode A - flag one specific object, like town
 			{
-				if(elem && elem->tempOwner != player) //check not flagged objs
-				{
-					switch(elem->ID)
-					{
-					case Obj::MINE: case Obj::ABANDONED_MINE:
-						return EVictoryLossCheckResult::NO_VICTORY_OR_LOSS; //found not flagged mine - player not won
-					}
-				}
+				return condition.object->tempOwner == player;
 			}
-			return EVictoryLossCheckResult::VICTORY_SPECIAL;
-		case EVictoryConditionType::TRANSPORTITEM:
+			else
 			{
-				const CGTownInstance *t = static_cast<const CGTownInstance *>(map->victoryCondition.obj);
-                if((t->visitingHero && t->visitingHero->hasArt(map->victoryCondition.objectId))
-                    || (t->garrisonHero && t->garrisonHero->hasArt(map->victoryCondition.objectId)))
+				for(auto & elem : map->objects) // mode B - flag all objects of this type
 				{
-					return EVictoryLossCheckResult::VICTORY_SPECIAL;
+					 //check not flagged objs
+					if(elem && elem->tempOwner != player && elem->ID == condition.objectType)
+						return false;
 				}
+				return true;
 			}
-			break;
- 		}
+		}
+		case EventCondition::TRANSPORT:
+		{
+			const CGTownInstance *t = static_cast<const CGTownInstance *>(condition.object);
+			if((t->visitingHero && t->visitingHero->hasArt(condition.objectType))
+				|| (t->garrisonHero && t->garrisonHero->hasArt(condition.objectType)))
+			{
+				return true;
+			}
+			return false;
+		}
+		case EventCondition::DAYS_PASSED:
+		{
+			return gs->day > condition.value;
+		}
+		case EventCondition::IS_HUMAN:
+		{
+			return p->human ? condition.value == 1 : condition.value == 0;
+		}
+		case EventCondition::DAYS_WITHOUT_TOWN:
+		{
+			if (p->daysWithoutCastle)
+				return p->daysWithoutCastle.get() >= condition.value;
+			else
+				return false;
+		}
 	}
-
-	return EVictoryLossCheckResult::NO_VICTORY_OR_LOSS;
+	assert(0);
+	return false;
 }
 
 PlayerColor CGameState::checkForStandardWin() const
@@ -2597,46 +2607,6 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
 #undef FILL_FIELD
 }
 
-EVictoryLossCheckResult CGameState::checkForLoss( PlayerColor player ) const
-{
-	const PlayerState *p = CGameInfoCallback::getPlayer(player);
-	//if(map->lossCondition.typeOfLossCon == lossStandard)
-		if(checkForStandardLoss(player))
-			return EVictoryLossCheckResult::LOSS_STANDARD_HEROES_AND_TOWNS;
-
-	if (p->enteredLosingCheatCode)
-	{
-		return EVictoryLossCheckResult::LOSS_SPECIAL;
-	}
-
-	if(p->human) //special loss condition applies only to human player
-	{
-		switch(map->lossCondition.typeOfLossCon)
-		{
-		case ELossConditionType::LOSSCASTLE:
-		case ELossConditionType::LOSSHERO:
-			{
-				const CGObjectInstance *obj = map->lossCondition.obj;
-				assert(obj);
-				if(obj->tempOwner != player)
-					return EVictoryLossCheckResult::LOSS_SPECIAL;
-			}
-			break;
-		case ELossConditionType::TIMEEXPIRES:
-			if(map->lossCondition.timeLimit < day)
-				return EVictoryLossCheckResult::LOSS_SPECIAL;
-			break;
-		}
-	}
-
-	if(p->towns.empty() && p->daysWithoutCastle && *p->daysWithoutCastle >= 6 && currentPlayer != player)
-	{
-		return EVictoryLossCheckResult::LOSS_STANDARD_TOWNS_AND_TIME_OVER;
-	}
-
-	return EVictoryLossCheckResult::NO_VICTORY_OR_LOSS;
-}
-
 std::map<ui32, ConstTransitivePtr<CGHeroInstance> > CGameState::unusedHeroesFromPool()
 {
 	std::map<ui32, ConstTransitivePtr<CGHeroInstance> > pool = hpool.heroesPool;
@@ -3479,21 +3449,16 @@ CPathfinder::CPathfinder(CPathsInfo &_out, CGameState *_gs, const CGHeroInstance
 	allowEmbarkAndDisembark = true;
 }
 
-const EVictoryLossCheckResult EVictoryLossCheckResult::NO_VICTORY_OR_LOSS = EVictoryLossCheckResult(0);
-const EVictoryLossCheckResult EVictoryLossCheckResult::VICTORY_STANDARD = EVictoryLossCheckResult(1);
-const EVictoryLossCheckResult EVictoryLossCheckResult::VICTORY_SPECIAL = EVictoryLossCheckResult(2);
-const EVictoryLossCheckResult EVictoryLossCheckResult::LOSS_STANDARD_HEROES_AND_TOWNS = EVictoryLossCheckResult(3);
-const EVictoryLossCheckResult EVictoryLossCheckResult::LOSS_STANDARD_TOWNS_AND_TIME_OVER = EVictoryLossCheckResult(4);
-const EVictoryLossCheckResult EVictoryLossCheckResult::LOSS_SPECIAL = EVictoryLossCheckResult(5);
-
-EVictoryLossCheckResult::EVictoryLossCheckResult() : intValue(0)
+EVictoryLossCheckResult::EVictoryLossCheckResult() :
+	intValue(0)
 {
-
 }
 
-EVictoryLossCheckResult::EVictoryLossCheckResult(si32 intValue) : intValue(intValue)
+EVictoryLossCheckResult::EVictoryLossCheckResult(si32 intValue, std::string toSelf, std::string toOthers) :
+	messageToSelf(toSelf),
+	messageToOthers(toOthers),
+	intValue(intValue)
 {
-
 }
 
 bool EVictoryLossCheckResult::operator==(EVictoryLossCheckResult const & other) const
@@ -3508,27 +3473,31 @@ bool EVictoryLossCheckResult::operator!=(EVictoryLossCheckResult const & other)
 
 bool EVictoryLossCheckResult::victory() const
 {
-	return *this == VICTORY_STANDARD || *this == VICTORY_SPECIAL;
+	return intValue == VICTORY;
 }
 
 bool EVictoryLossCheckResult::loss() const
 {
-	return !victory();
+	return intValue == DEFEAT;
+}
+
+EVictoryLossCheckResult EVictoryLossCheckResult::invert()
+{
+	return EVictoryLossCheckResult(-intValue, messageToOthers, messageToSelf);
+}
+
+EVictoryLossCheckResult EVictoryLossCheckResult::victory(std::string toSelf, std::string toOthers)
+{
+	return EVictoryLossCheckResult(VICTORY, toSelf, toOthers);
 }
 
-std::string EVictoryLossCheckResult::toString() const
+EVictoryLossCheckResult EVictoryLossCheckResult::defeat(std::string toSelf, std::string toOthers)
 {
-	if(*this == EVictoryLossCheckResult::NO_VICTORY_OR_LOSS) return "No victory or loss";
-	else if(*this == EVictoryLossCheckResult::VICTORY_STANDARD) return "Victory standard";
-	else if(*this == EVictoryLossCheckResult::VICTORY_SPECIAL) return "Victory special";
-	else if(*this == EVictoryLossCheckResult::LOSS_STANDARD_HEROES_AND_TOWNS) return "Loss standard heroes and towns";
-	else if(*this == EVictoryLossCheckResult::LOSS_STANDARD_TOWNS_AND_TIME_OVER) return "Loss standard towns and time over";
-	else if(*this == EVictoryLossCheckResult::LOSS_SPECIAL) return "Loss special";
-	else return "Unknown type";
+	return EVictoryLossCheckResult(DEFEAT, toSelf, toOthers);
 }
 
 std::ostream & operator<<(std::ostream & os, const EVictoryLossCheckResult & victoryLossCheckResult)
 {
-	os << victoryLossCheckResult.toString();
+	os << victoryLossCheckResult.messageToSelf;
 	return os;
 }

+ 23 - 18
lib/CGameState.h

@@ -357,28 +357,34 @@ struct BattleInfo;
 class DLL_LINKAGE EVictoryLossCheckResult
 {
 public:
-	static const EVictoryLossCheckResult NO_VICTORY_OR_LOSS;
-	static const EVictoryLossCheckResult VICTORY_STANDARD;
-	static const EVictoryLossCheckResult VICTORY_SPECIAL;
-	static const EVictoryLossCheckResult LOSS_STANDARD_HEROES_AND_TOWNS;
-	static const EVictoryLossCheckResult LOSS_STANDARD_TOWNS_AND_TIME_OVER;
-	static const EVictoryLossCheckResult LOSS_SPECIAL;
+	static EVictoryLossCheckResult victory(std::string toSelf, std::string toOthers);
+	static EVictoryLossCheckResult defeat(std::string toSelf, std::string toOthers);
 
 	EVictoryLossCheckResult();
 	bool operator==(EVictoryLossCheckResult const & other) const;
 	bool operator!=(EVictoryLossCheckResult const & other) const;
 	bool victory() const;
 	bool loss() const;
-	std::string toString() const;
+
+	EVictoryLossCheckResult invert();
+
+	std::string messageToSelf;
+	std::string messageToOthers;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
-		h & intValue;
+		h & intValue & messageToSelf & messageToOthers;
 	}
-
 private:
-	EVictoryLossCheckResult(si32 intValue);
-	si32 intValue;
+	enum EResult
+	{
+		DEFEAT = -1,
+		INGAME =  0,
+		VICTORY= +1
+	};
+
+	EVictoryLossCheckResult(si32 intValue, std::string toSelf, std::string toOthers);
+	si32 intValue; // uses EResult
 };
 
 DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EVictoryLossCheckResult & victoryLossCheckResult);
@@ -426,7 +432,13 @@ public:
 	void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out, int3 src = int3(-1,-1,-1), int movement = -1); //calculates possible paths for hero, by default uses current hero position and movement left; returns pointer to newly allocated CPath or nullptr if path does not exists
 	int3 guardingCreaturePosition (int3 pos) const;
 	std::vector<CGObjectInstance*> guardingCreatures (int3 pos) const;
+
+	// ----- victory, loss condition checks -----
+
 	EVictoryLossCheckResult checkForVictoryAndLoss(PlayerColor player) const;
+	bool checkForVictory(PlayerColor player, const EventCondition & condition) const; //checks if given player is winner
+	PlayerColor checkForStandardWin() const; //returns color of player that accomplished standard victory conditions or 255 (NEUTRAL) if no winner
+	bool checkForStandardLoss(PlayerColor player) const; //checks if given player lost the game
 
 	void obtainPlayersStats(SThievesGuildInfo & tgi, int level); //fills tgi with info about other players that is available at given level of thieves' guild
 	std::map<ui32, ConstTransitivePtr<CGHeroInstance> > unusedHeroesFromPool(); //heroes pool without heroes that are available in taverns
@@ -476,13 +488,6 @@ private:
 	void initMapObjects();
 	void initVisitingAndGarrisonedHeroes();
 
-	// ----- victory, loss condition checks -----
-
-	EVictoryLossCheckResult checkForVictory(PlayerColor player) const; //checks if given player is winner
-	EVictoryLossCheckResult checkForLoss(PlayerColor player) const; //checks if given player is loser
-	PlayerColor checkForStandardWin() const; //returns color of player that accomplished standard victory conditions or 255 (NEUTRAL) if no winner
-	bool checkForStandardLoss(PlayerColor player) const; //checks if given player lost the game
-
 	// ----- bonus system handling -----
 
 	void buildBonusSystemTree();

+ 2 - 0
lib/CModHandler.cpp

@@ -3,6 +3,8 @@
 #include "CDefObjInfoHandler.h"
 #include "JsonNode.h"
 #include "filesystem/Filesystem.h"
+#include "filesystem/AdapterLoaders.h"
+#include "filesystem/CFilesystemLoader.h"
 
 #include "CCreatureHandler.h"
 #include "CArtHandler.h"

+ 0 - 11
lib/GameConstants.h

@@ -290,17 +290,6 @@ public:
 
 ID_LIKE_OPERATORS_DECLS(SecondarySkill, SecondarySkill::ESecondarySkill)
 
-namespace EVictoryConditionType
-{
-	enum EVictoryConditionType { ARTIFACT, GATHERTROOP, GATHERRESOURCE, BUILDCITY, BUILDGRAIL, BEATHERO,
-		CAPTURECITY, BEATMONSTER, TAKEDWELLINGS, TAKEMINES, TRANSPORTITEM, WINSTANDARD = 255 };
-}
-
-namespace ELossConditionType
-{
-	enum ELossConditionType { LOSSCASTLE, LOSSHERO, TIMEEXPIRES, LOSSSTANDARD = 255 };
-}
-
 namespace EAlignment
 {
 	enum EAlignment { GOOD, EVIL, NEUTRAL };

+ 51 - 1
lib/LogicalExpression.h

@@ -9,6 +9,7 @@ namespace LogicalExpressionDetail
 	template<typename ContainedClass>
 	class ExpressionBase
 	{
+	public:
 		/// Possible logical operations, mostly needed to create different types for boost::variant
 		enum EOperations
 		{
@@ -16,7 +17,6 @@ namespace LogicalExpressionDetail
 			ALL_OF,
 			NONE_OF
 		};
-	public:
 		template<EOperations tag> class Element;
 
 		typedef Element<ANY_OF> OperatorAny;
@@ -145,6 +145,44 @@ namespace LogicalExpressionDetail
 		}
 	};
 
+	/// Simple foreach visitor
+	template <typename ContainedClass>
+	class ForEachVisitor : public boost::static_visitor<void>
+	{
+		typedef ExpressionBase<ContainedClass> Base;
+
+		std::function<void(typename Base::Value &)> visitor;
+
+	public:
+		ForEachVisitor(std::function<void(typename Base::Value &)> visitor):
+			visitor(visitor)
+		{}
+
+		//FIXME: duplicated code
+		void operator()(typename Base::OperatorAny & element) const
+		{
+			for (auto & entry : element.expressions)
+				boost::apply_visitor(*this, entry);
+		}
+
+		void operator()(typename Base::OperatorAll & element) const
+		{
+			for (auto & entry : element.expressions)
+				boost::apply_visitor(*this, entry);
+		}
+
+		void operator()(typename Base::OperatorNone & element) const
+		{
+			for (auto & entry : element.expressions)
+				boost::apply_visitor(*this, entry);
+		}
+
+		void operator()(typename Base::Value & element) const
+		{
+			visitor(element);
+		}
+	};
+
 	/// Json parser for expressions
 	template <typename ContainedClass>
 	class Reader
@@ -289,6 +327,18 @@ public:
 		std::swap(data, expr.data);
 	}
 
+	Variant get()
+	{
+		return data;
+	}
+
+	/// Simple visitor that visits all entries in expression
+	void forEach(std::function<void(Value &)> visitor)
+	{
+		LogicalExpressionDetail::ForEachVisitor<Value> testVisitor(visitor);
+		boost::apply_visitor(testVisitor, data);
+	}
+
 	/// calculates if expression evaluates to "true".
 	/// Note: empty expressions always return true
 	bool test(std::function<bool(const Value &)> toBool) const

+ 2 - 1
lib/NetPacksLib.cpp

@@ -1612,7 +1612,8 @@ DLL_LINKAGE void YourTurn::applyGs( CGameState *gs )
 	auto & playerState = gs->players[player];
 	if(playerState.towns.empty())
 	{
-		if(playerState.daysWithoutCastle) ++(*playerState.daysWithoutCastle);
+		if(playerState.daysWithoutCastle)
+			++(*playerState.daysWithoutCastle);
 		else playerState.daysWithoutCastle = 0;
 	}
 	else

+ 8 - 8
lib/filesystem/AdapterLoaders.cpp

@@ -46,18 +46,18 @@ std::unordered_set<ResourceID> CMappedFileLoader::getFilteredFiles(std::function
 
 CFilesystemList::CFilesystemList()
 {
-	loaders = new std::vector<std::unique_ptr<ISimpleResourceLoader> >;
+	//loaders = new std::vector<std::unique_ptr<ISimpleResourceLoader> >;
 }
 
 CFilesystemList::~CFilesystemList()
 {
-	delete loaders;
+	//delete loaders;
 }
 
 std::unique_ptr<CInputStream> CFilesystemList::load(const ResourceID & resourceName) const
 {
 	// load resource from last loader that have it (last overriden version)
-	for (auto & loader : boost::adaptors::reverse(*loaders))
+	for (auto & loader : boost::adaptors::reverse(loaders))
 	{
 		if (loader->existsResource(resourceName))
 			return loader->load(resourceName);
@@ -69,7 +69,7 @@ std::unique_ptr<CInputStream> CFilesystemList::load(const ResourceID & resourceN
 
 bool CFilesystemList::existsResource(const ResourceID & resourceName) const
 {
-	for (auto & loader : *loaders)
+	for (auto & loader : loaders)
 		if (loader->existsResource(resourceName))
 			return true;
 	return false;
@@ -91,7 +91,7 @@ std::unordered_set<ResourceID> CFilesystemList::getFilteredFiles(std::function<b
 {
 	std::unordered_set<ResourceID> ret;
 
-	for (auto & loader : *loaders)
+	for (auto & loader : loaders)
 		for (auto & entry : loader->getFilteredFiles(filter))
 			ret.insert(entry);
 
@@ -101,7 +101,7 @@ std::unordered_set<ResourceID> CFilesystemList::getFilteredFiles(std::function<b
 bool CFilesystemList::createResource(std::string filename, bool update)
 {
 	logGlobal->traceStream()<< "Creating " << filename;
-	for (auto & loader : boost::adaptors::reverse(*loaders))
+	for (auto & loader : boost::adaptors::reverse(loaders))
 	{
 		if (writeableLoaders.count(loader.get()) != 0                       // writeable,
 			&& loader->createResource(filename, update))          // successfully created
@@ -123,7 +123,7 @@ std::vector<const ISimpleResourceLoader *> CFilesystemList::getResourcesWithName
 {
 	std::vector<const ISimpleResourceLoader *> ret;
 
-	for (auto & loader : *loaders)
+	for (auto & loader : loaders)
 		boost::range::copy(loader->getResourcesWithName(resourceName), std::back_inserter(ret));
 
 	return ret;
@@ -131,7 +131,7 @@ std::vector<const ISimpleResourceLoader *> CFilesystemList::getResourcesWithName
 
 void CFilesystemList::addLoader(ISimpleResourceLoader * loader, bool writeable)
 {
-	loaders->push_back(std::unique_ptr<ISimpleResourceLoader>(loader));
+	loaders.push_back(std::unique_ptr<ISimpleResourceLoader>(loader));
 	if (writeable)
 		writeableLoaders.insert(loader);
 }

+ 1 - 1
lib/filesystem/AdapterLoaders.h

@@ -54,7 +54,7 @@ private:
 
 class DLL_LINKAGE CFilesystemList : public ISimpleResourceLoader
 {
-	std::vector<std::unique_ptr<ISimpleResourceLoader> >* loaders;
+	std::vector<std::unique_ptr<ISimpleResourceLoader> > loaders;
 
 	std::set<ISimpleResourceLoader *> writeableLoaders;
 

+ 104 - 25
lib/mapping/CMap.cpp

@@ -3,9 +3,11 @@
 
 #include "../CArtHandler.h"
 #include "../VCMI_Lib.h"
+#include "../CCreatureHandler.h"
 #include "../CTownHandler.h"
 #include "../CHeroHandler.h"
 #include "../CDefObjInfoHandler.h"
+#include "../CGeneralTextHandler.h"
 #include "../CSpellHandler.h"
 #include "CMapEditManager.h"
 
@@ -59,17 +61,13 @@ bool PlayerInfo::hasCustomMainHero() const
 	return !mainCustomHeroName.empty() && mainCustomHeroPortrait != -1;
 }
 
-LossCondition::LossCondition() : typeOfLossCon(ELossConditionType::LOSSSTANDARD),
-	pos(int3(-1, -1, -1)), timeLimit(-1), obj(nullptr)
+EventCondition::EventCondition(EWinLoseType condition):
+	object(nullptr),
+	value(-1),
+	objectType(-1),
+	position(-1, -1, -1),
+	condition(condition)
 {
-
-}
-
-VictoryCondition::VictoryCondition() : condition(EVictoryConditionType::WINSTANDARD),
-	allowNormalVictory(false), appliesToAI(false), pos(int3(-1, -1, -1)), objectId(0),
-	count(0), obj(nullptr)
-{
-
 }
 
 DisposedHero::DisposedHero() : heroId(0), portrait(255), players(0)
@@ -146,9 +144,44 @@ const int CMapHeader::MAP_SIZE_MIDDLE = 72;
 const int CMapHeader::MAP_SIZE_LARGE = 108;
 const int CMapHeader::MAP_SIZE_XLARGE = 144;
 
+void CMapHeader::setupEvents()
+{
+	EventCondition victoryCondition(EventCondition::STANDARD_WIN);
+	EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN);
+	defeatCondition.value = 7;
+
+	//Victory condition - defeat all
+	TriggeredEvent standardVictory;
+	standardVictory.effect.type = EventEffect::VICTORY;
+	standardVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[5];
+	standardVictory.identifier = "standardVictory";
+	standardVictory.description = ""; // TODO: display in quest window
+	standardVictory.onFulfill = VLC->generaltexth->allTexts[659];
+	standardVictory.trigger = EventExpression(victoryCondition);
+
+	//Loss condition - 7 days without town
+	TriggeredEvent standardDefeat;
+	standardDefeat.effect.type = EventEffect::DEFEAT;
+	standardDefeat.effect.toOtherMessage = VLC->generaltexth->allTexts[8];
+	standardDefeat.identifier = "standardDefeat";
+	standardDefeat.description = ""; // TODO: display in quest window
+	standardDefeat.onFulfill = VLC->generaltexth->allTexts[7];
+	standardDefeat.trigger = EventExpression(defeatCondition);
+
+	triggeredEvents.push_back(standardVictory);
+	triggeredEvents.push_back(standardDefeat);
+
+	victoryIconIndex = 11;
+	victoryMessage = VLC->generaltexth->victoryConditions[0];
+
+	defeatIconIndex = 3;
+	defeatMessage = VLC->generaltexth->lossCondtions[0];
+}
+
 CMapHeader::CMapHeader() : version(EMapFormat::SOD), height(72), width(72),
 	twoLevel(true), difficulty(1), levelLimit(0), howManyTeams(0), areAnyPlayers(false)
 {
+	setupEvents();
 	allowedHeroes = VLC->heroh->getDefaultAllowed();
 	players.resize(PlayerColor::PLAYER_LIMIT_I);
 }
@@ -272,29 +305,75 @@ bool CMap::isWaterTile(const int3 &pos) const
 	return isInTheMap(pos) && getTile(pos).terType == ETerrainType::WATER;
 }
 
-const CGObjectInstance * CMap::getObjectiveObjectFrom(int3 pos, bool lookForHero)
+const CGObjectInstance * CMap::getObjectiveObjectFrom(int3 pos, Obj::EObj type)
 {
-	const std::vector<CGObjectInstance *> & objs = getTile(pos).visitableObjects;
-	assert(objs.size());
-	if(objs.size() > 1 && lookForHero && objs.front()->ID != Obj::HERO)
+	for (CGObjectInstance * object : getTile(pos).visitableObjects)
 	{
-		assert(objs.back()->ID == Obj::HERO);
-		return objs.back();
+		if (object->ID == type)
+			return object;
 	}
-	else
-		return objs.front();
+	// possibly may trigger for empty placeholders in campaigns
+	logGlobal->warnStream() << "Failed to find object of type " << int(type) << " at " << pos;
+	return nullptr;
 }
 
 void CMap::checkForObjectives()
 {
-	if(isInTheMap(victoryCondition.pos))
-	{
-		victoryCondition.obj = getObjectiveObjectFrom(victoryCondition.pos, victoryCondition.condition == EVictoryConditionType::BEATHERO);
-	}
-
-	if(isInTheMap(lossCondition.pos))
+	// NOTE: probably should be moved to MapFormatH3M.cpp
+	for (TriggeredEvent & event : triggeredEvents)
 	{
-		lossCondition.obj = getObjectiveObjectFrom(lossCondition.pos, lossCondition.typeOfLossCon == ELossConditionType::LOSSHERO);
+		auto patcher = [&](EventCondition & cond)
+		{
+			switch (cond.condition)
+			{
+				break; case EventCondition::HAVE_ARTIFACT:
+					boost::algorithm::replace_first(event.onFulfill, "%s", VLC->arth->artifacts[cond.objectType]->Name());
+
+				break; case EventCondition::HAVE_CREATURES:
+					boost::algorithm::replace_first(event.onFulfill, "%s", VLC->creh->creatures[cond.objectType]->nameSing);
+					boost::algorithm::replace_first(event.onFulfill, "%d", boost::lexical_cast<std::string>(cond.value));
+
+				break; case EventCondition::HAVE_RESOURCES:
+					boost::algorithm::replace_first(event.onFulfill, "%s", VLC->generaltexth->restypes[cond.objectType]);
+					boost::algorithm::replace_first(event.onFulfill, "%d", boost::lexical_cast<std::string>(cond.value));
+
+				break; case EventCondition::HAVE_BUILDING:
+					if (isInTheMap(cond.position))
+						cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN);
+
+				break; case EventCondition::CONTROL:
+					if (isInTheMap(cond.position))
+						cond.object = getObjectiveObjectFrom(cond.position, Obj::EObj(cond.objectType));
+
+					if (cond.object)
+					{
+						const CGTownInstance *town = dynamic_cast<const CGTownInstance*>(cond.object);
+						if (town)
+							boost::algorithm::replace_first(event.onFulfill, "%s", town->name);
+						const CGHeroInstance *hero = dynamic_cast<const CGHeroInstance*>(cond.object);
+						if (hero)
+							boost::algorithm::replace_first(event.onFulfill, "%s", hero->name);
+					}
+
+				break; case EventCondition::DESTROY:
+					if (isInTheMap(cond.position))
+						cond.object = getObjectiveObjectFrom(cond.position, Obj::EObj(cond.objectType));
+
+					if (cond.object)
+					{
+						const CGHeroInstance *hero = dynamic_cast<const CGHeroInstance*>(cond.object);
+						if (hero)
+							boost::algorithm::replace_first(event.onFulfill, "%s", hero->name);
+					}
+				break; case EventCondition::TRANSPORT:
+					cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN);
+				//break; case EventCondition::DAYS_PASSED:
+				//break; case EventCondition::IS_HUMAN:
+				//break; case EventCondition::DAYS_WITHOUT_TOWN:
+				//break; case EventCondition::STANDARD_WIN:
+			}
+		};
+		event.trigger.forEach(patcher);
 	}
 }
 

+ 78 - 30
lib/mapping/CMap.h

@@ -16,6 +16,7 @@
 #include "../ResourceSet.h"
 #include "../int3.h"
 #include "../GameConstants.h"
+#include "../LogicalExpression.h"
 
 class CArtifactInstance;
 class CGDefInfo;
@@ -104,45 +105,82 @@ struct DLL_LINKAGE PlayerInfo
 };
 
 /// The loss condition describes the condition to lose the game. (e.g. lose all own heroes/castles)
-struct DLL_LINKAGE LossCondition
+struct DLL_LINKAGE EventCondition
 {
-	LossCondition();
+	enum EWinLoseType {
+		HAVE_ARTIFACT,     // type - required artifact
+		HAVE_CREATURES,    // type - creatures to collect, value - amount to collect
+		HAVE_RESOURCES,    // type - resource ID, value - amount to collect
+		HAVE_BUILDING,     // position - town, optional, type - building to build
+		CONTROL,           // position - position of object, optional, type - type of object
+		DESTROY,           // position - position of object, optional, type - type of object
+		TRANSPORT,         // position - where artifact should be transported, type - type of artifact
+		DAYS_PASSED,       // value - number of days from start of the game
+		IS_HUMAN,          // value - 0 = player is AI, 1 = player is human
+		DAYS_WITHOUT_TOWN, // value - how long player can live without town, 0=instakill
+		STANDARD_WIN       // normal defeat all enemies condition
+	};
+
+	EventCondition(EWinLoseType condition = STANDARD_WIN);
+
+	const CGObjectInstance * object; // object that was at specified position on start
+	si32 value;
+	si32 objectType;
+	int3 position;
+	EWinLoseType condition;
 
-	ELossConditionType::ELossConditionType typeOfLossCon;
-	int3 pos; /// the position of an object which mustn't be lost
-	si32 timeLimit; /// time limit in days, -1 if not used
-	const CGObjectInstance * obj;
+	template <typename Handler>
+	void serialize(Handler & h, const int version)
+	{
+		h & object & value & objectType & position & condition;
+	}
+};
+
+typedef LogicalExpression<EventCondition> EventExpression;
+
+struct EventEffect
+{
+	enum EType
+	{
+		VICTORY,
+		DEFEAT
+	};
+
+	/// effect type, using EType enum
+	si8 type;
+
+	/// message that will be sent to other players
+	std::string toOtherMessage;
 
 	template <typename Handler>
 	void serialize(Handler & h, const int version)
 	{
-		h & typeOfLossCon & pos & timeLimit & obj;
+		h & type & toOtherMessage;
 	}
 };
 
-/// The victory condition describes the condition to win the game. (e.g. defeat all enemy heroes/castles,
-/// receive a specific artifact, ...)
-struct DLL_LINKAGE VictoryCondition
+struct TriggeredEvent
 {
-	VictoryCondition();
-
-	EVictoryConditionType::EVictoryConditionType condition;
-	bool allowNormalVictory; /// true if a normal victory is allowed (defeat all enemy towns, heroes)
-	bool appliesToAI;
-	/// pos of city to upgrade (3); pos of town to build grail, {-1,-1,-1} if not relevant (4); hero pos (5); town pos(6);
-	///	monster pos (7); destination pos(8)
-	int3 pos;
-	/// artifact ID (0); monster ID (1); resource ID (2); needed fort level in upgraded town (3); artifact ID (8)
-	si32 objectId;
-	/// needed count for creatures (1) / resource (2); upgraded town hall level (3);
-	si32 count;
-	/// object of specific monster / city / hero instance (nullptr if not used); set during map parsing
-	const CGObjectInstance * obj;
+
+	/// base condition that must be evaluated
+	EventExpression trigger;
+
+	/// string identifier read from config file (e.g. captureKreelah)
+	std::string identifier;
+
+	/// string-description, for use in UI (capture town to win)
+	std::string description;
+
+	/// Message that will be displayed when this event is triggered (You captured town. You won!)
+	std::string onFulfill;
+
+	/// Effect of this event. TODO: refactor into something more flexible
+	EventEffect effect;
 
 	template <typename Handler>
 	void serialize(Handler & h, const int version)
 	{
-		h & condition & allowNormalVictory & appliesToAI & pos & objectId & count & obj;
+		h & identifier & trigger & description & onFulfill & effect;
 	}
 };
 
@@ -287,6 +325,7 @@ enum EMapFormat
 	ROE = 0x0e, // 14
 	AB  = 0x15, // 21
 	SOD = 0x1c, // 28
+// HOTA = 0x1e ... 0x20 // 28 ... 30
 	WOG = 0x33  // 51
 };
 }
@@ -294,6 +333,7 @@ enum EMapFormat
 /// The map header holds information about loss/victory condition,map format, version, players, height, width,...
 class DLL_LINKAGE CMapHeader
 {
+	void setupEvents();
 public:
 	static const int MAP_SIZE_SMALL;
 	static const int MAP_SIZE_MIDDLE;
@@ -313,19 +353,27 @@ public:
 	/// Specifies the maximum level to reach for a hero. A value of 0 states that there is no
 	///	maximum level for heroes. This is the default value.
 	ui8 levelLimit;
-	LossCondition lossCondition; /// The default value is lose all your towns and heroes.
-	VictoryCondition victoryCondition; /// The default value is defeat all enemies.
+
+	std::string victoryMessage;
+	std::string defeatMessage;
+	ui16 victoryIconIndex;
+	ui16 defeatIconIndex;
+
 	std::vector<PlayerInfo> players; /// The default size of the vector is PlayerColor::PLAYER_LIMIT.
 	ui8 howManyTeams;
 	std::vector<bool> allowedHeroes;
 	std::vector<ui16> placeholdedHeroes;
 	bool areAnyPlayers; /// Unused. True if there are any playable players on the map.
 
+	/// "main quests" of the map that describe victory and loss conditions
+	std::vector<TriggeredEvent> triggeredEvents;
+
 	template <typename Handler>
 	void serialize(Handler & h, const int Version)
 	{
 		h & version & name & description & width & height & twoLevel & difficulty & levelLimit & areAnyPlayers;
-		h & players & lossCondition & victoryCondition & howManyTeams & allowedHeroes;
+		h & players & howManyTeams & allowedHeroes & triggeredEvents;
+		h & victoryMessage & victoryIconIndex & defeatMessage & defeatIconIndex;
 	}
 };
 
@@ -350,8 +398,8 @@ public:
 	void eraseArtifactInstance(CArtifactInstance * art);
 	void addQuest(CGObjectInstance * quest);
 
-	/// Gets the topmost object or the lowermost object depending on the flag lookForHero from the specified position.
-	const CGObjectInstance * getObjectiveObjectFrom(int3 pos, bool lookForHero);
+	/// Gets object of specified type on requested position
+	const CGObjectInstance * getObjectiveObjectFrom(int3 pos, Obj::EObj type);
 	CGHeroInstance * getHero(int heroId);
 
 	/// Sets the victory/loss condition objectives ??

+ 242 - 39
lib/mapping/MapFormatH3M.cpp

@@ -20,6 +20,7 @@
 
 #include "../CSpellHandler.h"
 #include "../CCreatureHandler.h"
+#include "../CGeneralTextHandler.h"
 #include "../CHeroHandler.h"
 #include "../CObjectHandler.h"
 #include "../CDefObjInfoHandler.h"
@@ -281,98 +282,292 @@ void CMapLoaderH3M::readPlayerInfo()
 	}
 }
 
+namespace EVictoryConditionType
+{
+	enum EVictoryConditionType { ARTIFACT, GATHERTROOP, GATHERRESOURCE, BUILDCITY, BUILDGRAIL, BEATHERO,
+		CAPTURECITY, BEATMONSTER, TAKEDWELLINGS, TAKEMINES, TRANSPORTITEM, WINSTANDARD = 255 };
+}
+
+namespace ELossConditionType
+{
+	enum ELossConditionType { LOSSCASTLE, LOSSHERO, TIMEEXPIRES, LOSSSTANDARD = 255 };
+}
+
 void CMapLoaderH3M::readVictoryLossConditions()
 {
-	mapHeader->victoryCondition.obj = nullptr;
-	mapHeader->victoryCondition.condition = (EVictoryConditionType::EVictoryConditionType)reader.readUInt8();
+	mapHeader->triggeredEvents.clear();
+
+	auto vicCondition = (EVictoryConditionType::EVictoryConditionType)reader.readUInt8();
+
+	EventCondition victoryCondition(EventCondition::STANDARD_WIN);
+	EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN);
+	defeatCondition.value = 7;
+
+	TriggeredEvent standardVictory;
+	standardVictory.effect.type = EventEffect::VICTORY;
+	standardVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[5];
+	standardVictory.identifier = "standardVictory";
+	standardVictory.description = ""; // TODO: display in quest window
+	standardVictory.onFulfill = VLC->generaltexth->allTexts[659];
+	standardVictory.trigger = EventExpression(victoryCondition);
+
+	TriggeredEvent standardDefeat;
+	standardDefeat.effect.type = EventEffect::DEFEAT;
+	standardDefeat.effect.toOtherMessage = VLC->generaltexth->allTexts[8];
+	standardDefeat.identifier = "standardDefeat";
+	standardDefeat.description = ""; // TODO: display in quest window
+	standardDefeat.onFulfill = VLC->generaltexth->allTexts[7];
+	standardDefeat.trigger = EventExpression(defeatCondition);
 
 	// Specific victory conditions
-	if(mapHeader->victoryCondition.condition != EVictoryConditionType::WINSTANDARD)
+	if(vicCondition == EVictoryConditionType::WINSTANDARD)
+	{
+		// create normal condition
+		mapHeader->triggeredEvents.push_back(standardVictory);
+		mapHeader->victoryIconIndex = 11;
+		mapHeader->victoryMessage = VLC->generaltexth->victoryConditions[0];
+	}
+	else
 	{
-		mapHeader->victoryCondition.allowNormalVictory = reader.readBool();
-		mapHeader->victoryCondition.appliesToAI = reader.readBool();
+		TriggeredEvent specialVictory;
+		specialVictory.effect.type = EventEffect::VICTORY;
+		specialVictory.identifier = "specialVictory";
+		specialVictory.description = ""; // TODO: display in quest window
 
-		// Read victory conditions
-//		int nr = 0;
-		switch(mapHeader->victoryCondition.condition)
+		mapHeader->victoryIconIndex = ui16(vicCondition);
+		mapHeader->victoryMessage = VLC->generaltexth->victoryConditions[size_t(vicCondition) + 1];
+
+		bool allowNormalVictory = reader.readBool();
+		bool appliesToAI = reader.readBool();
+
+		switch(vicCondition)
 		{
 		case EVictoryConditionType::ARTIFACT:
 			{
-				mapHeader->victoryCondition.objectId = reader.readUInt8();
+				EventCondition cond(EventCondition::HAVE_ARTIFACT);
+				cond.objectType = reader.readUInt8();
 				if (mapHeader->version != EMapFormat::ROE)
 					reader.skip(1);
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[281];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[280];
+				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
 		case EVictoryConditionType::GATHERTROOP:
 			{
-				mapHeader->victoryCondition.objectId = reader.readUInt8();
+				EventCondition cond(EventCondition::HAVE_CREATURES);
+				cond.objectType = reader.readUInt8();
 				if (mapHeader->version != EMapFormat::ROE)
 					reader.skip(1);
-				mapHeader->victoryCondition.count = reader.readUInt32();
+				cond.value = reader.readUInt32();
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[277];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[276];
+				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
 		case EVictoryConditionType::GATHERRESOURCE:
 			{
-				mapHeader->victoryCondition.objectId = reader.readUInt8();
-				mapHeader->victoryCondition.count = reader.readUInt32();
+				EventCondition cond(EventCondition::HAVE_RESOURCES);
+				cond.objectType = reader.readUInt8();
+				cond.value = reader.readUInt32();
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[279];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[278];
+				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
 		case EVictoryConditionType::BUILDCITY:
 			{
-				mapHeader->victoryCondition.pos = readInt3();
-				mapHeader->victoryCondition.count = reader.readUInt8();
-				mapHeader->victoryCondition.objectId = reader.readUInt8();
+				EventExpression::OperatorAll oper;
+				EventCondition cond(EventCondition::HAVE_BUILDING);
+				cond.position = readInt3();
+				cond.objectType = BuildingID::VILLAGE_HALL + reader.readUInt8();
+				oper.expressions.push_back(cond);
+				cond.objectType = BuildingID::FORT + reader.readUInt8();
+				oper.expressions.push_back(cond);
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[283];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[282];
+				specialVictory.trigger = EventExpression(oper);
 				break;
 			}
 		case EVictoryConditionType::BUILDGRAIL:
 			{
-				int3 p = readInt3();
-				if(p.z > 2)
-				{
-					p = int3(-1,-1,-1);
-				}
-				mapHeader->victoryCondition.pos = p;
+				EventCondition cond(EventCondition::HAVE_BUILDING);
+				cond.objectType = BuildingID::GRAIL;
+				cond.position = readInt3();
+				if(cond.position.z > 2)
+					cond.position = int3(-1,-1,-1);
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[285];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[284];
+				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
 		case EVictoryConditionType::BEATHERO:
+			{
+				EventCondition cond(EventCondition::DESTROY);
+				cond.objectType = Obj::HERO;
+				cond.position = readInt3();
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[253];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[252];
+				specialVictory.trigger = EventExpression(cond);
+				break;
+			}
 		case EVictoryConditionType::CAPTURECITY:
+			{
+				EventCondition cond(EventCondition::CONTROL);
+				cond.objectType = Obj::TOWN;
+				cond.position = readInt3();
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[250];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[249];
+				specialVictory.trigger = EventExpression(cond);
+				break;
+			}
 		case EVictoryConditionType::BEATMONSTER:
 			{
-				mapHeader->victoryCondition.pos = readInt3();
+				EventCondition cond(EventCondition::DESTROY);
+				cond.objectType = Obj::MONSTER;
+				cond.position = readInt3();
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[287];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[286];
+				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
 		case EVictoryConditionType::TAKEDWELLINGS:
+			{
+				EventCondition cond(EventCondition::CONTROL);
+				cond.objectType = Obj::CREATURE_GENERATOR1; // FIXME: generators 2-4?
+				cond.position = readInt3();
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[289];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[288];
+				specialVictory.trigger = EventExpression(cond);
+				break;
+			}
 		case EVictoryConditionType::TAKEMINES:
 			{
+				EventCondition cond(EventCondition::CONTROL);
+				cond.objectType = Obj::MINE;
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[291];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[290];
+				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
 		case EVictoryConditionType::TRANSPORTITEM:
 			{
-				mapHeader->victoryCondition.objectId = reader.readUInt8();
-				mapHeader->victoryCondition.pos = readInt3();
+				EventCondition cond(EventCondition::TRANSPORT);
+				cond.objectType = reader.readUInt8();
+				cond.position = readInt3();
+
+				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[293];
+				specialVictory.onFulfill = VLC->generaltexth->allTexts[292];
+				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
 		default:
 			assert(0);
 		}
+		//bool allowNormalVictory = reader.readBool();
+		// if condition is human-only turn it into following construction: AllOf(human, condition)
+		if (!appliesToAI)
+		{
+			EventExpression::OperatorAll oper;
+			EventCondition notAI(EventCondition::IS_HUMAN);
+			notAI.value = 1;
+			oper.expressions.push_back(notAI);
+			oper.expressions.push_back(specialVictory.trigger.get());
+			specialVictory.trigger = EventExpression(oper);
+		}
+
+		// if normal victory allowed - add one more quest
+		if (allowNormalVictory)
+		{
+			mapHeader->victoryMessage += " / ";
+			mapHeader->victoryMessage += VLC->generaltexth->victoryConditions[0];
+			mapHeader->triggeredEvents.push_back(standardVictory);
+		}
+		mapHeader->triggeredEvents.push_back(specialVictory);
 	}
 
 	// Read loss conditions
-	mapHeader->lossCondition.typeOfLossCon = (ELossConditionType::ELossConditionType) reader.readUInt8();
-	switch(mapHeader->lossCondition.typeOfLossCon)
+	auto lossCond = (ELossConditionType::ELossConditionType)reader.readUInt8();
+	if (lossCond == ELossConditionType::LOSSSTANDARD)
 	{
-	case ELossConditionType::LOSSCASTLE:
-	case ELossConditionType::LOSSHERO:
-		{
-			mapHeader->lossCondition.pos = readInt3();
-			break;
-		}
-	case ELossConditionType::TIMEEXPIRES:
+		mapHeader->defeatIconIndex = 3;
+		mapHeader->defeatMessage = VLC->generaltexth->lossCondtions[0];
+	}
+	else
+	{
+		TriggeredEvent specialDefeat;
+		specialDefeat.effect.type = EventEffect::DEFEAT;
+		specialDefeat.effect.toOtherMessage = VLC->generaltexth->allTexts[5];
+		specialDefeat.identifier = "specialDefeat";
+		specialDefeat.description = ""; // TODO: display in quest window
+
+		mapHeader->defeatIconIndex = ui16(lossCond);
+		mapHeader->defeatMessage = VLC->generaltexth->lossCondtions[size_t(lossCond) + 1];
+
+		switch(lossCond)
 		{
-			mapHeader->lossCondition.timeLimit = reader.readUInt16();
-			break;
+		case ELossConditionType::LOSSCASTLE:
+			{
+				EventExpression::OperatorNone noneOf;
+				EventCondition cond(EventCondition::CONTROL);
+				cond.objectType = Obj::TOWN;
+				cond.position = readInt3();
+
+				noneOf.expressions.push_back(cond);
+				specialDefeat.onFulfill = VLC->generaltexth->allTexts[251];
+				specialDefeat.trigger = EventExpression(noneOf);
+				break;
+			}
+		case ELossConditionType::LOSSHERO:
+			{
+				EventExpression::OperatorNone noneOf;
+				EventCondition cond(EventCondition::CONTROL);
+				cond.objectType = Obj::HERO;
+				cond.position = readInt3();
+
+				noneOf.expressions.push_back(cond);
+				specialDefeat.onFulfill = VLC->generaltexth->allTexts[253];
+				specialDefeat.trigger = EventExpression(noneOf);
+				break;
+			}
+		case ELossConditionType::TIMEEXPIRES:
+			{
+				EventCondition cond(EventCondition::DAYS_PASSED);
+				cond.value = reader.readUInt16();
+
+				specialDefeat.onFulfill = VLC->generaltexth->allTexts[254];
+				specialDefeat.trigger = EventExpression(cond);
+				break;
+			}
 		}
+		// turn simple loss condition into complete one that can be evaluated later:
+		// - any of :
+		//   - days without town: 7
+		//   - all of:
+		//     - is human
+		//     - (expression)
+
+		EventExpression::OperatorAll allOf;
+		EventCondition isHuman(EventCondition::IS_HUMAN);
+		isHuman.value = 1;
+
+		allOf.expressions.push_back(isHuman);
+		allOf.expressions.push_back(specialDefeat.trigger.get());
+		specialDefeat.trigger = EventExpression(allOf);
+
+		mapHeader->triggeredEvents.push_back(specialDefeat);
 	}
+	mapHeader->triggeredEvents.push_back(standardDefeat);
 }
 
 void CMapLoaderH3M::readTeamInfo()
@@ -470,10 +665,18 @@ void CMapLoaderH3M::readAllowedArtifacts()
 	}
 
 	// Messy, but needed
-	if(map->victoryCondition.condition == EVictoryConditionType::ARTIFACT
-			|| map->victoryCondition.condition == EVictoryConditionType::TRANSPORTITEM)
+	for (TriggeredEvent & event : map->triggeredEvents)
 	{
-		map->allowedArtifact[map->victoryCondition.objectId] = false;
+		auto patcher = [&](EventCondition & cond)
+		{
+			if (cond.condition == EventCondition::HAVE_ARTIFACT ||
+				cond.condition == EventCondition::TRANSPORT)
+			{
+				map->allowedArtifact[cond.objectType] = false;
+			}
+		};
+
+		event.trigger.forEach(patcher);
 	}
 }
 

+ 34 - 121
server/CGameHandler.cpp

@@ -1923,8 +1923,7 @@ void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInsta
 	vistiCastleObjects (obj, hero);
 	giveSpells (obj, hero);
 
-	if(gs->map->victoryCondition.condition == EVictoryConditionType::TRANSPORTITEM)
-		checkVictoryLossConditionsForPlayer(hero->tempOwner); //transported artifact?
+	checkVictoryLossConditionsForPlayer(hero->tempOwner); //transported artifact?
 }
 
 void CGameHandler::vistiCastleObjects (const CGTownInstance *t, const CGHeroInstance *h)
@@ -2168,29 +2167,25 @@ void CGameHandler::applyAndSend(CPackForClient * info)
 void CGameHandler::sendAndApply(CGarrisonOperationPack * info)
 {
 	sendAndApply(static_cast<CPackForClient*>(info));
-	if(gs->map->victoryCondition.condition == EVictoryConditionType::GATHERTROOP)
-		checkVictoryLossConditionsForAll();
+	checkVictoryLossConditionsForAll();
 }
 
 void CGameHandler::sendAndApply( SetResource * info )
 {
 	sendAndApply(static_cast<CPackForClient*>(info));
-	if(gs->map->victoryCondition.condition == EVictoryConditionType::GATHERRESOURCE)
-		checkVictoryLossConditionsForPlayer(info->player);
+	checkVictoryLossConditionsForPlayer(info->player);
 }
 
 void CGameHandler::sendAndApply( SetResources * info )
 {
 	sendAndApply(static_cast<CPackForClient*>(info));
-	if(gs->map->victoryCondition.condition == EVictoryConditionType::GATHERRESOURCE)
-		checkVictoryLossConditionsForPlayer(info->player);
+	checkVictoryLossConditionsForPlayer(info->player);
 }
 
 void CGameHandler::sendAndApply( NewStructures * info )
 {
 	sendAndApply(static_cast<CPackForClient*>(info));
-	if(gs->map->victoryCondition.condition == EVictoryConditionType::BUILDCITY)
-		checkVictoryLossConditionsForPlayer(getTown(info->tid)->tempOwner);
+	checkVictoryLossConditionsForPlayer(getTown(info->tid)->tempOwner);
 }
 
 void CGameHandler::save(const std::string & filename )
@@ -5048,7 +5043,8 @@ void CGameHandler::checkVictoryLossConditions(const std::set<PlayerColor> & play
 {
 	for(auto playerColor : playerColors)
 	{
-		if(gs->getPlayer(playerColor)) checkVictoryLossConditionsForPlayer(playerColor);
+		if(gs->getPlayer(playerColor))
+			checkVictoryLossConditionsForPlayer(playerColor);
 	}
 }
 
@@ -5069,7 +5065,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
 
 	auto victoryLossCheckResult = gs->checkForVictoryAndLoss(player);
 
-	if(victoryLossCheckResult != EVictoryLossCheckResult::NO_VICTORY_OR_LOSS)
+	if(victoryLossCheckResult.victory() || victoryLossCheckResult.loss())
 	{
 		InfoWindow iw;
 		getVictoryLossMessage(player, victoryLossCheckResult, iw);
@@ -5083,18 +5079,19 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
 		if(victoryLossCheckResult.victory())
 		{
 			//one player won -> all enemies lost
-			iw.text.localStrings.front().second++; //message about losing because enemy won first is just after victory message
-
 			for (auto i = gs->players.cbegin(); i!=gs->players.cend(); i++)
 			{
 				if(i->first != player && gs->getPlayer(i->first)->status == EPlayerStatus::INGAME)
 				{
-					iw.player = i->first;
-					sendAndApply(&iw);
-
 					peg.player = i->first;
 					peg.victoryLossCheckResult = gameState()->getPlayerRelations(player, i->first) == PlayerRelations::ALLIES ?
-								victoryLossCheckResult : EVictoryLossCheckResult::LOSS_STANDARD_HEROES_AND_TOWNS; // ally of winner
+								victoryLossCheckResult : victoryLossCheckResult.invert(); // ally of winner
+
+					InfoWindow iw;
+					getVictoryLossMessage(player, peg.victoryLossCheckResult, iw);
+					iw.player = i->first;
+
+					sendAndApply(&iw);
 					sendAndApply(&peg);
 				}
 			}
@@ -5122,7 +5119,6 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
 							connection->prepareForSendingHeroes();
 					}
 
-
 					UpdateCampaignState ucs;
 					ucs.camp = gs->scenarioOps->campState;
 					sendAndApply(&ucs);
@@ -5149,6 +5145,19 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
 				if(player.getNum() != i) playerColors.insert(PlayerColor(i));
 			}
 
+			//notify all players
+			for (auto i = gs->players.cbegin(); i!=gs->players.cend(); i++)
+			{
+				if(i->first != player && gs->getPlayer(i->first)->status == EPlayerStatus::INGAME)
+				{
+					InfoWindow iw;
+					getVictoryLossMessage(player, victoryLossCheckResult.invert(), iw);
+					iw.player = i->first;
+					sendAndApply(&iw);
+				}
+			}
+
+
 			checkVictoryLossConditions(playerColors);
 		}
 
@@ -5162,110 +5171,14 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
 
 void CGameHandler::getVictoryLossMessage(PlayerColor player, EVictoryLossCheckResult victoryLossCheckResult, InfoWindow & out) const
 {
-//	const PlayerState *p = gs->getPlayer(player);
-// 	if(!p->human)
-// 		return; //AI doesn't need text info of loss
-
 	out.player = player;
+	out.text.clear();
+	out.text << victoryLossCheckResult.messageToSelf;
+	// hackish, insert one player-specific string, if applicable
+	if (victoryLossCheckResult.messageToSelf.find("%s") != std::string::npos)
+		out.text.addReplacement(MetaString::COLOR, player.getNum());
 
-	if(victoryLossCheckResult == EVictoryLossCheckResult::VICTORY_SPECIAL)
-	{
-		switch(gs->map->victoryCondition.condition)
-		{
-		case EVictoryConditionType::ARTIFACT:
-			out.text.addTxt(MetaString::GENERAL_TXT, 280); //Congratulations! You have found the %s, and can claim victory!
-			out.text.addReplacement(MetaString::ART_NAMES,gs->map->victoryCondition.objectId); //artifact name
-			break;
-		case EVictoryConditionType::GATHERTROOP:
-			out.text.addTxt(MetaString::GENERAL_TXT, 276); //Congratulations! You have over %d %s in your armies. Your enemies have no choice but to bow down before your power!
-			out.text.addReplacement(gs->map->victoryCondition.count);
-			out.text.addReplacement(MetaString::CRE_PL_NAMES, gs->map->victoryCondition.objectId);
-			break;
-		case EVictoryConditionType::GATHERRESOURCE:
-			out.text.addTxt(MetaString::GENERAL_TXT, 278); //Congratulations! You have collected over %d %s in your treasury. Victory is yours!
-			out.text.addReplacement(gs->map->victoryCondition.count);
-			out.text.addReplacement(MetaString::RES_NAMES, gs->map->victoryCondition.objectId);
-			break;
-		case EVictoryConditionType::BUILDCITY:
-			out.text.addTxt(MetaString::GENERAL_TXT, 282); //Congratulations! You have successfully upgraded your town, and can claim victory!
-			break;
-		case EVictoryConditionType::BUILDGRAIL:
-			out.text.addTxt(MetaString::GENERAL_TXT, 284); //Congratulations! You have constructed a permanent home for the Grail, and can claim victory!
-			break;
-		case EVictoryConditionType::BEATHERO:
-			{
-				out.text.addTxt(MetaString::GENERAL_TXT, 252); //Congratulations! You have completed your quest to defeat the enemy hero %s. Victory is yours!
-				const CGHeroInstance *h = dynamic_cast<const CGHeroInstance*>(gs->map->victoryCondition.obj);
-				assert(h);
-				out.text.addReplacement(h->name);
-			}
-			break;
-		case EVictoryConditionType::CAPTURECITY:
-			{
-				out.text.addTxt(MetaString::GENERAL_TXT, 249); //Congratulations! You captured %s, and are victorious!
-				const CGTownInstance *t = dynamic_cast<const CGTownInstance*>(gs->map->victoryCondition.obj);
-				assert(t);
-				out.text.addReplacement(t->name);
-			}
-			break;
-		case EVictoryConditionType::BEATMONSTER:
-			out.text.addTxt(MetaString::GENERAL_TXT, 286); //Congratulations! You have completed your quest to kill the fearsome beast, and can claim victory!
-			break;
-		case EVictoryConditionType::TAKEDWELLINGS:
-			out.text.addTxt(MetaString::GENERAL_TXT, 288); //Congratulations! Your flag flies on the dwelling of every creature. Victory is yours!
-			break;
-		case EVictoryConditionType::TAKEMINES:
-			out.text.addTxt(MetaString::GENERAL_TXT, 290); //Congratulations! Your flag flies on every mine. Victory is yours!
-			break;
-		case EVictoryConditionType::TRANSPORTITEM:
-			out.text.addTxt(MetaString::GENERAL_TXT, 292); //Congratulations! You have reached your destination, precious cargo intact, and can claim victory!
-			break;
-		}
-	}
-	else if(victoryLossCheckResult == EVictoryLossCheckResult::VICTORY_STANDARD)
-	{
-		out.text.addTxt(MetaString::GENERAL_TXT, 659); // Congratulations! All your enemies have been defeated! Victory is yours!
-	}
-	else if(victoryLossCheckResult == EVictoryLossCheckResult::LOSS_STANDARD_TOWNS_AND_TIME_OVER)
-	{
-			out.text.addTxt(MetaString::GENERAL_TXT, 7);//%s, your heroes abandon you, and you are banished from this land.
-			out.text.addReplacement(MetaString::COLOR, player.getNum());
-			out.components.push_back(Component(Component::FLAG, player.getNum(), 0, 0));
-	}
-	else if(victoryLossCheckResult == EVictoryLossCheckResult::LOSS_SPECIAL)
-	{
-		switch(gs->map->lossCondition.typeOfLossCon)
-		{
-		case ELossConditionType::LOSSCASTLE:
-			{
-				out.text.addTxt(MetaString::GENERAL_TXT, 251); //The town of %s has fallen - all is lost!
-				const CGTownInstance *t = dynamic_cast<const CGTownInstance*>(gs->map->lossCondition.obj);
-				assert(t);
-				out.text.addReplacement(t->name);
-			}
-			break;
-		case ELossConditionType::LOSSHERO:
-			{
-				out.text.addTxt(MetaString::GENERAL_TXT, 253); //The hero, %s, has suffered defeat - your quest is over!
-				const CGHeroInstance *h = dynamic_cast<const CGHeroInstance*>(gs->map->lossCondition.obj);
-				assert(h);
-				out.text.addReplacement(h->name);
-			}
-			break;
-		case ELossConditionType::TIMEEXPIRES:
-			out.text.addTxt(MetaString::GENERAL_TXT, 254); //Alas, time has run out on your quest. All is lost.
-			break;
-		}
-	}
-	else if(victoryLossCheckResult == EVictoryLossCheckResult::LOSS_STANDARD_HEROES_AND_TOWNS)
-	{
-		out.text.addTxt(MetaString::GENERAL_TXT, 660); //All your forces have been defeated, and you are banished from this land!
-	}
-	else
-	{
-		assert(0);
-		logGlobal->warnStream() << "Unknown victory loss check result";
-	}
+	out.components.push_back(Component(Component::FLAG, player.getNum(), 0, 0));
 }
 
 bool CGameHandler::dig( const CGHeroInstance *h )