Browse Source

Merge branch 'develop' into regions

Laserlicht 1 year ago
parent
commit
d2e24e01b4
75 changed files with 5282 additions and 700 deletions
  1. 86 33
      AI/BattleAI/AttackPossibility.cpp
  2. 1 0
      AI/BattleAI/AttackPossibility.h
  3. 1 1
      AI/BattleAI/BattleEvaluator.cpp
  4. 51 59
      AI/BattleAI/BattleExchangeVariant.cpp
  5. 1 1
      AI/BattleAI/BattleExchangeVariant.h
  6. 5 0
      AI/BattleAI/StackWithBonuses.h
  7. 2 31
      client/CServerHandler.cpp
  8. 0 4
      client/CServerHandler.h
  9. 0 68
      client/mainmenu/CHighScoreScreen.cpp
  10. 0 10
      client/mainmenu/CHighScoreScreen.h
  11. 12 12
      client/render/CAnimation.cpp
  12. 3 0
      lib/CMakeLists.txt
  13. 70 20
      lib/battle/CBattleInfoCallback.cpp
  14. 22 3
      lib/battle/CBattleInfoCallback.h
  15. 7 146
      lib/gameState/CGameState.cpp
  16. 5 0
      lib/gameState/CGameState.h
  17. 370 0
      lib/gameState/GameStatistics.cpp
  18. 156 0
      lib/gameState/GameStatistics.h
  19. 111 0
      lib/gameState/HighScore.cpp
  20. 27 0
      lib/gameState/HighScore.h
  21. 1 1
      lib/mapObjects/CBank.cpp
  22. 1 1
      lib/mapObjects/CBank.h
  23. 1 1
      lib/mapObjects/CGCreature.cpp
  24. 1 1
      lib/mapObjects/CGCreature.h
  25. 1 1
      lib/mapObjects/CGDwelling.cpp
  26. 1 1
      lib/mapObjects/CGDwelling.h
  27. 1 1
      lib/mapObjects/CGPandoraBox.cpp
  28. 1 1
      lib/mapObjects/CGPandoraBox.h
  29. 1 1
      lib/mapObjects/CGTownBuilding.cpp
  30. 1 1
      lib/mapObjects/CGTownBuilding.h
  31. 7 1
      lib/mapObjects/CGTownInstance.cpp
  32. 2 2
      lib/mapObjects/CGTownInstance.h
  33. 2 2
      lib/mapObjects/CQuest.cpp
  34. 2 2
      lib/mapObjects/CQuest.h
  35. 1 1
      lib/mapObjects/CRewardableObject.cpp
  36. 1 1
      lib/mapObjects/CRewardableObject.h
  37. 1 1
      lib/mapObjects/IObjectInterface.cpp
  38. 1 1
      lib/mapObjects/IObjectInterface.h
  39. 3 3
      lib/mapObjects/MiscObjects.cpp
  40. 3 3
      lib/mapObjects/MiscObjects.h
  41. 1 1
      lib/networkPacks/PacksForClient.h
  42. 3 2
      lib/serializer/ESerializationVersion.h
  43. 10 0
      mapeditor/CMakeLists.txt
  44. 4 0
      mapeditor/inspector/inspector.cpp
  45. 144 43
      mapeditor/inspector/townbuildingswidget.cpp
  46. 22 2
      mapeditor/inspector/townbuildingswidget.h
  47. 34 2
      mapeditor/inspector/townbuildingswidget.ui
  48. 289 0
      mapeditor/inspector/towneventdialog.cpp
  49. 53 0
      mapeditor/inspector/towneventdialog.h
  50. 266 0
      mapeditor/inspector/towneventdialog.ui
  51. 177 0
      mapeditor/inspector/towneventswidget.cpp
  52. 58 0
      mapeditor/inspector/towneventswidget.h
  53. 93 0
      mapeditor/inspector/towneventswidget.ui
  54. 166 0
      mapeditor/inspector/townspellswidget.cpp
  55. 60 0
      mapeditor/inspector/townspellswidget.h
  56. 304 0
      mapeditor/inspector/townspellswidget.ui
  57. 20 0
      mapeditor/mapeditorroles.h
  58. 3 0
      mapeditor/mapsettings/eventsettings.h
  59. 211 21
      mapeditor/translation/chinese.ts
  60. 211 21
      mapeditor/translation/czech.ts
  61. 211 9
      mapeditor/translation/english.ts
  62. 213 23
      mapeditor/translation/french.ts
  63. 211 21
      mapeditor/translation/german.ts
  64. 211 21
      mapeditor/translation/polish.ts
  65. 211 21
      mapeditor/translation/portuguese.ts
  66. 211 21
      mapeditor/translation/russian.ts
  67. 211 21
      mapeditor/translation/spanish.ts
  68. 211 21
      mapeditor/translation/ukrainian.ts
  69. 211 21
      mapeditor/translation/vietnamese.ts
  70. 29 4
      server/CGameHandler.cpp
  71. 1 0
      server/CGameHandler.h
  72. 32 10
      server/battles/BattleResultProcessor.cpp
  73. 21 0
      server/processors/PlayerMessageProcessor.cpp
  74. 1 0
      server/processors/PlayerMessageProcessor.h
  75. 205 0
      test/battle/CBattleInfoCallbackTest.cpp

+ 86 - 33
AI/BattleAI/AttackPossibility.cpp

@@ -93,6 +93,8 @@ int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const batt
 AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
 	: from(from), dest(dest), attack(attack)
 {
+	this->attack.attackerPos = from;
+	this->attack.defenderPos = dest;
 }
 
 float AttackPossibility::damageDiff() const
@@ -261,63 +263,105 @@ AttackPossibility AttackPossibility::evaluate(
 		if (!attackInfo.shooting)
 			ap.attackerState->setPosition(hex);
 
-		std::vector<const battle::Unit*> units;
+		std::vector<const battle::Unit *> defenderUnits;
+		std::vector<const battle::Unit *> retaliatedUnits = {attacker};
+		std::vector<const battle::Unit *> affectedUnits;
 
 		if (attackInfo.shooting)
-			units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID);
+			defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, true, hex, defender->getPosition());
 		else
-			units = state->getAttackedBattleUnits(attacker, defHex, false, hex);
-
-		// ensure the defender is also affected
-		bool addDefender = true;
-		for(auto unit : units)
 		{
-			if (unit->unitId() == defender->unitId())
+			defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, false, hex, defender->getPosition());
+			retaliatedUnits = state->getAttackedBattleUnits(defender, attacker, hex, false, defender->getPosition(), hex);
+
+			// attacker can not melle-attack itself but still can hit that place where it was before moving
+			vstd::erase_if(defenderUnits, [attacker](const battle::Unit * u) -> bool { return u->unitId() == attacker->unitId(); });
+
+			if(!vstd::contains_if(retaliatedUnits, [attacker](const battle::Unit * u) -> bool { return u->unitId() == attacker->unitId(); }))
 			{
-				addDefender = false;
-				break;
+				retaliatedUnits.push_back(attacker);
 			}
 		}
 
-		if(addDefender)
-			units.push_back(defender);
+		// ensure the defender is also affected
+		if(!vstd::contains_if(defenderUnits, [defender](const battle::Unit * u) -> bool { return u->unitId() == defender->unitId(); }))
+		{
+			defenderUnits.push_back(defender);
+		}
+
+		affectedUnits = defenderUnits;
+		vstd::concatenate(affectedUnits, retaliatedUnits);
+
+		logAi->trace("Attacked battle units count %d, %d->%d", affectedUnits.size(), hex.hex, defHex.hex);
 
-		for(auto u : units)
+		std::map<uint32_t, std::shared_ptr<battle::CUnitState>> defenderStates;
+
+		for(auto u : affectedUnits)
 		{
-			if(!ap.attackerState->alive())
-				break;
+			if(u->unitId() == attacker->unitId())
+				continue;
 
 			auto defenderState = u->acquireState();
+
 			ap.affectedUnits.push_back(defenderState);
+			defenderStates[u->unitId()] = defenderState;
+		}
+
+		for(int i = 0; i < totalAttacks; i++)
+		{
+			if(!ap.attackerState->alive() || !defenderStates[defender->unitId()]->alive())
+				break;
 
-			for(int i = 0; i < totalAttacks; i++)
+			for(auto u : defenderUnits)
 			{
+				auto defenderState = defenderStates.at(u->unitId());
+
 				int64_t damageDealt;
-				int64_t damageReceived;
 				float defenderDamageReduce;
 				float attackerDamageReduce;
 
 				DamageEstimation retaliation;
 				auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation);
 
-				vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth());
-				vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth());
-
-				vstd::amin(retaliation.damage.min, ap.attackerState->getAvailableHealth());
-				vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth());
-
 				damageDealt = averageDmg(attackDmg.damage);
-				defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, damageCache, state);
+				vstd::amin(damageDealt, defenderState->getAvailableHealth());
+
+				defenderDamageReduce = calculateDamageReduce(attacker, u, damageDealt, damageCache, state);
 				ap.attackerState->afterAttack(attackInfo.shooting, false);
 
 				//FIXME: use ranged retaliation
-				damageReceived = 0;
 				attackerDamageReduce = 0;
 
-				if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked)
+				if (!attackInfo.shooting && u->unitId() == defender->unitId() && defenderState->ableToRetaliate() && !counterAttacksBlocked)
 				{
-					damageReceived = averageDmg(retaliation.damage);
-					attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, damageCache, state);
+					for(auto retaliated : retaliatedUnits)
+					{
+						if(retaliated->unitId() == attacker->unitId())
+						{
+							int64_t damageReceived = averageDmg(retaliation.damage);
+
+							vstd::amin(damageReceived, ap.attackerState->getAvailableHealth());
+
+							attackerDamageReduce = calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
+							ap.attackerState->damage(damageReceived);
+						}
+						else
+						{
+							auto retaliationCollateral = state->battleEstimateDamage(defender, retaliated, 0);
+							int64_t damageReceived = averageDmg(retaliationCollateral.damage);
+
+							vstd::amin(damageReceived, retaliated->getAvailableHealth());
+
+							if(defender->unitSide() == retaliated->unitSide())
+								defenderDamageReduce += calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
+							else
+								ap.collateralDamageReduce += calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
+
+							defenderStates.at(retaliated->unitId())->damage(damageReceived);
+						}
+						
+					}
+
 					defenderState->afterAttack(attackInfo.shooting, true);
 				}
 
@@ -331,21 +375,30 @@ AttackPossibility AttackPossibility::evaluate(
 				if(attackerSide == u->unitSide())
 					ap.collateralDamageReduce += defenderDamageReduce;
 
-				if(u->unitId() == defender->unitId() || 
-					(!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex)))
+				if(u->unitId() == defender->unitId()
+					|| (!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex)))
 				{
 					//FIXME: handle RANGED_RETALIATION ?
 					ap.attackerDamageReduce += attackerDamageReduce;
 				}
 
-				ap.attackerState->damage(damageReceived);
 				defenderState->damage(damageDealt);
 
-				if (!ap.attackerState->alive() || !defenderState->alive())
-					break;
+				if(u->unitId() == defender->unitId())
+				{
+					ap.defenderDead = !defenderState->alive();
+				}
 			}
 		}
 
+#if BATTLE_TRACE_LEVEL>=2
+		logAi->trace("BattleAI AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
+			attackInfo.attacker->unitType()->getJsonKey(),
+			attackInfo.defender->unitType()->getJsonKey(),
+			(int)ap.dest, (int)ap.from, (int)ap.affectedUnits.size(),
+			ap.defenderDamageReduce, ap.attackerDamageReduce, ap.collateralDamageReduce, ap.shootersBlockedDmg);
+#endif
+
 		if(!bestAp.dest.isValid() || ap.attackValue() > bestAp.attackValue())
 			bestAp = ap;
 	}

+ 1 - 0
AI/BattleAI/AttackPossibility.h

@@ -49,6 +49,7 @@ public:
 	float attackerDamageReduce = 0; //usually by counter-attack
 	float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks)
 	int64_t shootersBlockedDmg = 0;
+	bool defenderDead = false;
 
 	AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_);
 

+ 1 - 1
AI/BattleAI/BattleEvaluator.cpp

@@ -189,7 +189,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 					else
 					{
 						activeActionMade = true;
-						return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from);
+						return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from);
 					}
 				}
 			}

+ 51 - 59
AI/BattleAI/BattleExchangeVariant.cpp

@@ -30,100 +30,89 @@ float BattleExchangeVariant::trackAttack(
 {
 	auto attacker = hb->getForUpdate(ap.attack.attacker->unitId());
 
-	const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
-	static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
-	const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
-
-	float attackValue = 0;
+	float attackValue = ap.attackValue();
 	auto affectedUnits = ap.affectedUnits;
 
+	dpsScore.ourDamageReduce += ap.attackerDamageReduce + ap.collateralDamageReduce;
+	dpsScore.enemyDamageReduce += ap.defenderDamageReduce + ap.shootersBlockedDmg;
+	attackerValue[attacker->unitId()].value = attackValue;
+
 	affectedUnits.push_back(ap.attackerState);
 
 	for(auto affectedUnit : affectedUnits)
 	{
 		auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId());
+		auto damageDealt = unitToUpdate->getTotalHealth() - affectedUnit->getTotalHealth();
+
+		if(damageDealt > 0)
+		{
+			unitToUpdate->damage(damageDealt);
+		}
 
 		if(unitToUpdate->unitSide() == attacker->unitSide())
 		{
 			if(unitToUpdate->unitId() == attacker->unitId())
 			{
-				auto defender = hb->getForUpdate(ap.attack.defender->unitId());
-
-				if(!defender->alive() || counterAttacksBlocked || ap.attack.shooting || !defender->ableToRetaliate())
-					continue;
-
-				auto retaliationDamage = damageCache.getDamage(defender.get(), unitToUpdate.get(), hb);
-				auto attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), unitToUpdate.get(), retaliationDamage, damageCache, hb);
-
-				attackValue -= attackerDamageReduce;
-				dpsScore.ourDamageReduce += attackerDamageReduce;
-				attackerValue[unitToUpdate->unitId()].isRetaliated = true;
-
-				unitToUpdate->damage(retaliationDamage);
-				defender->afterAttack(false, true);
+				unitToUpdate->afterAttack(ap.attack.shooting, false);
 
 #if BATTLE_TRACE_LEVEL>=1
 				logAi->trace(
-					"%s -> %s, ap retaliation, %s, dps: %2f, score: %2f",
-					defender->getDescription(),
-					unitToUpdate->getDescription(),
+					"%s -> %s, ap retaliation, %s, dps: %lld",
+					ap.attack.defender->getDescription(),
+					ap.attack.attacker->getDescription(),
 					ap.attack.shooting ? "shot" : "mellee",
-					retaliationDamage,
-					attackerDamageReduce);
+					damageDealt);
 #endif
 			}
 			else
 			{
-				auto collateralDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb);
-				auto collateralDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), collateralDamage, damageCache, hb);
-
-				attackValue -= collateralDamageReduce;
-				dpsScore.ourDamageReduce += collateralDamageReduce;
-
-				unitToUpdate->damage(collateralDamage);
-
 #if BATTLE_TRACE_LEVEL>=1
 				logAi->trace(
-					"%s -> %s, ap collateral, %s, dps: %2f, score: %2f",
-					attacker->getDescription(),
+					"%s, ap collateral, dps: %lld",
 					unitToUpdate->getDescription(),
-					ap.attack.shooting ? "shot" : "mellee",
-					collateralDamage,
-					collateralDamageReduce);
+					damageDealt);
 #endif
 			}
 		}
 		else
 		{
-			int64_t attackDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb);
-			float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), attackDamage, damageCache, hb);
-
-			attackValue += defenderDamageReduce;
-			dpsScore.enemyDamageReduce += defenderDamageReduce;
-			attackerValue[attacker->unitId()].value += defenderDamageReduce;
-
-			unitToUpdate->damage(attackDamage);
+			if(unitToUpdate->unitId() == ap.attack.defender->unitId())
+			{
+				if(unitToUpdate->ableToRetaliate() && !affectedUnit->ableToRetaliate())
+				{
+					unitToUpdate->afterAttack(ap.attack.shooting, true);
+				}
 
 #if BATTLE_TRACE_LEVEL>=1
-			logAi->trace(
-				"%s -> %s, ap attack, %s, dps: %2f, score: %2f",
-				attacker->getDescription(),
-				unitToUpdate->getDescription(),
-				ap.attack.shooting ? "shot" : "mellee",
-				attackDamage,
-				defenderDamageReduce);
+				logAi->trace(
+					"%s -> %s, ap attack, %s, dps: %lld",
+					attacker->getDescription(),
+					ap.attack.defender->getDescription(),
+					ap.attack.shooting ? "shot" : "mellee",
+					damageDealt);
 #endif
+			}
+			else
+			{
+#if BATTLE_TRACE_LEVEL>=1
+				logAi->trace(
+					"%s, ap enemy collateral, dps: %lld",
+					unitToUpdate->getDescription(),
+					damageDealt);
+#endif
+			}
 		}
 	}
 
 #if BATTLE_TRACE_LEVEL >= 1
-	logAi->trace("ap shooters blocking: %lld", ap.shootersBlockedDmg);
+	logAi->trace(
+		"ap score: our: %2f, enemy: %2f, collateral: %2f, blocked: %2f",
+		ap.attackerDamageReduce,
+		ap.defenderDamageReduce,
+		ap.collateralDamageReduce,
+		ap.shootersBlockedDmg);
 #endif
 
-	attackValue += ap.shootersBlockedDmg;
-	dpsScore.enemyDamageReduce += ap.shootersBlockedDmg;
-	attacker->afterAttack(ap.attack.shooting, false);
-
 	return attackValue;
 }
 
@@ -230,6 +219,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 		auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
 
+		hbWaited->resetActiveUnit();
 		hbWaited->getForUpdate(activeStack->unitId())->waiting = true;
 		hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true;
 
@@ -259,6 +249,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 	updateReachabilityMap(hb);
 
 	if(result.bestAttack.attack.shooting
+		&& !result.bestAttack.defenderDead
 		&& !activeStack->waited()
 		&& hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest))
 	{
@@ -269,8 +260,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 	for(auto & ap : targets.possibleAttacks)
 	{
 		float score = evaluateExchange(ap, 0, targets, damageCache, hb);
+		bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
 
-		if(score > result.score || (vstd::isAlmostEqual(score, result.score) && result.wait))
+		if(score > result.score || sameScoreButWaited)
 		{
 			result.score = score;
 			result.bestAttack = ap;
@@ -739,7 +731,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
 {
 	std::vector<const battle::Unit *> result;
 
-	for(int i = 0; i < turnOrder.size(); i++, turn++)
+	for(int i = 0; i < turnOrder.size(); i++)
 	{
 		auto & turnQueue = turnOrder[i];
 		HypotheticBattle turnBattle(env.get(), cb);

+ 1 - 1
AI/BattleAI/BattleExchangeVariant.h

@@ -148,7 +148,7 @@ public:
 		std::shared_ptr<CBattleInfoCallback> cb,
 		std::shared_ptr<Environment> env,
 		float strengthRatio): cb(cb), env(env) {
-		negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio;
+		negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio * strengthRatio;
 	}
 
 	EvaluationResult findBestTarget(

+ 5 - 0
AI/BattleAI/StackWithBonuses.h

@@ -164,6 +164,11 @@ public:
 
 	int64_t getTreeVersion() const;
 
+	void resetActiveUnit()
+	{
+		activeUnitId = -1;
+	}
+
 #if SCRIPTING_ENABLED
 	scripting::Pool * getContextPool() const override;
 #endif

+ 2 - 31
client/CServerHandler.cpp

@@ -35,6 +35,7 @@
 #include "../lib/TurnTimerInfo.h"
 #include "../lib/VCMIDirs.h"
 #include "../lib/campaign/CampaignState.h"
+#include "../lib/gameState/HighScore.h"
 #include "../lib/CPlayerState.h"
 #include "../lib/mapping/CMapInfo.h"
 #include "../lib/mapObjects/CGTownInstance.h"
@@ -672,39 +673,9 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
 	setState(EClientState::GAMEPLAY);
 }
 
-HighScoreParameter CServerHandler::prepareHighScores(PlayerColor player, bool victory)
-{
-	const auto * gs = client->gameState();
-	const auto * playerState = gs->getPlayerState(player);
-
-	HighScoreParameter param;
-	param.difficulty = gs->getStartInfo()->difficulty;
-	param.day = gs->getDate();
-	param.townAmount = gs->howManyTowns(player);
-	param.usedCheat = gs->getPlayerState(player)->cheated;
-	param.hasGrail = false;
-	for(const CGHeroInstance * h : playerState->heroes)
-		if(h->hasArt(ArtifactID::GRAIL))
-			param.hasGrail = true;
-	for(const CGTownInstance * t : playerState->towns)
-		if(t->builtBuildings.count(BuildingID::GRAIL))
-			param.hasGrail = true;
-	param.allEnemiesDefeated = true;
-	for (PlayerColor otherPlayer(0); otherPlayer < PlayerColor::PLAYER_LIMIT; ++otherPlayer)
-	{
-		auto ps = gs->getPlayerState(otherPlayer, false);
-		if(ps && otherPlayer != player && !ps->checkVanquished())
-			param.allEnemiesDefeated = false;
-	}
-	param.scenarioName = gs->getMapHeader()->name.toString();
-	param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name;
-
-	return param;
-}
-
 void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory)
 {
-	HighScoreParameter param = prepareHighScores(player, victory);
+	HighScoreParameter param = HighScore::prepareHighScores(client->gameState(), player, victory);
 
 	if(victory && client->gameState()->getStartInfo()->campState)
 	{

+ 0 - 4
client/CServerHandler.h

@@ -40,8 +40,6 @@ class GlobalLobbyClient;
 class GameChatHandler;
 class IServerRunner;
 
-class HighScoreCalculation;
-
 enum class ESelectionScreen : ui8;
 enum class ELoadMode : ui8;
 
@@ -128,8 +126,6 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
 
 	bool isServerLocal() const;
 
-	HighScoreParameter prepareHighScores(PlayerColor player, bool victory);
-
 public:
 	/// High-level connection overlay that is capable of (de)serializing network data
 	std::shared_ptr<CConnection> logicConnection;

+ 0 - 68
client/mainmenu/CHighScoreScreen.cpp

@@ -34,74 +34,6 @@
 #include "../../lib/constants/EntityIdentifiers.h"
 #include "../../lib/gameState/HighScore.h"
 
-auto HighScoreCalculation::calculate()
-{
-	struct Result
-	{
-		int basic = 0;
-		int total = 0;
-		int sumDays = 0;
-		bool cheater = false;
-	};
-	
-	Result firstResult;
-	Result summary;
-	const std::array<double, 5> difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0}; 
-	for(auto & param : parameters)
-	{
-		double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allEnemiesDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0);
-		firstResult = Result{static_cast<int>(tmp), static_cast<int>(tmp * difficultyMultipliers.at(param.difficulty)), param.day, param.usedCheat};
-		summary.basic += firstResult.basic * 5.0 / parameters.size();
-		summary.total += firstResult.total * 5.0 / parameters.size();
-		summary.sumDays += firstResult.sumDays;
-		summary.cheater |= firstResult.cheater;
-	}
-
-	if(parameters.size() == 1)
-		return firstResult;
-
-	return summary;
-}
-
-struct HighScoreCreature
-{
-	CreatureID creature;
-	int min;
-	int max;
-};
-
-static std::vector<HighScoreCreature> getHighscoreCreaturesList()
-{
-	JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json"));
-
-	std::vector<HighScoreCreature> ret;
-
-	for(auto & json : configCreatures["creatures"].Vector())
-	{
-		HighScoreCreature entry;
-		entry.creature = CreatureID::decode(json["creature"].String());
-		entry.max = json["max"].isNull() ? std::numeric_limits<int>::max() : json["max"].Integer();
-		entry.min = json["min"].isNull() ? std::numeric_limits<int>::min() : json["min"].Integer();
-
-		ret.push_back(entry);
-	}
-
-	return ret;
-}
-
-CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign)
-{
-	static const std::vector<HighScoreCreature> creatures = getHighscoreCreaturesList();
-
-	int divide = campaign ? 5 : 1;
-
-	for(auto & creature : creatures)
-		if(points / divide <= creature.max && points / divide >= creature.min)
-			return creature.creature;
-
-	throw std::runtime_error("Unable to find creature for score " + std::to_string(points));
-}
-
 CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted)
 	: CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted)
 {

+ 0 - 10
client/mainmenu/CHighScoreScreen.h

@@ -21,16 +21,6 @@ class CFilledTexture;
 
 class TransparentFilledRectangle;
 
-class HighScoreCalculation
-{
-public:
-	std::vector<HighScoreParameter> parameters;
-	bool isCampaign = false;
-
-	auto calculate();
-	static CreatureID getCreatureForPoints(int points, bool campaign);
-};
-
 class CHighScoreScreen : public CWindowObject
 {
 public:

+ 12 - 12
client/render/CAnimation.cpp

@@ -161,13 +161,13 @@ void CAnimation::verticalFlip()
 
 void CAnimation::horizontalFlip(size_t frame, size_t group)
 {
-	try
+	auto i1 = images.find(group);
+	if(i1 != images.end())
 	{
-		images.at(group).at(frame) = nullptr;
-	}
-	catch (const std::out_of_range &)
-	{
-		// ignore - image not loaded
+		auto i2 = i1->second.find(frame);
+
+		if(i2 != i1->second.end())
+			i2->second = nullptr;
 	}
 
 	auto locator = getImageLocator(frame, group);
@@ -177,13 +177,13 @@ void CAnimation::horizontalFlip(size_t frame, size_t group)
 
 void CAnimation::verticalFlip(size_t frame, size_t group)
 {
-	try
+	auto i1 = images.find(group);
+	if(i1 != images.end())
 	{
-		images.at(group).at(frame) = nullptr;
-	}
-	catch (const std::out_of_range &)
-	{
-		// ignore - image not loaded
+		auto i2 = i1->second.find(frame);
+
+		if(i2 != i1->second.end())
+			i2->second = nullptr;
 	}
 
 	auto locator = getImageLocator(frame, group);

+ 3 - 0
lib/CMakeLists.txt

@@ -99,9 +99,11 @@ set(lib_MAIN_SRCS
 
 	gameState/CGameState.cpp
 	gameState/CGameStateCampaign.cpp
+	gameState/HighScore.cpp
 	gameState/InfoAboutArmy.cpp
 	gameState/RumorState.cpp
 	gameState/TavernHeroesPool.cpp
+	gameState/GameStatistics.cpp
 
 	mapObjectConstructors/AObjectTypeHandler.cpp
 	mapObjectConstructors/CBankInstanceConstructor.cpp
@@ -468,6 +470,7 @@ set(lib_MAIN_HEADERS
 	gameState/RumorState.h
 	gameState/SThievesGuildInfo.h
 	gameState/TavernHeroesPool.h
+	gameState/GameStatistics.h
 	gameState/TavernSlot.h
 	gameState/QuestInfo.h
 

+ 70 - 20
lib/battle/CBattleInfoCallback.cpp

@@ -1248,19 +1248,40 @@ ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityIn
 	return ret;
 }
 
-AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const
+AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(
+	const battle::Unit * attacker,
+	BattleHex destinationTile,
+	BattleHex attackerPos) const
+{
+	const auto * defender = battleGetUnitByPos(destinationTile, true);
+
+	if(!defender)
+		return AttackableTiles(); // can't attack thin air
+
+	return getPotentiallyAttackableHexes(
+		attacker,
+		defender,
+		destinationTile,
+		attackerPos,
+		defender->getPosition());
+}
+
+AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(
+	const battle::Unit* attacker,
+	const battle::Unit * defender,
+	BattleHex destinationTile,
+	BattleHex attackerPos,
+	BattleHex defenderPos) const
 {
 	//does not return hex attacked directly
 	AttackableTiles at;
 	RETURN_IF_NOT_BATTLE(at);
 
 	BattleHex attackOriginHex = (attackerPos != BattleHex::INVALID) ? attackerPos : attacker->getPosition(); //real or hypothetical (cursor) position
-
-	const auto * defender = battleGetUnitByPos(destinationTile, true);
-	if (!defender)
-		return at; // can't attack thin air
-
-	bool reverse = isToReverse(attacker, defender);
+	
+	defenderPos = (defenderPos != BattleHex::INVALID) ? defenderPos : defender->getPosition(); //real or hypothetical (cursor) position
+	
+	bool reverse = isToReverse(attacker, defender, attackerPos, defenderPos);
 	if(reverse && attacker->doubleWide())
 	{
 		attackOriginHex = attacker->occupiedHex(attackOriginHex); //the other hex stack stands on
@@ -1304,19 +1325,26 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(const battle:
 	else if(attacker->hasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH))
 	{
 		auto direction = BattleHex::mutualPosition(attackOriginHex, destinationTile);
+		
+		if(direction == BattleHex::NONE
+			&& defender->doubleWide()
+			&& attacker->doubleWide()
+			&& defenderPos == destinationTile)
+		{
+			direction = BattleHex::mutualPosition(attackOriginHex, defender->occupiedHex(defenderPos));
+		}
+
 		if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation
 		{
 			BattleHex nextHex = destinationTile.cloneInDirection(direction, false);
 
 			if ( defender->doubleWide() )
 			{
-				auto secondHex = destinationTile == defender->getPosition() ?
-					defender->occupiedHex():
-					defender->getPosition();
+				auto secondHex = destinationTile == defenderPos ? defender->occupiedHex(defenderPos) : defenderPos;
 
 				// if targeted double-wide creature is attacked from above or below ( -> second hex is also adjacent to attack origin)
 				// then dragon breath should target tile on the opposite side of targeted creature
-				if (BattleHex::mutualPosition(attackOriginHex, secondHex) != BattleHex::NONE)
+				if(BattleHex::mutualPosition(attackOriginHex, secondHex) != BattleHex::NONE)
 					nextHex = secondHex.cloneInDirection(direction, false);
 			}
 
@@ -1348,17 +1376,29 @@ AttackableTiles CBattleInfoCallback::getPotentiallyShootableHexes(const battle::
 	return at;
 }
 
-std::vector<const battle::Unit*> CBattleInfoCallback::getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const
+std::vector<const battle::Unit*> CBattleInfoCallback::getAttackedBattleUnits(
+	const battle::Unit * attacker,
+	const  battle::Unit * defender,
+	BattleHex destinationTile,
+	bool rangedAttack,
+	BattleHex attackerPos,
+	BattleHex defenderPos) const
 {
 	std::vector<const battle::Unit*> units;
 	RETURN_IF_NOT_BATTLE(units);
 
+	if(attackerPos == BattleHex::INVALID)
+		attackerPos = attacker->getPosition();
+
+	if(defenderPos == BattleHex::INVALID)
+		defenderPos = defender->getPosition();
+
 	AttackableTiles at;
 
 	if (rangedAttack)
 		at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos);
 	else
-		at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos);
+		at = getPotentiallyAttackableHexes(attacker, defender, destinationTile, attackerPos, defenderPos);
 
 	units = battleGetUnitsIf([=](const battle::Unit * unit)
 	{
@@ -1384,7 +1424,7 @@ std::set<const CStack*> CBattleInfoCallback::getAttackedCreatures(const CStack*
 	RETURN_IF_NOT_BATTLE(attackedCres);
 
 	AttackableTiles at;
-
+	
 	if(rangedAttack)
 		at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos);
 	else
@@ -1423,10 +1463,13 @@ static bool isHexInFront(BattleHex hex, BattleHex testHex, BattleSide::Type side
 }
 
 //TODO: this should apply also to mechanics and cursor interface
-bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battle::Unit * defender) const
+bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerHex, BattleHex defenderHex) const
 {
-	BattleHex attackerHex = attacker->getPosition();
-	BattleHex defenderHex = defender->getPosition();
+	if(!defenderHex.isValid())
+		defenderHex = defender->getPosition();
+
+	if(!attackerHex.isValid())
+		attackerHex = attacker->getPosition();
 
 	if (attackerHex < 0 ) //turret
 		return false;
@@ -1434,15 +1477,22 @@ bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battl
 	if(isHexInFront(attackerHex, defenderHex, static_cast<BattleSide::Type>(attacker->unitSide())))
 		return false;
 
+	auto defenderOtherHex = defenderHex;
+	auto attackerOtherHex = defenderHex;
+
 	if (defender->doubleWide())
 	{
-		if(isHexInFront(attackerHex, defender->occupiedHex(), static_cast<BattleSide::Type>(attacker->unitSide())))
+		defenderOtherHex = battle::Unit::occupiedHex(defenderHex, true, defender->unitSide());
+
+		if(isHexInFront(attackerHex, defenderOtherHex, static_cast<BattleSide::Type>(attacker->unitSide())))
 			return false;
 	}
 
 	if (attacker->doubleWide())
 	{
-		if(isHexInFront(attacker->occupiedHex(), defenderHex, static_cast<BattleSide::Type>(attacker->unitSide())))
+		attackerOtherHex = battle::Unit::occupiedHex(attackerHex, true, attacker->unitSide());
+
+		if(isHexInFront(attackerOtherHex, defenderHex, static_cast<BattleSide::Type>(attacker->unitSide())))
 			return false;
 	}
 
@@ -1450,7 +1500,7 @@ bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battl
 	// but this is how H3 handles it which is important, e.g. for direction of dragon breath attacks
 	if (attacker->doubleWide() && defender->doubleWide())
 	{
-		if(isHexInFront(attacker->occupiedHex(), defender->occupiedHex(), static_cast<BattleSide::Type>(attacker->unitSide())))
+		if(isHexInFront(attackerOtherHex, defenderOtherHex, static_cast<BattleSide::Type>(attacker->unitSide())))
 			return false;
 	}
 	return true;

+ 22 - 3
lib/battle/CBattleInfoCallback.h

@@ -131,11 +131,30 @@ public:
 	bool isInTacticRange(BattleHex dest) const;
 	si8 battleGetTacticDist() const; //returns tactic distance for calling player or 0 if this player is not in tactic phase (for ALL_KNOWING actual distance for tactic side)
 
-	AttackableTiles getPotentiallyAttackableHexes(const  battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; //TODO: apply rotation to two-hex attacker
+	AttackableTiles getPotentiallyAttackableHexes(
+		const  battle::Unit* attacker,
+		const  battle::Unit* defender,
+		BattleHex destinationTile,
+		BattleHex attackerPos,
+		BattleHex defenderPos) const; //TODO: apply rotation to two-hex attacker
+
+	AttackableTiles getPotentiallyAttackableHexes(
+		const  battle::Unit * attacker,
+		BattleHex destinationTile,
+		BattleHex attackerPos) const;
+
 	AttackableTiles getPotentiallyShootableHexes(const  battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const;
-	std::vector<const battle::Unit *> getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks
+
+	std::vector<const battle::Unit *> getAttackedBattleUnits(
+		const battle::Unit* attacker,
+		const  battle::Unit * defender,
+		BattleHex destinationTile,
+		bool rangedAttack,
+		BattleHex attackerPos = BattleHex::INVALID,
+		BattleHex defenderPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks
+	
 	std::set<const CStack*> getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks
-	bool isToReverse(const battle::Unit * attacker, const battle::Unit * defender) const; //determines if attacker standing at attackerHex should reverse in order to attack defender
+	bool isToReverse(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerHex = BattleHex::INVALID, BattleHex defenderHex = BattleHex::INVALID) const; //determines if attacker standing at attackerHex should reverse in order to attack defender
 
 	ReachabilityInfo getReachability(const battle::Unit * unit) const;
 	ReachabilityInfo getReachability(const ReachabilityInfo::Parameters & params) const;

+ 7 - 146
lib/gameState/CGameState.cpp

@@ -875,7 +875,7 @@ void CGameState::initTowns()
 		}
 		//init spells
 		vti->spells.resize(GameConstants::SPELL_LEVELS);
-
+		vti->possibleSpells -= SpellID::PRESET;
 		for(ui32 z=0; z<vti->obligatorySpells.size();z++)
 		{
 			const auto * s = vti->obligatorySpells[z].toSpell();
@@ -1538,137 +1538,6 @@ bool CGameState::checkForStandardLoss(const PlayerColor & player) const
 	return pState.checkVanquished();
 }
 
-struct statsHLP
-{
-	using TStat = std::pair<PlayerColor, si64>;
-	//converts [<player's color, value>] to vec[place] -> platers
-	static std::vector< std::vector< PlayerColor > > getRank( std::vector<TStat> stats )
-	{
-		std::sort(stats.begin(), stats.end(), statsHLP());
-
-		//put first element
-		std::vector< std::vector<PlayerColor> > ret;
-		std::vector<PlayerColor> tmp;
-		tmp.push_back( stats[0].first );
-		ret.push_back( tmp );
-
-		//the rest of elements
-		for(int g=1; g<stats.size(); ++g)
-		{
-			if(stats[g].second == stats[g-1].second)
-			{
-				(ret.end()-1)->push_back( stats[g].first );
-			}
-			else
-			{
-				//create next occupied rank
-				std::vector<PlayerColor> tmp;
-				tmp.push_back(stats[g].first);
-				ret.push_back(tmp);
-			}
-		}
-
-		return ret;
-	}
-
-	bool operator()(const TStat & a, const TStat & b) const
-	{
-		return a.second > b.second;
-	}
-
-	static const CGHeroInstance * findBestHero(CGameState * gs, const PlayerColor & color)
-	{
-		std::vector<ConstTransitivePtr<CGHeroInstance> > &h = gs->players[color].heroes;
-		if(h.empty())
-			return nullptr;
-		//best hero will be that with highest exp
-		int best = 0;
-		for(int b=1; b<h.size(); ++b)
-		{
-			if(h[b]->exp > h[best]->exp)
-			{
-				best = b;
-			}
-		}
-		return h[best];
-	}
-
-	//calculates total number of artifacts that belong to given player
-	static int getNumberOfArts(const PlayerState * ps)
-	{
-		int ret = 0;
-		for(auto h : ps->heroes)
-		{
-			ret += (int)h->artifactsInBackpack.size() + (int)h->artifactsWorn.size();
-		}
-		return ret;
-	}
-
-	// get total strength of player army
-	static si64 getArmyStrength(const PlayerState * ps)
-	{
-		si64 str = 0;
-
-		for(auto h : ps->heroes)
-		{
-			if(!h->inTownGarrison)		//original h3 behavior
-				str += h->getArmyStrength();
-		}
-		return str;
-	}
-
-	// get total gold income
-	static int getIncome(const PlayerState * ps, int percentIncome)
-	{
-		int totalIncome = 0;
-		const CGObjectInstance * heroOrTown = nullptr;
-
-		//Heroes can produce gold as well - skill, specialty or arts
-		for(const auto & h : ps->heroes)
-		{
-			totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))) * percentIncome / 100;
-
-			if(!heroOrTown)
-				heroOrTown = h;
-		}
-
-		//Add town income of all towns
-		for(const auto & t : ps->towns)
-		{
-			totalIncome += t->dailyIncome()[EGameResID::GOLD];
-
-			if(!heroOrTown)
-				heroOrTown = t;
-		}
-
-		/// FIXME: Dirty dirty hack
-		/// Stats helper need some access to gamestate.
-		std::vector<const CGObjectInstance *> ownedObjects;
-		for(const CGObjectInstance * obj : heroOrTown->cb->gameState()->map->objects)
-		{
-			if(obj && obj->tempOwner == ps->color)
-				ownedObjects.push_back(obj);
-		}
-		/// This is code from CPlayerSpecificInfoCallback::getMyObjects
-		/// I'm really need to find out about callback interface design...
-
-		for(const auto * object : ownedObjects)
-		{
-			//Mines
-			if ( object->ID == Obj::MINE )
-			{
-				const auto * mine = dynamic_cast<const CGMine *>(object);
-				assert(mine);
-
-				if (mine->producedResource == EGameResID::GOLD)
-					totalIncome += mine->getProducedQuantity();
-			}
-		}
-
-		return totalIncome;
-	}
-};
-
 void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
 {
 	auto playerInactive = [&](const PlayerColor & color) 
@@ -1688,7 +1557,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
 			stat.second = VAL_GETTER; \
 			stats.push_back(stat); \
 		} \
-		tgi.FIELD = statsHLP::getRank(stats); \
+		tgi.FIELD = Statistic::getRank(stats); \
 	}
 
 	for(auto & elem : players)
@@ -1710,7 +1579,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
 		{
 			if(playerInactive(player.second.color))
 				continue;
-			const CGHeroInstance * best = statsHLP::findBestHero(this, player.second.color);
+			const CGHeroInstance * best = Statistic::findBestHero(this, player.second.color);
 			InfoAboutHero iah;
 			iah.initFromHero(best, (level >= 2) ? InfoAboutHero::EInfoLevel::DETAILED : InfoAboutHero::EInfoLevel::BASIC);
 			iah.army.clear();
@@ -1731,27 +1600,19 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
 	}
 	if(level >= 3) //obelisks found
 	{
-		auto getObeliskVisited = [&](const TeamID & t)
-		{
-			if(map->obelisksVisited.count(t))
-				return map->obelisksVisited[t];
-			else
-				return ui8(0);
-		};
-
-		FILL_FIELD(obelisks, getObeliskVisited(gs->getPlayerTeam(g->second.color)->id))
+		FILL_FIELD(obelisks, Statistic::getObeliskVisited(gs, gs->getPlayerTeam(g->second.color)->id))
 	}
 	if(level >= 4) //artifacts
 	{
-		FILL_FIELD(artifacts, statsHLP::getNumberOfArts(&g->second))
+		FILL_FIELD(artifacts, Statistic::getNumberOfArts(&g->second))
 	}
 	if(level >= 4) //army strength
 	{
-		FILL_FIELD(army, statsHLP::getArmyStrength(&g->second))
+		FILL_FIELD(army, Statistic::getArmyStrength(&g->second))
 	}
 	if(level >= 5) //income
 	{
-		FILL_FIELD(income, statsHLP::getIncome(&g->second, scenarioOps->getIthPlayersSettings(g->second.color).handicap.percentIncome))
+		FILL_FIELD(income, Statistic::getIncome(gs, &g->second))
 	}
 	if(level >= 2) //best hero's stats
 	{

+ 5 - 0
lib/gameState/CGameState.h

@@ -15,6 +15,7 @@
 #include "../ConstTransitivePtr.h"
 
 #include "RumorState.h"
+#include "GameStatistics.h"
 
 namespace boost
 {
@@ -90,6 +91,8 @@ public:
 	CBonusSystemNode globalEffects;
 	RumorState currentRumor;
 
+	StatisticDataSet statistic;
+
 	static boost::shared_mutex mutex;
 
 	void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override;
@@ -167,6 +170,8 @@ public:
 		h & currentRumor;
 		h & campaign;
 		h & allocatedArtifacts;
+		if (h.version >= Handler::Version::STATISTICS)
+			h & statistic;
 
 		BONUS_TREE_DESERIALIZATION_FIX
 	}

+ 370 - 0
lib/gameState/GameStatistics.cpp

@@ -0,0 +1,370 @@
+/*
+ * GameStatistics.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "GameStatistics.h"
+#include "../CPlayerState.h"
+#include "../constants/StringConstants.h"
+#include "CGameState.h"
+#include "TerrainHandler.h"
+#include "CHeroHandler.h"
+#include "StartInfo.h"
+#include "HighScore.h"
+#include "../mapObjects/CGHeroInstance.h"
+#include "../mapObjects/CGTownInstance.h"
+#include "../mapObjects/CGObjectInstance.h"
+#include "../mapObjects/MiscObjects.h"
+#include "../mapping/CMap.h"
+#include "../entities/building/CBuilding.h"
+
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+void StatisticDataSet::add(StatisticDataSetEntry entry)
+{
+	data.push_back(entry);
+}
+
+StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, const CGameState * gs)
+{
+	StatisticDataSetEntry data;
+
+	HighScoreParameter param = HighScore::prepareHighScores(gs, ps->color, false);
+	HighScoreCalculation scenarioHighScores;
+	scenarioHighScores.parameters.push_back(param);
+	scenarioHighScores.isCampaign = false;
+
+	data.map = gs->map->name.toString();
+	data.timestamp = std::time(0);
+	data.day = gs->getDate(Date::DAY);
+	data.player = ps->color;
+	data.team = ps->team;
+	data.isHuman = ps->isHuman();
+	data.status = ps->status;
+	data.resources = ps->resources;
+	data.numberHeroes = ps->heroes.size();
+	data.numberTowns = gs->howManyTowns(ps->color);
+	data.numberArtifacts = Statistic::getNumberOfArts(ps);
+	data.numberDwellings = gs->getPlayerState(ps->color)->dwellings.size();
+	data.armyStrength = Statistic::getArmyStrength(ps, true);
+	data.totalExperience = Statistic::getTotalExperience(ps);
+	data.income = Statistic::getIncome(gs, ps);
+	data.mapExploredRatio = Statistic::getMapExploredRatio(gs, ps->color);
+	data.obeliskVisitedRatio = Statistic::getObeliskVisitedRatio(gs, ps->team);
+	data.townBuiltRatio = Statistic::getTownBuiltRatio(ps);
+	data.hasGrail = param.hasGrail;
+	data.numMines = Statistic::getNumMines(gs, ps);
+	data.score = scenarioHighScores.calculate().total;
+	data.maxHeroLevel = Statistic::findBestHero(gs, ps->color) ? Statistic::findBestHero(gs, ps->color)->level : 0;
+	data.numBattlesNeutral = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numBattlesNeutral : 0;
+	data.numBattlesPlayer = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numBattlesPlayer : 0;
+	data.numWinBattlesNeutral = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numWinBattlesNeutral : 0;
+	data.numWinBattlesPlayer = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numWinBattlesPlayer : 0;
+	data.numHeroSurrendered = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numHeroSurrendered : 0;
+	data.numHeroEscaped = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numHeroEscaped : 0;
+	data.spentResourcesForArmy = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources();
+	data.spentResourcesForBuildings = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources();
+	data.tradeVolume = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).tradeVolume : TResources();
+	data.movementPointsUsed = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).movementPointsUsed : 0;
+
+	return data;
+}
+
+std::string StatisticDataSet::toCsv()
+{
+	std::stringstream ss;
+
+	auto resources = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS};
+
+	ss << "Map" << ";";
+	ss << "Timestamp" << ";";
+	ss << "Day" << ";";
+	ss << "Player" << ";";
+	ss << "Team" << ";";
+	ss << "IsHuman" << ";";
+	ss << "Status" << ";";
+	ss << "NumberHeroes" << ";";
+	ss << "NumberTowns" << ";";
+	ss << "NumberArtifacts" << ";";
+	ss << "NumberDwellings" << ";";
+	ss << "ArmyStrength" << ";";
+	ss << "TotalExperience" << ";";
+	ss << "Income" << ";";
+	ss << "MapExploredRatio" << ";";
+	ss << "ObeliskVisitedRatio" << ";";
+	ss << "TownBuiltRatio" << ";";
+	ss << "HasGrail" << ";";
+	ss << "Score" << ";";
+	ss << "MaxHeroLevel" << ";";
+	ss << "NumBattlesNeutral" << ";";
+	ss << "NumBattlesPlayer" << ";";
+	ss << "NumWinBattlesNeutral" << ";";
+	ss << "NumWinBattlesPlayer" << ";";
+	ss << "NumHeroSurrendered" << ";";
+	ss << "NumHeroEscaped" << ";";
+	ss << "MovementPointsUsed";
+	for(auto & resource : resources)
+		ss << ";" << GameConstants::RESOURCE_NAMES[resource];
+	for(auto & resource : resources)
+		ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "Mines";
+	for(auto & resource : resources)
+		ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForArmy";
+	for(auto & resource : resources)
+		ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForBuildings";
+	for(auto & resource : resources)
+		ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "TradeVolume";
+	ss << "\r\n";
+
+	for(auto & entry : data)
+	{
+		ss << entry.map << ";";
+		ss << vstd::getFormattedDateTime(entry.timestamp, "%Y-%m-%dT%H:%M:%S") << ";";
+		ss << entry.day << ";";
+		ss << GameConstants::PLAYER_COLOR_NAMES[entry.player] << ";";
+		ss << entry.team.getNum() << ";";
+		ss << entry.isHuman << ";";
+		ss << (int)entry.status << ";";
+		ss << entry.numberHeroes << ";";
+		ss << entry.numberTowns <<  ";";
+		ss << entry.numberArtifacts << ";";
+		ss << entry.numberDwellings << ";";
+		ss << entry.armyStrength << ";";
+		ss << entry.totalExperience << ";";
+		ss << entry.income << ";";
+		ss << entry.mapExploredRatio << ";";
+		ss << entry.obeliskVisitedRatio << ";";
+		ss << entry.townBuiltRatio << ";";
+		ss << entry.hasGrail << ";";
+		ss << entry.score << ";";
+		ss << entry.maxHeroLevel << ";";
+		ss << entry.numBattlesNeutral << ";";
+		ss << entry.numBattlesPlayer << ";";
+		ss << entry.numWinBattlesNeutral << ";";
+		ss << entry.numWinBattlesPlayer << ";";
+		ss << entry.numHeroSurrendered << ";";
+		ss << entry.numHeroEscaped << ";";
+		ss << entry.movementPointsUsed;
+		for(auto & resource : resources)
+			ss << ";" << entry.resources[resource];
+		for(auto & resource : resources)
+			ss << ";" << entry.numMines[resource];
+		for(auto & resource : resources)
+			ss << ";" << entry.spentResourcesForArmy[resource];
+		for(auto & resource : resources)
+			ss << ";" << entry.spentResourcesForBuildings[resource];
+		for(auto & resource : resources)
+			ss << ";" << entry.tradeVolume[resource];
+		ss << "\r\n";
+	}
+
+	return ss.str();
+}
+
+std::vector<const CGMine *> Statistic::getMines(const CGameState * gs, const PlayerState * ps)
+{
+	std::vector<const CGMine *> tmp;
+
+	/// FIXME: Dirty dirty hack
+	/// Stats helper need some access to gamestate.
+	std::vector<const CGObjectInstance *> ownedObjects;
+	for(const CGObjectInstance * obj : gs->map->objects)
+	{
+		if(obj && obj->tempOwner == ps->color)
+			ownedObjects.push_back(obj);
+	}
+	/// This is code from CPlayerSpecificInfoCallback::getMyObjects
+	/// I'm really need to find out about callback interface design...
+
+	for(const auto * object : ownedObjects)
+	{
+		//Mines
+		if ( object->ID == Obj::MINE )
+		{
+			const auto * mine = dynamic_cast<const CGMine *>(object);
+			assert(mine);
+
+			tmp.push_back(mine);
+		}
+	}
+
+	return tmp;
+}
+
+//calculates total number of artifacts that belong to given player
+int Statistic::getNumberOfArts(const PlayerState * ps)
+{
+	int ret = 0;
+	for(auto h : ps->heroes)
+	{
+		ret += (int)h->artifactsInBackpack.size() + (int)h->artifactsWorn.size();
+	}
+	return ret;
+}
+
+// get total strength of player army
+si64 Statistic::getArmyStrength(const PlayerState * ps, bool withTownGarrison)
+{
+	si64 str = 0;
+
+	for(auto h : ps->heroes)
+	{
+		if(!h->inTownGarrison || withTownGarrison)		//original h3 behavior
+			str += h->getArmyStrength();
+	}
+	return str;
+}
+
+// get total experience of all heroes
+si64 Statistic::getTotalExperience(const PlayerState * ps)
+{
+	si64 tmp = 0;
+
+	for(auto h : ps->heroes)
+		tmp += h->exp;
+	
+	return tmp;
+}
+
+// get total gold income
+int Statistic::getIncome(const CGameState * gs, const PlayerState * ps)
+{
+	int percentIncome = gs->getStartInfo()->getIthPlayersSettings(ps->color).handicap.percentIncome;
+	int totalIncome = 0;
+
+	//Heroes can produce gold as well - skill, specialty or arts
+	for(const auto & h : ps->heroes)
+		totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))) * percentIncome / 100;
+
+	//Add town income of all towns
+	for(const auto & t : ps->towns)
+		totalIncome += t->dailyIncome()[EGameResID::GOLD];
+
+	for(const CGMine * mine : getMines(gs, ps))
+		if(mine->producedResource == EGameResID::GOLD)
+			totalIncome += mine->getProducedQuantity();
+
+	return totalIncome;
+}
+
+float Statistic::getMapExploredRatio(const CGameState * gs, PlayerColor player)
+{
+	float visible = 0.0;
+	float numTiles = 0.0;
+
+	for(int layer = 0; layer < (gs->map->twoLevel ? 2 : 1); layer++)
+		for(int y = 0; y < gs->map->height; ++y)
+			for(int x = 0; x < gs->map->width; ++x)
+			{
+				TerrainTile tile = gs->map->getTile(int3(x, y, layer));
+
+				if(tile.blocked && (!tile.visitable))
+					continue;
+
+				if(gs->isVisible(int3(x, y, layer), player))
+					visible++;
+				numTiles++;
+			}
+	
+	return visible / numTiles;
+}
+
+const CGHeroInstance * Statistic::findBestHero(const CGameState * gs, const PlayerColor & color)
+{
+	auto &h = gs->players.at(color).heroes;
+	if(h.empty())
+		return nullptr;
+	//best hero will be that with highest exp
+	int best = 0;
+	for(int b=1; b<h.size(); ++b)
+	{
+		if(h[b]->exp > h[best]->exp)
+		{
+			best = b;
+		}
+	}
+	return h[best];
+}
+
+std::vector<std::vector<PlayerColor>> Statistic::getRank(std::vector<std::pair<PlayerColor, si64>> stats)
+{
+	std::sort(stats.begin(), stats.end(), [](const std::pair<PlayerColor, si64> & a, const std::pair<PlayerColor, si64> & b) { return a.second > b.second; });
+
+	//put first element
+	std::vector< std::vector<PlayerColor> > ret;
+	std::vector<PlayerColor> tmp;
+	tmp.push_back( stats[0].first );
+	ret.push_back( tmp );
+
+	//the rest of elements
+	for(int g=1; g<stats.size(); ++g)
+	{
+		if(stats[g].second == stats[g-1].second)
+		{
+			(ret.end()-1)->push_back( stats[g].first );
+		}
+		else
+		{
+			//create next occupied rank
+			std::vector<PlayerColor> tmp;
+			tmp.push_back(stats[g].first);
+			ret.push_back(tmp);
+		}
+	}
+
+	return ret;
+}
+
+int Statistic::getObeliskVisited(const CGameState * gs, const TeamID & t)
+{
+	if(gs->map->obelisksVisited.count(t))
+		return gs->map->obelisksVisited.at(t);
+	else
+		return 0;
+}
+
+float Statistic::getObeliskVisitedRatio(const CGameState * gs, const TeamID & t)
+{
+	if(!gs->map->obeliskCount)
+		return 0;
+	return (float)getObeliskVisited(gs, t) / (float)gs->map->obeliskCount;
+}
+
+std::map<EGameResID, int> Statistic::getNumMines(const CGameState * gs, const PlayerState * ps)
+{
+	std::map<EGameResID, int> tmp;
+
+	for(auto & res : EGameResID::ALL_RESOURCES())
+		tmp[res] = 0;
+
+	for(const CGMine * mine : getMines(gs, ps))
+		tmp[mine->producedResource]++;
+	
+	return tmp;
+}
+
+float Statistic::getTownBuiltRatio(const PlayerState * ps)
+{
+	float built = 0.0;
+	float total = 0.0;
+
+	for(const auto & t : ps->towns)
+	{
+		built += t->builtBuildings.size();
+		for(const auto & b : t->town->buildings)
+			if(!t->forbiddenBuildings.count(b.first))
+				total += 1;
+	}
+
+	if(total < 1)
+		return 0;
+	
+	return built / total;
+}
+
+VCMI_LIB_NAMESPACE_END

+ 156 - 0
lib/gameState/GameStatistics.h

@@ -0,0 +1,156 @@
+/*
+ * GameSTatistics.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../GameConstants.h"
+#include "../ResourceSet.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct PlayerState;
+class CGameState;
+class CGHeroInstance;
+class CGMine;
+
+struct DLL_LINKAGE StatisticDataSetEntry
+{
+	std::string map;
+	time_t timestamp;
+    int day;
+    PlayerColor player;
+	TeamID team;
+	bool isHuman;
+	EPlayerStatus status;
+	TResources resources;
+	int numberHeroes;
+	int numberTowns;
+	int numberArtifacts;
+	int numberDwellings;
+	si64 armyStrength;
+	si64 totalExperience;
+	int income;
+	float mapExploredRatio;
+	float obeliskVisitedRatio;
+	float townBuiltRatio;
+	bool hasGrail;
+	std::map<EGameResID, int> numMines;
+	int score;
+	int maxHeroLevel;
+	int numBattlesNeutral;
+	int numBattlesPlayer;
+	int numWinBattlesNeutral;
+	int numWinBattlesPlayer;
+	int numHeroSurrendered;
+	int numHeroEscaped;
+	TResources spentResourcesForArmy;
+	TResources spentResourcesForBuildings;
+	TResources tradeVolume;
+	si64 movementPointsUsed;
+
+	template <typename Handler> void serialize(Handler &h)
+	{
+		h & map;
+		h & timestamp;
+		h & day;
+		h & player;
+		h & team;
+		h & isHuman;
+		h & status;
+		h & resources;
+		h & numberHeroes;
+		h & numberTowns;
+		h & numberArtifacts;
+		h & numberDwellings;
+		h & armyStrength;
+		h & totalExperience;
+		h & income;
+		h & mapExploredRatio;
+		h & obeliskVisitedRatio;
+		h & townBuiltRatio;
+		h & hasGrail;
+		h & numMines;
+		h & score;
+		h & maxHeroLevel;
+		h & numBattlesNeutral;
+		h & numBattlesPlayer;
+		h & numWinBattlesNeutral;
+		h & numWinBattlesPlayer;
+		h & numHeroSurrendered;
+		h & numHeroEscaped;
+		h & spentResourcesForArmy;
+		h & spentResourcesForBuildings;
+		h & tradeVolume;
+		h & movementPointsUsed;
+	}
+};
+
+class DLL_LINKAGE StatisticDataSet
+{
+    std::vector<StatisticDataSetEntry> data;
+
+public:
+    void add(StatisticDataSetEntry entry);
+	static StatisticDataSetEntry createEntry(const PlayerState * ps, const CGameState * gs);
+    std::string toCsv();
+
+	struct PlayerAccumulatedValueStorage // holds some actual values needed for stats
+	{
+		int numBattlesNeutral;
+		int numBattlesPlayer;
+		int numWinBattlesNeutral;
+		int numWinBattlesPlayer;
+		int numHeroSurrendered;
+		int numHeroEscaped;
+		TResources spentResourcesForArmy;
+		TResources spentResourcesForBuildings;
+		TResources tradeVolume;
+		si64 movementPointsUsed;
+
+		template <typename Handler> void serialize(Handler &h)
+		{
+			h & numBattlesNeutral;
+			h & numBattlesPlayer;
+			h & numWinBattlesNeutral;
+			h & numWinBattlesPlayer;
+			h & numHeroSurrendered;
+			h & numHeroEscaped;
+			h & spentResourcesForArmy;
+			h & spentResourcesForBuildings;
+			h & tradeVolume;
+			h & movementPointsUsed;
+		}
+	};
+	std::map<PlayerColor, PlayerAccumulatedValueStorage> accumulatedValues;
+
+	template <typename Handler> void serialize(Handler &h)
+	{
+		h & data;
+		h & accumulatedValues;
+	}
+};
+
+class DLL_LINKAGE Statistic
+{
+	static std::vector<const CGMine *> getMines(const CGameState * gs, const PlayerState * ps);
+public:
+	static int getNumberOfArts(const PlayerState * ps);
+	static si64 getArmyStrength(const PlayerState * ps, bool withTownGarrison = false);
+	static si64 getTotalExperience(const PlayerState * ps);
+	static int getIncome(const CGameState * gs, const PlayerState * ps);
+	static float getMapExploredRatio(const CGameState * gs, PlayerColor player);
+	static const CGHeroInstance * findBestHero(const CGameState * gs, const PlayerColor & color);
+	static std::vector<std::vector<PlayerColor>> getRank(std::vector<std::pair<PlayerColor, si64>> stats);
+	static int getObeliskVisited(const CGameState * gs, const TeamID & t);
+	static float getObeliskVisitedRatio(const CGameState * gs, const TeamID & t);
+	static std::map<EGameResID, int> getNumMines(const CGameState * gs, const PlayerState * ps);
+	static float getTownBuiltRatio(const PlayerState * ps);
+};
+
+VCMI_LIB_NAMESPACE_END

+ 111 - 0
lib/gameState/HighScore.cpp

@@ -0,0 +1,111 @@
+/*
+ * HighScore.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "HighScore.h"
+#include "../CPlayerState.h"
+#include "../constants/StringConstants.h"
+#include "CGameState.h"
+#include "StartInfo.h"
+#include "../mapping/CMapHeader.h"
+#include "../mapObjects/CGHeroInstance.h"
+#include "../mapObjects/CGTownInstance.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+HighScoreParameter HighScore::prepareHighScores(const CGameState * gs, PlayerColor player, bool victory)
+{
+	const auto * playerState = gs->getPlayerState(player);
+
+	HighScoreParameter param;
+	param.difficulty = gs->getStartInfo()->difficulty;
+	param.day = gs->getDate();
+	param.townAmount = gs->howManyTowns(player);
+	param.usedCheat = gs->getPlayerState(player)->cheated;
+	param.hasGrail = false;
+	for(const CGHeroInstance * h : playerState->heroes)
+		if(h->hasArt(ArtifactID::GRAIL))
+			param.hasGrail = true;
+	for(const CGTownInstance * t : playerState->towns)
+		if(t->builtBuildings.count(BuildingID::GRAIL))
+			param.hasGrail = true;
+	param.allEnemiesDefeated = true;
+	for (PlayerColor otherPlayer(0); otherPlayer < PlayerColor::PLAYER_LIMIT; ++otherPlayer)
+	{
+		auto ps = gs->getPlayerState(otherPlayer, false);
+		if(ps && otherPlayer != player && !ps->checkVanquished())
+			param.allEnemiesDefeated = false;
+	}
+	param.scenarioName = gs->getMapHeader()->name.toString();
+	param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name;
+
+	return param;
+}
+
+HighScoreCalculation::Result HighScoreCalculation::calculate()
+{
+	Result firstResult;
+	Result summary;
+	const std::array<double, 5> difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0}; 
+	for(auto & param : parameters)
+	{
+		double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allEnemiesDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0);
+		firstResult = Result{static_cast<int>(tmp), static_cast<int>(tmp * difficultyMultipliers.at(param.difficulty)), param.day, param.usedCheat};
+		summary.basic += firstResult.basic * 5.0 / parameters.size();
+		summary.total += firstResult.total * 5.0 / parameters.size();
+		summary.sumDays += firstResult.sumDays;
+		summary.cheater |= firstResult.cheater;
+	}
+
+	if(parameters.size() == 1)
+		return firstResult;
+
+	return summary;
+}
+
+struct HighScoreCreature
+{
+	CreatureID creature;
+	int min;
+	int max;
+};
+
+static std::vector<HighScoreCreature> getHighscoreCreaturesList()
+{
+	JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json"));
+
+	std::vector<HighScoreCreature> ret;
+
+	for(auto & json : configCreatures["creatures"].Vector())
+	{
+		HighScoreCreature entry;
+		entry.creature = CreatureID::decode(json["creature"].String());
+		entry.max = json["max"].isNull() ? std::numeric_limits<int>::max() : json["max"].Integer();
+		entry.min = json["min"].isNull() ? std::numeric_limits<int>::min() : json["min"].Integer();
+
+		ret.push_back(entry);
+	}
+
+	return ret;
+}
+
+CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign)
+{
+	static const std::vector<HighScoreCreature> creatures = getHighscoreCreaturesList();
+
+	int divide = campaign ? 5 : 1;
+
+	for(auto & creature : creatures)
+		if(points / divide <= creature.max && points / divide >= creature.min)
+			return creature.creature;
+
+	throw std::runtime_error("Unable to find creature for score " + std::to_string(points));
+}
+
+VCMI_LIB_NAMESPACE_END

+ 27 - 0
lib/gameState/HighScore.h

@@ -9,8 +9,12 @@
  */
 #pragma once
 
+#include "../GameConstants.h"
+
 VCMI_LIB_NAMESPACE_BEGIN
 
+class CGameState;
+
 class DLL_LINKAGE HighScoreParameter
 {
 public:
@@ -37,5 +41,28 @@ public:
 		h & playerName;
 	}
 };
+class DLL_LINKAGE HighScore
+{
+public:
+	static HighScoreParameter prepareHighScores(const CGameState * gs, PlayerColor player, bool victory);
+};
+
+class DLL_LINKAGE HighScoreCalculation
+{
+public:
+	struct Result
+	{
+		int basic = 0;
+		int total = 0;
+		int sumDays = 0;
+		bool cheater = false;
+	};
+
+	std::vector<HighScoreParameter> parameters;
+	bool isCampaign = false;
+
+	Result calculate();
+	static CreatureID getCreatureForPoints(int points, bool campaign);
+};
 
 VCMI_LIB_NAMESPACE_END

+ 1 - 1
lib/mapObjects/CBank.cpp

@@ -393,7 +393,7 @@ void CBank::battleFinished(const CGHeroInstance *hero, const BattleResult &resul
 	}
 }
 
-void CBank::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CBank::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	if (answer)
 	{

+ 1 - 1
lib/mapObjects/CBank.h

@@ -40,7 +40,7 @@ public:
 	bool isCoastVisitable() const override;
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 
 	std::vector<Component> getPopupComponents(PlayerColor player) const override;
 

+ 1 - 1
lib/mapObjects/CGCreature.cpp

@@ -523,7 +523,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult &
 	}
 }
 
-void CGCreature::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CGCreature::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	auto action = takenAction(hero);
 	if(!refusedJoining && action >= JOIN_FOR_FREE) //higher means price

+ 1 - 1
lib/mapObjects/CGCreature.h

@@ -48,7 +48,7 @@ public:
 	void pickRandomObject(vstd::RNG & rand) override;
 	void newTurn(vstd::RNG & rand) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 	CreatureID getCreature() const;
 
 	//stack formation depends on position,

+ 1 - 1
lib/mapObjects/CGDwelling.cpp

@@ -516,7 +516,7 @@ void CGDwelling::battleFinished(const CGHeroInstance *hero, const BattleResult &
 	}
 }
 
-void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	auto relations = cb->getPlayerRelations(getOwner(), hero->getOwner());
 	if(stacksCount() > 0  && relations == PlayerRelations::ENEMIES) //guards present

+ 1 - 1
lib/mapObjects/CGDwelling.h

@@ -54,7 +54,7 @@ private:
 	void newTurn(vstd::RNG & rand) const override;
 	void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 	std::vector<Component> getPopupComponents(PlayerColor player) const override;
 
 	void updateGuards() const;

+ 1 - 1
lib/mapObjects/CGPandoraBox.cpp

@@ -186,7 +186,7 @@ void CGPandoraBox::battleFinished(const CGHeroInstance *hero, const BattleResult
 	}
 }
 
-void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	if(answer)
 	{

+ 1 - 1
lib/mapObjects/CGPandoraBox.h

@@ -26,7 +26,7 @@ public:
 	void initObj(vstd::RNG & rand) override;
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 
 	template <typename Handler> void serialize(Handler &h)
 	{

+ 1 - 1
lib/mapObjects/CGTownBuilding.cpp

@@ -385,7 +385,7 @@ void CTownRewardableBuilding::heroLevelUpDone(const CGHeroInstance *hero) const
 	grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), town, hero);
 }
 
-void CTownRewardableBuilding::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CTownRewardableBuilding::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	if(answer == 0)
 		return; // player refused

+ 1 - 1
lib/mapObjects/CGTownBuilding.h

@@ -133,7 +133,7 @@ public:
 	void initObj(vstd::RNG & rand) override;
 	
 	/// applies player selection of reward
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 	
 	CTownRewardableBuilding(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * town, vstd::RNG & rand);
 	CTownRewardableBuilding(IGameCallback *cb);

+ 7 - 1
lib/mapObjects/CGTownInstance.cpp

@@ -281,7 +281,7 @@ void CGTownInstance::setOwner(const PlayerColor & player) const
 	cb->setOwner(this, player);
 }
 
-void CGTownInstance::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CGTownInstance::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	for (auto building : bonusingBuildings)
 		building->blockingDialogAnswered(hero, answer);
@@ -1221,6 +1221,12 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler)
 		handler.serializeIdArray( "possibleSpells", possibleSpells);
 		handler.serializeIdArray( "obligatorySpells", obligatorySpells);
 	}
+
+	{
+		auto eventsHandler = handler.enterArray("events");
+		eventsHandler.syncSize(events, JsonNode::JsonType::DATA_VECTOR);
+		eventsHandler.serializeStruct(events);
+	}
 }
 
 FactionID CGTownInstance::getFaction() const

+ 2 - 2
lib/mapObjects/CGTownInstance.h

@@ -65,7 +65,7 @@ public:
 	std::vector<CGTownBuilding*> bonusingBuildings;
 	std::vector<SpellID> possibleSpells, obligatorySpells;
 	std::vector<std::vector<SpellID> > spells; //spells[level] -> vector of spells, first will be available in guild
-	std::list<CCastleEvent> events;
+	std::vector<CCastleEvent> events;
 	std::pair<si32, si32> bonusValue;//var to store town bonuses (rampart = resources from mystic pond);
 
 	//////////////////////////////////////////////////////////////////////////
@@ -223,7 +223,7 @@ public:
 protected:
 	void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 
 private:
 	FactionID randomizeFaction(vstd::RNG & rand);

+ 2 - 2
lib/mapObjects/CQuest.cpp

@@ -660,7 +660,7 @@ const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const
 	return dynamic_cast<const CGCreature *>(o);
 }
 
-void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	CRewardableObject::blockingDialogAnswered(hero, answer);
 	if(answer)
@@ -865,7 +865,7 @@ void CGBorderGuard::onHeroVisit(const CGHeroInstance * h) const
 	}
 }
 
-void CGBorderGuard::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CGBorderGuard::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	if (answer)
 		cb->removeObject(this, hero->getOwner());

+ 2 - 2
lib/mapObjects/CQuest.h

@@ -150,7 +150,7 @@ public:
 	std::vector<Component> getPopupComponents(const CGHeroInstance * hero) const override;
 	void newTurn(vstd::RNG & rand) const override;
 	void onHeroVisit(const CGHeroInstance * h) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 	void getVisitText (MetaString &text, std::vector<Component> &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override;
 
 	virtual void init(vstd::RNG & rand);
@@ -229,7 +229,7 @@ public:
 
 	void initObj(vstd::RNG & rand) override;
 	void onHeroVisit(const CGHeroInstance * h) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 
 	void getVisitText (MetaString &text, std::vector<Component> &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override;
 	void getRolloverText (MetaString &text, bool onHover) const;

+ 1 - 1
lib/mapObjects/CRewardableObject.cpp

@@ -181,7 +181,7 @@ void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const
 	grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), this, hero);
 }
 
-void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	if(answer == 0)
 	{

+ 1 - 1
lib/mapObjects/CRewardableObject.h

@@ -63,7 +63,7 @@ public:
 	void heroLevelUpDone(const CGHeroInstance *hero) const override;
 
 	/// applies player selection of reward
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 
 	void initObj(vstd::RNG & rand) override;
 	

+ 1 - 1
lib/mapObjects/IObjectInterface.cpp

@@ -68,7 +68,7 @@ void IObjectInterface::preInit()
 void IObjectInterface::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const
 {}
 
-void IObjectInterface::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void IObjectInterface::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {}
 
 void IObjectInterface::garrisonDialogClosed(const CGHeroInstance *hero) const

+ 1 - 1
lib/mapObjects/IObjectInterface.h

@@ -61,7 +61,7 @@ public:
 	//Called when queries created DURING HERO VISIT are resolved
 	//First parameter is always hero that visited object and triggered the query
 	virtual void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const;
-	virtual void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const;
+	virtual void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const;
 	virtual void garrisonDialogClosed(const CGHeroInstance *hero) const;
 	virtual void heroLevelUpDone(const CGHeroInstance *hero) const;
 

+ 3 - 3
lib/mapObjects/MiscObjects.cpp

@@ -215,7 +215,7 @@ void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &resu
 	}
 }
 
-void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	if(answer)
 		cb->startBattleI(hero, this);
@@ -348,7 +348,7 @@ void CGResource::battleFinished(const CGHeroInstance *hero, const BattleResult &
 		collectRes(hero->getOwner());
 }
 
-void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	if(answer)
 		cb->startBattleI(hero, this);
@@ -915,7 +915,7 @@ void CGArtifact::battleFinished(const CGHeroInstance *hero, const BattleResult &
 		pick(hero);
 }
 
-void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 	if(answer)
 		cb->startBattleI(hero, this);

+ 3 - 3
lib/mapObjects/MiscObjects.h

@@ -91,7 +91,7 @@ public:
 
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 
 	std::string getObjectName() const override;
 	std::string getPopupText(PlayerColor player) const override;
@@ -132,7 +132,7 @@ public:
 	void initObj(vstd::RNG & rand) override;
 	void pickRandomObject(vstd::RNG & rand) override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 	std::string getHoverText(PlayerColor player) const override;
 
 	void collectRes(const PlayerColor & player) const;
@@ -163,7 +163,7 @@ private:
 
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 
 	void flagMine(const PlayerColor & player) const;
 	void newTurn(vstd::RNG & rand) const override;

+ 1 - 1
lib/networkPacks/PacksForClient.h

@@ -561,7 +561,7 @@ struct DLL_LINKAGE UpdateMapEvents : public CPackForClient
 struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient
 {
 	ObjectInstanceID town;
-	std::list<CCastleEvent> events;
+	std::vector<CCastleEvent> events;
 
 	void applyGs(CGameState * gs) const;
 	void visitTyped(ICPackVisitor & visitor) override;

+ 3 - 2
lib/serializer/ESerializationVersion.h

@@ -59,7 +59,8 @@ enum class ESerializationVersion : int32_t
 	REMOVE_LIB_RNG, // 849 - removed random number generators from library classes
 	HIGHSCORE_PARAMETERS, // 850 - saves parameter for campaign
 	PLAYER_HANDICAP, // 851 - player handicap selection at game start
-	CAMPAIGN_REGIONS, // 852 - configurable campaign regions
+	STATISTICS, // 852 - removed random number generators from library classes
+	CAMPAIGN_REGIONS, // 853 - configurable campaign regions
 
-	CURRENT = PLAYER_HANDICAP
+	CURRENT = CAMPAIGN_REGIONS
 };

+ 10 - 0
mapeditor/CMakeLists.txt

@@ -29,6 +29,9 @@ set(editor_SRCS
 		validator.cpp
 		inspector/inspector.cpp
 		inspector/townbuildingswidget.cpp
+		inspector/towneventdialog.cpp
+		inspector/towneventswidget.cpp
+		inspector/townspellswidget.cpp
 		inspector/armywidget.cpp
 		inspector/messagewidget.cpp
 		inspector/rewardswidget.cpp
@@ -70,6 +73,9 @@ set(editor_HEADERS
 		validator.h
 		inspector/inspector.h
 		inspector/townbuildingswidget.h
+		inspector/towneventdialog.h
+		inspector/towneventswidget.h
+		inspector/townspellswidget.h
 		inspector/armywidget.h
 		inspector/messagewidget.h
 		inspector/rewardswidget.h
@@ -79,6 +85,7 @@ set(editor_HEADERS
 		inspector/PickObjectDelegate.h
 		inspector/portraitwidget.h
 		resourceExtractor/ResourceConverter.h
+		mapeditorroles.h
 )
 
 set(editor_FORMS
@@ -98,6 +105,9 @@ set(editor_FORMS
 		playerparams.ui
 		validator.ui
 		inspector/townbuildingswidget.ui
+		inspector/towneventdialog.ui
+		inspector/towneventswidget.ui
+		inspector/townspellswidget.ui
 		inspector/armywidget.ui
 		inspector/messagewidget.ui
 		inspector/rewardswidget.ui

+ 4 - 0
mapeditor/inspector/inspector.cpp

@@ -21,6 +21,8 @@
 #include "../lib/constants/StringConstants.h"
 
 #include "townbuildingswidget.h"
+#include "towneventswidget.h"
+#include "townspellswidget.h"
 #include "armywidget.h"
 #include "messagewidget.h"
 #include "rewardswidget.h"
@@ -342,6 +344,8 @@ void Inspector::updateProperties(CGTownInstance * o)
 	
 	auto * delegate = new TownBuildingsDelegate(*o);
 	addProperty("Buildings", PropertyEditorPlaceholder(), delegate, false);
+	addProperty("Spells", PropertyEditorPlaceholder(), new TownSpellsDelegate(*o), false);
+	addProperty("Events", PropertyEditorPlaceholder(), new TownEventsDelegate(*o, controller), false);
 }
 
 void Inspector::updateProperties(CGArtifact * o)

+ 144 - 43
mapeditor/inspector/townbuildingswidget.cpp

@@ -10,6 +10,7 @@
 #include "StdInc.h"
 #include "townbuildingswidget.h"
 #include "ui_townbuildingswidget.h"
+#include "mapeditorroles.h"
 #include "../lib/entities/building/CBuilding.h"
 #include "../lib/entities/faction/CTownHandler.h"
 #include "../lib/texts/CGeneralTextHandler.h"
@@ -68,6 +69,56 @@ std::string defaultBuildingIdConversion(BuildingID bId)
 	}
 }
 
+QStandardItem * getBuildingParentFromTreeModel(const CBuilding * building, const QStandardItemModel & model)
+{
+	QStandardItem * parent = nullptr;
+	std::vector<QModelIndex> stack(1);
+	do
+	{
+		auto pindex = stack.back();
+		stack.pop_back();
+		auto rowCount = model.rowCount(pindex);
+		for (int i = 0; i < rowCount; ++i)
+		{
+			QModelIndex index = model.index(i, 0, pindex);
+			if (building->upgrade.getNum() == model.itemFromIndex(index)->data(MapEditorRoles::BuildingIDRole).toInt())
+			{
+				parent = model.itemFromIndex(index);
+				break;
+			}
+			if (model.hasChildren(index))
+				stack.push_back(index);
+		}
+	} while(!parent && !stack.empty());
+	return parent;
+}
+
+QVariantList getBuildingVariantsFromModel(const QStandardItemModel & model, int modelColumn, Qt::CheckState checkState)
+{
+	QVariantList result;
+	std::vector<QModelIndex> stack(1);
+	do
+	{
+		auto pindex = stack.back();
+		stack.pop_back();
+		auto rowCount = model.rowCount(pindex);
+		for (int i = 0; i < rowCount; ++i)
+		{
+			QModelIndex index = model.index(i, modelColumn, pindex);
+			auto * item = model.itemFromIndex(index);
+			if(item && item->checkState() == checkState)
+				result.push_back(item->data(MapEditorRoles::BuildingIDRole));
+			index = model.index(i, 0, pindex);
+			if (model.hasChildren(index))
+				stack.push_back(index);
+		}
+	} while(!stack.empty());
+
+	return result;
+}
+
+
+
 TownBuildingsWidget::TownBuildingsWidget(CGTownInstance & t, QWidget *parent) :
 	town(t),
 	QDialog(parent),
@@ -76,8 +127,8 @@ TownBuildingsWidget::TownBuildingsWidget(CGTownInstance & t, QWidget *parent) :
 	ui->setupUi(this);
 	ui->treeView->setModel(&model);
 	//ui->treeView->setColumnCount(3);
-	model.setHorizontalHeaderLabels(QStringList() << QStringLiteral("Type") << QStringLiteral("Enabled") << QStringLiteral("Built"));
-	
+	model.setHorizontalHeaderLabels(QStringList() << tr("Type") << tr("Enabled") << tr("Built"));
+	connect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged);
 	//setAttribute(Qt::WA_DeleteOnClose);
 }
 
@@ -96,7 +147,7 @@ QStandardItem * TownBuildingsWidget::addBuilding(const CTown & ctown, int bId, s
 		return nullptr;
 	}
 	
-	QString name = tr(building->getNameTranslated().c_str());
+	QString name = QString::fromStdString(building->getNameTranslated());
 	
 	if(name.isEmpty())
 		name = QString::fromStdString(defaultBuildingIdConversion(buildingId));
@@ -104,17 +155,17 @@ QStandardItem * TownBuildingsWidget::addBuilding(const CTown & ctown, int bId, s
 	QList<QStandardItem *> checks;
 	
 	checks << new QStandardItem(name);
-	checks.back()->setData(bId, Qt::UserRole);
+	checks.back()->setData(bId, MapEditorRoles::BuildingIDRole);
 	
 	checks << new QStandardItem;
 	checks.back()->setCheckable(true);
 	checks.back()->setCheckState(town.forbiddenBuildings.count(buildingId) ? Qt::Unchecked : Qt::Checked);
-	checks.back()->setData(bId, Qt::UserRole);
+	checks.back()->setData(bId, MapEditorRoles::BuildingIDRole);
 	
 	checks << new QStandardItem;
 	checks.back()->setCheckable(true);
 	checks.back()->setCheckState(town.builtBuildings.count(buildingId) ? Qt::Checked : Qt::Unchecked);
-	checks.back()->setData(bId, Qt::UserRole);
+	checks.back()->setData(bId, MapEditorRoles::BuildingIDRole);
 	
 	if(building->getBase() == buildingId)
 	{
@@ -122,25 +173,7 @@ QStandardItem * TownBuildingsWidget::addBuilding(const CTown & ctown, int bId, s
 	}
 	else
 	{
-		QStandardItem * parent = nullptr;
-		std::vector<QModelIndex> stack;
-		stack.push_back(QModelIndex());
-		while(!parent && !stack.empty())
-		{
-			auto pindex = stack.back();
-			stack.pop_back();
-			for(int i = 0; i < model.rowCount(pindex); ++i)
-			{
-				QModelIndex index = model.index(i, 0, pindex);
-				if(building->upgrade.getNum() == model.itemFromIndex(index)->data(Qt::UserRole).toInt())
-				{
-					parent = model.itemFromIndex(index);
-					break;
-				}
-				if(model.hasChildren(index))
-					stack.push_back(index);
-			}
-		}
+		QStandardItem * parent = getBuildingParentFromTreeModel(building, model);
 		
 		if(!parent)
 			parent = addBuilding(ctown, building->upgrade.getNum(), remaining);
@@ -172,36 +205,23 @@ void TownBuildingsWidget::addBuildings(const CTown & ctown)
 
 std::set<BuildingID> TownBuildingsWidget::getBuildingsFromModel(int modelColumn, Qt::CheckState checkState)
 {
+	auto buildingVariants = getBuildingVariantsFromModel(model, modelColumn, checkState);
 	std::set<BuildingID> result;
-	std::vector<QModelIndex> stack;
-	stack.push_back(QModelIndex());
-	while(!stack.empty())
+	for (const auto & buildingId : buildingVariants)
 	{
-		auto pindex = stack.back();
-		stack.pop_back();
-		for(int i = 0; i < model.rowCount(pindex); ++i)
-		{
-			QModelIndex index = model.index(i, modelColumn, pindex);
-			if(auto * item = model.itemFromIndex(index))
-				if(item->checkState() == checkState)
-					result.emplace(item->data(Qt::UserRole).toInt());
-			index = model.index(i, 0, pindex); //children are linked to first column of the model
-			if(model.hasChildren(index))
-				stack.push_back(index);
-		}
+		result.insert(buildingId.toInt());
 	}
-	
 	return result;
 }
 
 std::set<BuildingID> TownBuildingsWidget::getForbiddenBuildings()
 {
-	return getBuildingsFromModel(1, Qt::Unchecked);
+	return getBuildingsFromModel(Column::ENABLED, Qt::Unchecked);
 }
 
 std::set<BuildingID> TownBuildingsWidget::getBuiltBuildings()
 {
-	return getBuildingsFromModel(2, Qt::Checked);
+	return getBuildingsFromModel(Column::BUILT, Qt::Checked);
 }
 
 void TownBuildingsWidget::on_treeView_expanded(const QModelIndex &index)
@@ -214,6 +234,87 @@ void TownBuildingsWidget::on_treeView_collapsed(const QModelIndex &index)
 	ui->treeView->resizeColumnToContents(0);
 }
 
+void TownBuildingsWidget::on_buildAll_clicked()
+{
+	setAllRowsColumnCheckState(Column::BUILT, Qt::Checked);
+}
+
+void TownBuildingsWidget::on_demolishAll_clicked()
+{
+	setAllRowsColumnCheckState(Column::BUILT, Qt::Unchecked);
+}
+
+void TownBuildingsWidget::on_enableAll_clicked()
+{
+	setAllRowsColumnCheckState(Column::ENABLED, Qt::Checked);
+}
+
+void TownBuildingsWidget::on_disableAll_clicked()
+{
+	setAllRowsColumnCheckState(Column::ENABLED, Qt::Unchecked);
+}
+
+
+void TownBuildingsWidget::setRowColumnCheckState(const QStandardItem * item, Column column, Qt::CheckState checkState) {
+	auto sibling = item->model()->sibling(item->row(), column, item->index());
+	model.itemFromIndex(sibling)->setCheckState(checkState);
+}
+
+void TownBuildingsWidget::setAllRowsColumnCheckState(Column column, Qt::CheckState checkState)
+{
+	std::vector<QModelIndex> stack(1);
+	do
+	{
+		auto parentIndex = stack.back();
+		stack.pop_back();
+		auto rowCount = model.rowCount(parentIndex);
+		for (int i = 0; i < rowCount; ++i)
+		{
+			QModelIndex index = model.index(i, column, parentIndex);
+			if (auto* item = model.itemFromIndex(index))
+				item->setCheckState(checkState);
+			index = model.index(i, 0, parentIndex);
+			if (model.hasChildren(index))
+				stack.push_back(index);
+		}
+	} while(!stack.empty());
+}
+
+void TownBuildingsWidget::onItemChanged(const QStandardItem * item) {
+	disconnect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged);
+	auto rowFirstColumnIndex = item->model()->sibling(item->row(), Column::TYPE, item->index());
+	QStandardItem * nextRow = model.itemFromIndex(rowFirstColumnIndex);
+	if (item->checkState() == Qt::Checked) {
+		while (nextRow) {
+			setRowColumnCheckState(nextRow, Column(item->column()), Qt::Checked);
+			if (item->column() == Column::BUILT) {
+				setRowColumnCheckState(nextRow, Column::ENABLED, Qt::Checked);
+			}
+			nextRow = nextRow->parent();
+
+		}
+	}
+	else if (item->checkState() == Qt::Unchecked) {
+		std::vector<QStandardItem*> stack;
+		stack.push_back(nextRow);
+		do
+		{
+			nextRow = stack.back();
+			stack.pop_back();
+			setRowColumnCheckState(nextRow, Column(item->column()), Qt::Unchecked);
+			if (item->column() == Column::ENABLED) {
+				setRowColumnCheckState(nextRow, Column::BUILT, Qt::Unchecked);
+			}
+			if (nextRow->hasChildren()) {
+				for (int i = 0; i < nextRow->rowCount(); ++i) {
+					stack.push_back(nextRow->child(i, Column::TYPE));
+				}
+			}
+			
+		} while(!stack.empty());
+	}
+	connect(&model, &QStandardItemModel::itemChanged, this, &TownBuildingsWidget::onItemChanged);
+}
 
 TownBuildingsDelegate::TownBuildingsDelegate(CGTownInstance & t): town(t), QStyledItemDelegate()
 {

+ 22 - 2
mapeditor/inspector/townbuildingswidget.h

@@ -19,6 +19,10 @@ class TownBuildingsWidget;
 
 std::string defaultBuildingIdConversion(BuildingID bId);
 
+QStandardItem * getBuildingParentFromTreeModel(const CBuilding * building, const QStandardItemModel & model);
+
+QVariantList getBuildingVariantsFromModel(const QStandardItemModel & model, int modelColumn, Qt::CheckState checkState);
+
 class TownBuildingsWidget : public QDialog
 {
 	Q_OBJECT
@@ -26,9 +30,13 @@ class TownBuildingsWidget : public QDialog
 	QStandardItem * addBuilding(const CTown & ctown, int bId, std::set<si32> & remaining);
 	
 public:
+	enum Column
+	{
+		TYPE, ENABLED, BUILT
+	};
 	explicit TownBuildingsWidget(CGTownInstance &, QWidget *parent = nullptr);
 	~TownBuildingsWidget();
-	
+
 	void addBuildings(const CTown & ctown);
 	std::set<BuildingID> getForbiddenBuildings();
 	std::set<BuildingID> getBuiltBuildings();
@@ -38,9 +46,21 @@ private slots:
 
 	void on_treeView_collapsed(const QModelIndex &index);
 
+	void on_buildAll_clicked();
+
+	void on_demolishAll_clicked();
+
+	void on_enableAll_clicked();
+
+	void on_disableAll_clicked();
+
+	void onItemChanged(const QStandardItem * item);
+
 private:
 	std::set<BuildingID> getBuildingsFromModel(int modelColumn, Qt::CheckState checkState);
-	
+	void setRowColumnCheckState(const QStandardItem * item, Column column, Qt::CheckState checkState);
+	void setAllRowsColumnCheckState(Column column, Qt::CheckState checkState);
+
 	Ui::TownBuildingsWidget *ui;
 	CGTownInstance & town;
 	mutable QStandardItemModel model;

+ 34 - 2
mapeditor/inspector/townbuildingswidget.ui

@@ -9,7 +9,7 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>480</width>
+    <width>580</width>
     <height>280</height>
    </rect>
   </property>
@@ -21,7 +21,7 @@
   </property>
   <property name="minimumSize">
    <size>
-    <width>480</width>
+    <width>580</width>
     <height>280</height>
    </size>
   </property>
@@ -45,6 +45,38 @@
      </attribute>
     </widget>
    </item>
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout">
+     <item>
+      <widget class="QPushButton" name="buildAll">
+       <property name="text">
+        <string>Build all</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="demolishAll">
+       <property name="text">
+        <string>Demolish all</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="enableAll">
+       <property name="text">
+        <string>Enable all</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="disableAll">
+       <property name="text">
+        <string>Disable all</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
   </layout>
  </widget>
  <resources/>

+ 289 - 0
mapeditor/inspector/towneventdialog.cpp

@@ -0,0 +1,289 @@
+/*
+ * towneventdialog.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "../StdInc.h"
+#include "townbuildingswidget.h"
+#include "towneventdialog.h"
+#include "ui_towneventdialog.h"
+#include "mapeditorroles.h"
+#include "../../lib/entities/building/CBuilding.h"
+#include "../../lib/entities/faction/CTownHandler.h"
+#include "../../lib/constants/NumericConstants.h"
+#include "../../lib/constants/StringConstants.h"
+
+static const int FIRST_DAY_FOR_EVENT = 1;
+static const int LAST_DAY_FOR_EVENT = 999;
+static const int MAXIMUM_EVENT_REPEAT_AFTER = 999;
+
+static const int MAXIMUM_GOLD_CHANGE = 999999;
+static const int MAXIMUM_RESOURCE_CHANGE = 999;
+static const int GOLD_STEP = 100;
+static const int RESOURCE_STEP = 1;
+
+static const int MAXIMUM_CREATURES_CHANGE = 999999;
+
+TownEventDialog::TownEventDialog(CGTownInstance & t, QListWidgetItem * item, QWidget * parent) :
+	QDialog(parent),
+	ui(new Ui::TownEventDialog),
+	town(t),
+	townEventListItem(item)
+{
+	ui->setupUi(this);
+
+	ui->buildingsTree->setModel(&buildingsModel);
+
+	params = townEventListItem->data(MapEditorRoles::TownEventRole).toMap();
+	ui->eventFirstOccurrence->setMinimum(FIRST_DAY_FOR_EVENT);
+	ui->eventFirstOccurrence->setMaximum(LAST_DAY_FOR_EVENT);
+	ui->eventRepeatAfter->setMaximum(MAXIMUM_EVENT_REPEAT_AFTER);
+	ui->eventNameText->setText(params.value("name").toString());
+	ui->eventMessageText->setPlainText(params.value("message").toString());
+	ui->eventAffectsCpu->setChecked(params.value("computerAffected").toBool());
+	ui->eventAffectsHuman->setChecked(params.value("humanAffected").toBool());
+	ui->eventFirstOccurrence->setValue(params.value("firstOccurrence").toInt()+1);
+	ui->eventRepeatAfter->setValue(params.value("nextOccurrence").toInt());
+
+	initPlayers();
+	initResources();
+	initBuildings();
+	initCreatures();
+}
+
+TownEventDialog::~TownEventDialog()
+{
+	delete ui;
+}
+
+void TownEventDialog::initPlayers()
+{
+	for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
+	{
+		bool isAffected = (1 << i) & params.value("players").toInt();
+		auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i]));
+		item->setData(MapEditorRoles::PlayerIDRole, QVariant::fromValue(i));
+		item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked);
+		ui->playersAffected->addItem(item);
+	}
+}
+
+void TownEventDialog::initResources()
+{
+	ui->resourcesTable->setRowCount(GameConstants::RESOURCE_QUANTITY);
+	auto resourcesMap = params.value("resources").toMap();
+	for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
+	{
+		auto name = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]);
+		auto * item = new QTableWidgetItem();
+		item->setFlags(item->flags() & ~Qt::ItemIsEditable);
+		item->setText(name);
+		ui->resourcesTable->setItem(i, 0, item);
+
+		int val = resourcesMap.value(name).toInt();
+		auto * edit = new QSpinBox(ui->resourcesTable);
+		edit->setMaximum(i == GameResID::GOLD ? MAXIMUM_GOLD_CHANGE : MAXIMUM_RESOURCE_CHANGE);
+		edit->setMinimum(i == GameResID::GOLD ? -MAXIMUM_GOLD_CHANGE : -MAXIMUM_RESOURCE_CHANGE);
+		edit->setSingleStep(i == GameResID::GOLD ? GOLD_STEP : RESOURCE_STEP);
+		edit->setValue(val);
+
+		ui->resourcesTable->setCellWidget(i, 1, edit);
+	}
+}
+
+void TownEventDialog::initBuildings()
+{
+	auto * ctown = town.town;
+	if (!ctown)
+		ctown = VLC->townh->randomTown;
+	if (!ctown)
+		throw std::runtime_error("No Town defined for type selected");
+	auto allBuildings = ctown->getAllBuildings();
+	while (!allBuildings.empty())
+	{
+		addBuilding(*ctown, *allBuildings.begin(), allBuildings);
+	}
+	ui->buildingsTree->resizeColumnToContents(0);
+
+	connect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged);
+}
+
+QStandardItem * TownEventDialog::addBuilding(const CTown& ctown, BuildingID buildingId, std::set<si32>& remaining)
+{
+	auto bId = buildingId.num;
+	const CBuilding * building = ctown.buildings.at(buildingId);
+
+	QString name = QString::fromStdString(building->getNameTranslated());
+
+	if (name.isEmpty())
+		name = QString::fromStdString(defaultBuildingIdConversion(buildingId));
+
+	QList<QStandardItem *> checks;
+
+	checks << new QStandardItem(name);
+	checks.back()->setData(bId, MapEditorRoles::BuildingIDRole);
+
+	checks << new QStandardItem;
+	checks.back()->setCheckable(true);
+	checks.back()->setCheckState(params["buildings"].toList().contains(bId) ? Qt::Checked : Qt::Unchecked);
+	checks.back()->setData(bId, MapEditorRoles::BuildingIDRole);
+
+	if (building->getBase() == buildingId)
+	{
+		buildingsModel.appendRow(checks);
+	}
+	else
+	{
+		QStandardItem * parent = getBuildingParentFromTreeModel(building, buildingsModel);
+
+		if (!parent)
+			parent = addBuilding(ctown, building->upgrade.getNum(), remaining);
+
+		parent->appendRow(checks);
+	}
+
+	remaining.erase(bId);
+	return checks.front();
+}
+
+void TownEventDialog::initCreatures()
+{
+	auto creatures = params.value("creatures").toList();
+	auto * ctown = town.town;
+	for (int i = 0; i < GameConstants::CREATURES_PER_TOWN; ++i)
+	{
+		QString creatureNames;
+		if (!ctown)
+		{
+			creatureNames.append(tr("Creature level %1 / Creature level %1 Upgrade").arg(i + 1));
+		}
+		else
+		{
+			auto creaturesOnLevel = ctown->creatures.at(i);
+			for (auto& creature : creaturesOnLevel)
+			{
+				auto cre = VLC->creatures()->getById(creature);
+				auto creatureName = QString::fromStdString(cre->getNameSingularTranslated());
+				creatureNames.append(creatureNames.isEmpty() ? creatureName : " / " + creatureName);
+			}
+		}
+		auto * item = new QTableWidgetItem();
+		item->setFlags(item->flags() & ~Qt::ItemIsEditable);
+		item->setText(creatureNames);
+		ui->creaturesTable->setItem(i, 0, item);
+
+		auto creatureNumber = creatures.size() > i ? creatures.at(i).toInt() : 0;
+		auto * edit = new QSpinBox(ui->creaturesTable);
+		edit->setValue(creatureNumber);
+		edit->setMaximum(MAXIMUM_CREATURES_CHANGE);
+		ui->creaturesTable->setCellWidget(i, 1, edit);
+
+	}
+	ui->creaturesTable->resizeColumnToContents(0);
+}
+
+void TownEventDialog::on_TownEventDialog_finished(int result)
+{
+	QVariantMap descriptor;
+	descriptor["name"] = ui->eventNameText->text();
+	descriptor["message"] = ui->eventMessageText->toPlainText();
+	descriptor["humanAffected"] = QVariant::fromValue(ui->eventAffectsHuman->isChecked());
+	descriptor["computerAffected"] = QVariant::fromValue(ui->eventAffectsCpu->isChecked());
+	descriptor["firstOccurrence"] = QVariant::fromValue(ui->eventFirstOccurrence->value()-1);
+	descriptor["nextOccurrence"] = QVariant::fromValue(ui->eventRepeatAfter->value());
+	descriptor["players"] = playersToVariant();
+	descriptor["resources"] = resourcesToVariant();
+	descriptor["buildings"] = buildingsToVariant();
+	descriptor["creatures"] = creaturesToVariant();
+
+	townEventListItem->setData(MapEditorRoles::TownEventRole, descriptor);
+	auto itemText = tr("Day %1 - %2").arg(ui->eventFirstOccurrence->value(), 3).arg(ui->eventNameText->text());
+	townEventListItem->setText(itemText);
+}
+
+QVariant TownEventDialog::playersToVariant()
+{
+	int players = 0;
+	for (int i = 0; i < ui->playersAffected->count(); ++i)
+	{
+		auto * item = ui->playersAffected->item(i);
+		if (item->checkState() == Qt::Checked)
+			players |= 1 << i;
+	}
+	return QVariant::fromValue(players);
+}
+
+QVariantMap TownEventDialog::resourcesToVariant()
+{
+	auto res = params.value("resources").toMap();
+	for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
+	{
+		auto * itemType = ui->resourcesTable->item(i, 0);
+		auto * itemQty = static_cast<QSpinBox *> (ui->resourcesTable->cellWidget(i, 1));
+
+		res[itemType->text()] = QVariant::fromValue(itemQty->value());
+	}
+	return res;
+}
+
+QVariantList TownEventDialog::buildingsToVariant()
+{
+	return getBuildingVariantsFromModel(buildingsModel, 1, Qt::Checked);
+}
+
+QVariantList TownEventDialog::creaturesToVariant()
+{
+	QVariantList creaturesList;
+	for (int i = 0; i < GameConstants::CREATURES_PER_TOWN; ++i)
+	{
+		auto * item = static_cast<QSpinBox *>(ui->creaturesTable->cellWidget(i, 1));
+		creaturesList.push_back(item->value());
+	}
+	return creaturesList;
+}
+
+void TownEventDialog::on_okButton_clicked()
+{
+	close();
+}
+
+void TownEventDialog::setRowColumnCheckState(const QStandardItem * item, int column, Qt::CheckState checkState) {
+	auto sibling = item->model()->sibling(item->row(), column, item->index());
+	buildingsModel.itemFromIndex(sibling)->setCheckState(checkState);
+}
+
+void TownEventDialog::onItemChanged(const QStandardItem * item)
+{
+	disconnect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged);
+	auto rowFirstColumnIndex = item->model()->sibling(item->row(), 0, item->index());
+	QStandardItem * nextRow = buildingsModel.itemFromIndex(rowFirstColumnIndex);
+	if (item->checkState() == Qt::Checked) {
+		while (nextRow) {
+			setRowColumnCheckState(nextRow,item->column(), Qt::Checked);
+			nextRow = nextRow->parent();
+
+		}
+	}
+	else if (item->checkState() == Qt::Unchecked) {
+		std::vector<QStandardItem *> stack;
+		stack.push_back(nextRow);
+		do
+		{
+			nextRow = stack.back();
+			stack.pop_back();
+			setRowColumnCheckState(nextRow, item->column(), Qt::Unchecked);
+			if (nextRow->hasChildren()) {
+				for (int i = 0; i < nextRow->rowCount(); ++i) {
+					stack.push_back(nextRow->child(i, 0));
+				}
+			}
+
+		} while(!stack.empty());
+	}
+	connect(&buildingsModel, &QStandardItemModel::itemChanged, this, &TownEventDialog::onItemChanged);
+}

+ 53 - 0
mapeditor/inspector/towneventdialog.h

@@ -0,0 +1,53 @@
+/*
+ * towneventdialog.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../StdInc.h"
+#include <QDialog>
+#include "../lib/mapObjects/CGTownInstance.h"
+
+namespace Ui {
+	class TownEventDialog;
+}
+
+class TownEventDialog : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit TownEventDialog(CGTownInstance & town, QListWidgetItem * item, QWidget * parent);
+	~TownEventDialog();
+
+
+private slots:
+	void onItemChanged(const QStandardItem * item);
+	void on_TownEventDialog_finished(int result);
+	void on_okButton_clicked();
+	void setRowColumnCheckState(const QStandardItem * item, int column, Qt::CheckState checkState);
+
+private:
+	void initPlayers();
+	void initResources();
+	void initBuildings();
+	void initCreatures();
+
+	QVariant playersToVariant();
+	QVariantMap resourcesToVariant();
+	QVariantList buildingsToVariant();
+	QVariantList creaturesToVariant();
+
+	QStandardItem * addBuilding(const CTown & ctown, BuildingID bId, std::set<si32> & remaining);
+
+	Ui::TownEventDialog * ui;
+	CGTownInstance & town;
+	QListWidgetItem * townEventListItem;
+	QMap<QString, QVariant> params;
+	QStandardItemModel buildingsModel;
+};

+ 266 - 0
mapeditor/inspector/towneventdialog.ui

@@ -0,0 +1,266 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TownEventDialog</class>
+ <widget class="QDialog" name="TownEventDialog">
+  <property name="windowModality">
+   <enum>Qt::ApplicationModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>693</width>
+    <height>525</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>Town event</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_16">
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <property name="leftMargin">
+    <number>3</number>
+   </property>
+   <property name="rightMargin">
+    <number>3</number>
+   </property>
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="generalTab">
+      <attribute name="title">
+       <string>General</string>
+      </attribute>
+      <widget class="QWidget" name="verticalLayoutWidget">
+       <property name="geometry">
+        <rect>
+         <x>9</x>
+         <y>9</y>
+         <width>511</width>
+         <height>351</height>
+        </rect>
+       </property>
+       <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
+        <item>
+         <widget class="QLineEdit" name="eventNameText">
+          <property name="placeholderText">
+           <string>Event name</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPlainTextEdit" name="eventMessageText">
+          <property name="placeholderText">
+           <string>Type event message text</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="horizontalLayoutWidget">
+       <property name="geometry">
+        <rect>
+         <x>10</x>
+         <y>370</y>
+         <width>511</width>
+         <height>61</height>
+        </rect>
+       </property>
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <item>
+         <layout class="QVBoxLayout" name="verticalLayout_2">
+          <item>
+           <widget class="QLabel" name="eventFirstOccurrenceText">
+            <property name="text">
+             <string>Day of first occurrence</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QSpinBox" name="eventFirstOccurrence"/>
+          </item>
+         </layout>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="verticalLayout_3">
+          <item>
+           <widget class="QLabel" name="eventRepeatAfterText">
+            <property name="text">
+             <string>Repeat after (0 = no repeat)</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QSpinBox" name="eventRepeatAfter"/>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="verticalLayoutWidget_4">
+       <property name="geometry">
+        <rect>
+         <x>529</x>
+         <y>9</y>
+         <width>141</width>
+         <height>421</height>
+        </rect>
+       </property>
+       <layout class="QVBoxLayout" name="verticalLayout_4">
+        <item>
+         <widget class="QLabel" name="playersAffectedText">
+          <property name="text">
+           <string>Affected players</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QListWidget" name="playersAffected">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>200</width>
+            <height>16777215</height>
+           </size>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QCheckBox" name="eventAffectsHuman">
+          <property name="text">
+           <string>affects human</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <layout class="QHBoxLayout" name="horizontalLayout_2">
+          <item>
+           <widget class="QCheckBox" name="eventAffectsCpu">
+            <property name="text">
+             <string>affects AI</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </widget>
+     </widget>
+     <widget class="QWidget" name="resourcesTab">
+      <attribute name="title">
+       <string>Resources</string>
+      </attribute>
+      <widget class="QTableWidget" name="resourcesTable">
+       <property name="geometry">
+        <rect>
+         <x>10</x>
+         <y>10</y>
+         <width>661</width>
+         <height>421</height>
+        </rect>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>0</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="columnCount">
+        <number>2</number>
+       </property>
+       <attribute name="horizontalHeaderVisible">
+        <bool>false</bool>
+       </attribute>
+       <attribute name="verticalHeaderVisible">
+        <bool>false</bool>
+       </attribute>
+       <column/>
+       <column/>
+      </widget>
+     </widget>
+     <widget class="QWidget" name="buildingsTab">
+      <attribute name="title">
+       <string>Buildings</string>
+      </attribute>
+      <widget class="QTreeView" name="buildingsTree">
+       <property name="geometry">
+        <rect>
+         <x>10</x>
+         <y>10</y>
+         <width>661</width>
+         <height>421</height>
+        </rect>
+       </property>
+       <property name="editTriggers">
+        <set>QAbstractItemView::NoEditTriggers</set>
+       </property>
+       <attribute name="headerVisible">
+        <bool>false</bool>
+       </attribute>
+      </widget>
+     </widget>
+     <widget class="QWidget" name="creaturesTab">
+      <attribute name="title">
+       <string>Creatures</string>
+      </attribute>
+      <widget class="QTableWidget" name="creaturesTable">
+       <property name="geometry">
+        <rect>
+         <x>10</x>
+         <y>10</y>
+         <width>661</width>
+         <height>421</height>
+        </rect>
+       </property>
+       <property name="rowCount">
+        <number>7</number>
+       </property>
+       <property name="columnCount">
+        <number>2</number>
+       </property>
+       <attribute name="horizontalHeaderVisible">
+        <bool>false</bool>
+       </attribute>
+       <attribute name="verticalHeaderVisible">
+        <bool>false</bool>
+       </attribute>
+       <row/>
+       <row/>
+       <row/>
+       <row/>
+       <row/>
+       <row/>
+       <row/>
+       <column/>
+       <column/>
+      </widget>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <widget class="QPushButton" name="okButton">
+     <property name="text">
+      <string>OK</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 177 - 0
mapeditor/inspector/towneventswidget.cpp

@@ -0,0 +1,177 @@
+/*
+ * towneventswidget.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "../StdInc.h"
+#include "towneventswidget.h"
+#include "ui_towneventswidget.h"
+#include "towneventdialog.h"
+#include "mapeditorroles.h"
+#include "mapsettings/eventsettings.h"
+#include "../../lib/constants/NumericConstants.h"
+#include "../../lib/constants/StringConstants.h"
+
+TownEventsWidget::TownEventsWidget(CGTownInstance & town, QWidget * parent) :
+	QDialog(parent),
+	ui(new Ui::TownEventsWidget),
+	town(town)
+{
+	ui->setupUi(this);
+}
+
+TownEventsWidget::~TownEventsWidget()
+{
+	delete ui;
+}
+
+QVariant toVariant(const std::set<BuildingID> & buildings)
+{
+	QVariantList result;
+	for (auto b : buildings)
+		result.push_back(QVariant::fromValue(b.num));
+	return result;
+}
+
+QVariant toVariant(const std::vector<si32> & creatures)
+{
+	QVariantList result;
+	for (auto c : creatures)
+		result.push_back(QVariant::fromValue(c));
+	return result;
+}
+
+std::set<BuildingID> buildingsFromVariant(const QVariant& v)
+{
+	std::set<BuildingID> result;
+	for (const auto & r : v.toList()) {
+		result.insert(BuildingID(r.toInt()));
+	}
+	return result;
+}
+
+std::vector<si32> creaturesFromVariant(const QVariant& v)
+{
+	std::vector<si32> result;
+	for (const auto & r : v.toList()) {
+		result.push_back(r.toInt());
+	}
+	return result;
+}
+
+QVariant toVariant(const CCastleEvent& event)
+{
+	QVariantMap result;
+	result["name"] = QString::fromStdString(event.name);
+	result["message"] = QString::fromStdString(event.message.toString());
+	result["players"] = QVariant::fromValue(event.players);
+	result["humanAffected"] = QVariant::fromValue(event.humanAffected);
+	result["computerAffected"] = QVariant::fromValue(event.computerAffected);
+	result["firstOccurrence"] = QVariant::fromValue(event.firstOccurrence);
+	result["nextOccurrence"] = QVariant::fromValue(event.nextOccurrence);
+	result["resources"] = toVariant(event.resources);
+	result["buildings"] = toVariant(event.buildings);
+	result["creatures"] = toVariant(event.creatures);
+
+	return QVariant(result);
+}
+
+CCastleEvent eventFromVariant(CMapHeader& map, const CGTownInstance& town, const QVariant& variant)
+{
+	CCastleEvent result;
+	auto v = variant.toMap();
+	result.name = v.value("name").toString().toStdString();
+	result.message.appendTextID(mapRegisterLocalizedString("map", map, TextIdentifier("town", town.instanceName, "event", result.name, "message"), v.value("message").toString().toStdString()));
+	result.players = v.value("players").toInt();
+	result.humanAffected = v.value("humanAffected").toInt();
+	result.computerAffected = v.value("computerAffected").toInt();
+	result.firstOccurrence = v.value("firstOccurrence").toInt();
+	result.nextOccurrence = v.value("nextOccurrence").toInt();
+	result.resources = resourcesFromVariant(v.value("resources"));
+	result.buildings = buildingsFromVariant(v.value("buildings"));
+	result.creatures = creaturesFromVariant(v.value("creatures"));
+	return result;
+}
+
+void TownEventsWidget::obtainData()
+{
+	for (const auto & event : town.events)
+	{
+		auto eventName = QString::fromStdString(event.name);
+		auto itemText = tr("Day %1 - %2").arg(event.firstOccurrence+1, 3).arg(eventName);
+
+		auto * item = new QListWidgetItem(itemText);
+		item->setData(MapEditorRoles::TownEventRole, toVariant(event));
+		ui->eventsList->addItem(item);
+	}
+}
+
+void TownEventsWidget::commitChanges(MapController& controller)
+{
+	town.events.clear();
+	for (int i = 0; i < ui->eventsList->count(); ++i)
+	{
+		const auto * item = ui->eventsList->item(i);
+		town.events.push_back(eventFromVariant(*controller.map(), town, item->data(MapEditorRoles::TownEventRole)));
+	}
+}
+
+void TownEventsWidget::on_timedEventAdd_clicked()
+{
+	CCastleEvent event;
+	event.name = tr("New event").toStdString();
+	auto* item = new QListWidgetItem(QString::fromStdString(event.name));
+	item->setData(MapEditorRoles::TownEventRole, toVariant(event));
+	ui->eventsList->addItem(item);
+	on_eventsList_itemActivated(item);
+}
+
+void TownEventsWidget::on_timedEventRemove_clicked()
+{
+	delete ui->eventsList->takeItem(ui->eventsList->currentRow());
+}
+
+void TownEventsWidget::on_eventsList_itemActivated(QListWidgetItem* item)
+{
+	TownEventDialog dlg{ town, item, parentWidget() };
+	dlg.exec();
+}
+
+
+TownEventsDelegate::TownEventsDelegate(CGTownInstance & town, MapController & c) : QStyledItemDelegate(), town(town), controller(c)
+{
+}
+
+QWidget* TownEventsDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const
+{
+	return new TownEventsWidget(town, parent);
+}
+
+void TownEventsDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const
+{
+	if (auto * ed = qobject_cast<TownEventsWidget *>(editor))
+	{
+		ed->obtainData();
+	}
+	else
+	{
+		QStyledItemDelegate::setEditorData(editor, index);
+	}
+}
+
+void TownEventsDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const
+{
+	if (auto * ed = qobject_cast<TownEventsWidget *>(editor))
+	{
+		ed->commitChanges(controller);
+	}
+	else
+	{
+		QStyledItemDelegate::setModelData(editor, model, index);
+	}
+}

+ 58 - 0
mapeditor/inspector/towneventswidget.h

@@ -0,0 +1,58 @@
+/*
+ * towneventswidget.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../StdInc.h"
+#include <QDialog>
+#include "../lib/mapping/CMapDefines.h"
+#include "../lib/mapObjects/CGTownInstance.h"
+#include "../mapcontroller.h"
+
+namespace Ui {
+	class TownEventsWidget;
+}
+
+class TownEventsWidget : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit TownEventsWidget(CGTownInstance &, QWidget * parent = nullptr);
+	~TownEventsWidget();
+
+	void obtainData();
+	void commitChanges(MapController & controller);
+private slots:
+	void on_timedEventAdd_clicked();
+	void on_timedEventRemove_clicked();
+	void on_eventsList_itemActivated(QListWidgetItem * item);
+
+private:
+
+	Ui::TownEventsWidget * ui;
+	CGTownInstance & town;
+};
+
+class TownEventsDelegate : public QStyledItemDelegate
+{
+	Q_OBJECT
+public:
+	using QStyledItemDelegate::QStyledItemDelegate;
+
+	TownEventsDelegate(CGTownInstance &, MapController &);
+
+	QWidget* createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
+	void setEditorData(QWidget * editor, const QModelIndex & index) const override;
+	void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
+
+private:
+	CGTownInstance & town;
+	MapController & controller;
+};

+ 93 - 0
mapeditor/inspector/towneventswidget.ui

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TownEventsWidget</class>
+ <widget class="QDialog" name="TownEventsWidget">
+  <property name="windowModality">
+   <enum>Qt::ApplicationModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>691</width>
+    <height>462</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>400</width>
+    <height>400</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Town events</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_7">
+     <item>
+      <widget class="QLabel" name="timedEventText">
+       <property name="text">
+        <string>Timed events</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="timedEventAdd">
+       <property name="minimumSize">
+        <size>
+         <width>90</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Add</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="timedEventRemove">
+       <property name="minimumSize">
+        <size>
+         <width>90</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Remove</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QListWidget" name="eventsList">
+     <property name="sortingEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 166 - 0
mapeditor/inspector/townspellswidget.cpp

@@ -0,0 +1,166 @@
+/*
+ * townspellswidget.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "townspellswidget.h"
+#include "ui_townspellswidget.h"
+#include "inspector.h"
+#include "mapeditorroles.h"
+#include "../../lib/constants/StringConstants.h"
+#include "../../lib/spells/CSpellHandler.h"
+
+TownSpellsWidget::TownSpellsWidget(CGTownInstance & town, QWidget * parent) :
+	QDialog(parent),
+	ui(new Ui::TownSpellsWidget),
+	town(town)
+{
+	ui->setupUi(this);
+
+	possibleSpellLists = { ui->possibleSpellList1, ui->possibleSpellList2, ui->possibleSpellList3, ui->possibleSpellList4, ui->possibleSpellList5 };
+	requiredSpellLists = { ui->requiredSpellList1, ui->requiredSpellList2, ui->requiredSpellList3, ui->requiredSpellList4, ui->requiredSpellList5 };
+
+	std::array<BuildingID, 5> mageGuilds = {BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5};
+	for (int i = 0; i < mageGuilds.size(); i++)
+	{
+		ui->tabWidget->setTabEnabled(i, vstd::contains(town.getTown()->buildings, mageGuilds[i]));
+	}
+}
+
+TownSpellsWidget::~TownSpellsWidget()
+{
+	delete ui;
+}
+
+
+void TownSpellsWidget::obtainData()
+{
+	initSpellLists();
+	if (vstd::contains(town.possibleSpells, SpellID::PRESET)) {
+		ui->customizeSpells->setChecked(true);
+	}
+	else
+	{
+		ui->customizeSpells->setChecked(false);
+		ui->tabWidget->setEnabled(false);
+	}
+}
+
+void TownSpellsWidget::resetSpells()
+{
+	town.possibleSpells.clear();
+	town.obligatorySpells.clear();
+	for (auto spellID : VLC->spellh->getDefaultAllowed())
+		town.possibleSpells.push_back(spellID);
+}
+
+void TownSpellsWidget::initSpellLists()
+{
+	auto spells = VLC->spellh->getDefaultAllowed();
+	for (int i = 0; i < GameConstants::SPELL_LEVELS; i++)
+	{
+		std::vector<SpellID> spellsByLevel;
+		auto getSpellsByLevel = [i](auto spellID) {
+			return spellID.toEntity(VLC)->getLevel() == i + 1;
+		};
+		vstd::copy_if(spells, std::back_inserter(spellsByLevel), getSpellsByLevel);
+		possibleSpellLists[i]->clear();
+		requiredSpellLists[i]->clear();
+		for (auto spellID : spellsByLevel)
+		{
+			auto spell = spellID.toEntity(VLC);
+			auto * possibleItem = new QListWidgetItem(QString::fromStdString(spell->getNameTranslated()));
+			possibleItem->setData(MapEditorRoles::SpellIDRole, QVariant::fromValue(spell->getIndex()));
+			possibleItem->setFlags(possibleItem->flags() | Qt::ItemIsUserCheckable);
+			possibleItem->setCheckState(vstd::contains(town.possibleSpells, spell->getId()) ? Qt::Checked : Qt::Unchecked);
+			possibleSpellLists[i]->addItem(possibleItem);
+
+			auto * requiredItem = new QListWidgetItem(QString::fromStdString(spell->getNameTranslated()));
+			requiredItem->setData(MapEditorRoles::SpellIDRole, QVariant::fromValue(spell->getIndex()));
+			requiredItem->setFlags(requiredItem->flags() | Qt::ItemIsUserCheckable);
+			requiredItem->setCheckState(vstd::contains(town.obligatorySpells, spell->getId()) ? Qt::Checked : Qt::Unchecked);
+			requiredSpellLists[i]->addItem(requiredItem);
+		}
+	}
+}
+
+void TownSpellsWidget::commitChanges()
+{
+	if (!ui->tabWidget->isEnabled())
+	{
+		resetSpells();
+		return;
+	}
+
+	auto updateTownSpellList = [](auto uiSpellLists, auto & townSpellList) {
+		for (const QListWidget * spellList : uiSpellLists)
+		{
+			for (int i = 0; i < spellList->count(); ++i)
+			{
+				const auto * item = spellList->item(i);
+				if (item->checkState() == Qt::Checked)
+				{
+					townSpellList.push_back(item->data(MapEditorRoles::SpellIDRole).toInt());
+				}
+			}
+		}
+	};
+
+	town.possibleSpells.clear();
+	town.obligatorySpells.clear();
+	town.possibleSpells.push_back(SpellID::PRESET);
+	updateTownSpellList(possibleSpellLists, town.possibleSpells);
+	updateTownSpellList(requiredSpellLists, town.obligatorySpells);
+}
+
+void TownSpellsWidget::on_customizeSpells_toggled(bool checked)
+{
+	if (checked)
+	{
+		town.possibleSpells.push_back(SpellID::PRESET);
+	}
+	else
+	{
+		resetSpells();
+	}
+	ui->tabWidget->setEnabled(checked);
+	initSpellLists();
+}
+
+TownSpellsDelegate::TownSpellsDelegate(CGTownInstance & town) : QStyledItemDelegate(), town(town)
+{
+}
+
+QWidget * TownSpellsDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const
+{
+	return new TownSpellsWidget(town, parent);
+}
+
+void TownSpellsDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const
+{
+	if (auto * ed = qobject_cast<TownSpellsWidget *>(editor))
+	{
+		ed->obtainData();
+	}
+	else
+	{
+		QStyledItemDelegate::setEditorData(editor, index);
+	}
+}
+
+void TownSpellsDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const
+{
+	if (auto * ed = qobject_cast<TownSpellsWidget *>(editor))
+	{
+		ed->commitChanges();
+	}
+	else
+	{
+		QStyledItemDelegate::setModelData(editor, model, index);
+	}
+}

+ 60 - 0
mapeditor/inspector/townspellswidget.h

@@ -0,0 +1,60 @@
+/*
+ * townspellswidget.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include <QDialog>
+#include "../../lib/mapObjects/CGTownInstance.h"
+
+namespace Ui {
+	class TownSpellsWidget;
+}
+
+
+class TownSpellsWidget : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit TownSpellsWidget(CGTownInstance &, QWidget * parent = nullptr);
+	~TownSpellsWidget();
+
+	void obtainData();
+	void commitChanges();
+
+private slots:
+	void on_customizeSpells_toggled(bool checked);
+
+private:
+	Ui::TownSpellsWidget * ui;
+
+	CGTownInstance & town;
+
+	std::array<QListWidget *, 5> possibleSpellLists;
+	std::array<QListWidget *, 5> requiredSpellLists;
+
+	void resetSpells();
+	void initSpellLists();
+};
+
+class TownSpellsDelegate : public QStyledItemDelegate
+{
+	Q_OBJECT
+public:
+	using QStyledItemDelegate::QStyledItemDelegate;
+
+	TownSpellsDelegate(CGTownInstance&);
+
+	QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex& index) const override;
+	void setEditorData(QWidget * editor, const QModelIndex & index) const override;
+	void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
+
+private:
+	CGTownInstance& town;
+};

+ 304 - 0
mapeditor/inspector/townspellswidget.ui

@@ -0,0 +1,304 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TownSpellsWidget</class>
+ <widget class="QDialog" name="TownSpellsWidget">
+  <property name="windowModality">
+   <enum>Qt::NonModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>480</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>600</width>
+    <height>480</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Spells</string>
+  </property>
+  <property name="layoutDirection">
+   <enum>Qt::LeftToRight</enum>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="spacing">
+    <number>10</number>
+   </property>
+   <property name="topMargin">
+    <number>5</number>
+   </property>
+   <item>
+    <widget class="QCheckBox" name="customizeSpells">
+     <property name="text">
+      <string>Customize spells</string>
+     </property>
+     <property name="checked">
+      <bool>false</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <property name="documentMode">
+      <bool>true</bool>
+     </property>
+     <widget class="QWidget" name="level1">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <attribute name="title">
+       <string>Level 1</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_level1">
+       <property name="leftMargin">
+        <number>12</number>
+       </property>
+       <property name="rightMargin">
+        <number>12</number>
+       </property>
+       <property name="bottomMargin">
+        <number>12</number>
+       </property>
+       <item>
+        <layout class="QGridLayout" name="gridLayout1">
+         <item row="0" column="0">
+          <widget class="QLabel" name="possibleSpellsText1">
+           <property name="text">
+            <string>Spell that may appear in mage guild</string>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="1">
+          <widget class="QLabel" name="requiredSpellsText1">
+           <property name="text">
+            <string>Spell that must appear in mage guild</string>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="0">
+          <widget class="QListWidget" name="possibleSpellList1"/>
+         </item>
+         <item row="1" column="1">
+          <widget class="QListWidget" name="requiredSpellList1"/>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="level2">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <attribute name="title">
+       <string>Level 2</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_level2">
+       <property name="leftMargin">
+        <number>12</number>
+       </property>
+       <property name="rightMargin">
+        <number>12</number>
+       </property>
+       <property name="bottomMargin">
+        <number>12</number>
+       </property>
+       <item>
+        <layout class="QGridLayout" name="gridLayout2">
+         <item row="0" column="0">
+          <widget class="QLabel" name="possibleSpellsText2">
+           <property name="text">
+            <string>Spell that may appear in mage guild</string>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="1">
+          <widget class="QLabel" name="requiredSpellsText2">
+           <property name="text">
+            <string>Spell that must appear in mage guild</string>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="0">
+          <widget class="QListWidget" name="possibleSpellList2"/>
+         </item>
+         <item row="1" column="1">
+          <widget class="QListWidget" name="requiredSpellList2"/>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="level3">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <attribute name="title">
+       <string>Level 3</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_level3">
+       <property name="leftMargin">
+        <number>12</number>
+       </property>
+       <property name="rightMargin">
+        <number>12</number>
+       </property>
+       <property name="bottomMargin">
+        <number>12</number>
+       </property>
+       <item>
+        <layout class="QGridLayout" name="gridLayout3">
+         <item row="0" column="0">
+          <widget class="QLabel" name="possibleSpellsText3">
+           <property name="text">
+            <string>Spell that may appear in mage guild</string>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="1">
+          <widget class="QLabel" name="requiredSpellsText3">
+           <property name="text">
+            <string>Spell that must appear in mage guild</string>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="0">
+          <widget class="QListWidget" name="possibleSpellList3"/>
+         </item>
+         <item row="1" column="1">
+          <widget class="QListWidget" name="requiredSpellList3"/>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="level4">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <attribute name="title">
+       <string>Level 4</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_level4">
+       <property name="leftMargin">
+        <number>12</number>
+       </property>
+       <property name="rightMargin">
+        <number>12</number>
+       </property>
+       <property name="bottomMargin">
+        <number>12</number>
+       </property>
+       <item>
+        <layout class="QGridLayout" name="gridLayout4">
+         <item row="0" column="0">
+          <widget class="QLabel" name="possibleSpellsText4">
+           <property name="text">
+            <string>Spell that may appear in mage guild</string>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="1">
+          <widget class="QLabel" name="requiredSpellsText4">
+           <property name="text">
+            <string>Spell that must appear in mage guild</string>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="0">
+          <widget class="QListWidget" name="possibleSpellList4"/>
+         </item>
+         <item row="1" column="1">
+          <widget class="QListWidget" name="requiredSpellList4"/>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="level5">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <attribute name="title">
+       <string>Level 5</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_level5">
+       <property name="leftMargin">
+        <number>12</number>
+       </property>
+       <property name="rightMargin">
+        <number>12</number>
+       </property>
+       <property name="bottomMargin">
+        <number>12</number>
+       </property>
+       <item>
+        <layout class="QGridLayout" name="gridLayout5">
+         <item row="0" column="0">
+          <widget class="QLabel" name="possibleSpellsText5">
+           <property name="text">
+            <string>Spell that may appear in mage guild</string>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="1">
+          <widget class="QLabel" name="requiredSpellsText5">
+           <property name="text">
+            <string>Spell that must appear in mage guild</string>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="0">
+          <widget class="QListWidget" name="possibleSpellList5"/>
+         </item>
+         <item row="1" column="1">
+          <widget class="QListWidget" name="requiredSpellList5"/>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 20 - 0
mapeditor/mapeditorroles.h

@@ -0,0 +1,20 @@
+/*
+ * mapeditorroles.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "StdInc.h"
+
+enum MapEditorRoles
+{
+	TownEventRole = Qt::UserRole + 1,
+	PlayerIDRole,
+	BuildingIDRole,
+	SpellIDRole
+};

+ 3 - 0
mapeditor/mapsettings/eventsettings.h

@@ -15,6 +15,9 @@ namespace Ui {
 class EventSettings;
 }
 
+QVariant toVariant(const TResources & resources);
+TResources resourcesFromVariant(const QVariant & v);
+
 class EventSettings : public AbstractSettings
 {
 	Q_OBJECT

+ 211 - 21
mapeditor/translation/chinese.ts

@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation>无法放置物体</translation>
     </message>
@@ -889,38 +889,38 @@
         <translation>高级</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation>屈服的</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation>友善的</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation>好斗的</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation>有敌意的</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation>野蛮的</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation>中立</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation>没有旗帜</translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation>建筑</translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation type="unfinished">类型</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation type="unfinished">通用</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation type="unfinished">事件名</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation type="unfinished">输入事件信息文本</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation type="unfinished">首次发生天数</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation type="unfinished">重复周期 (0 = 不重复)</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation type="unfinished">生效玩家</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation type="unfinished">人类玩家生效</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation type="unfinished">AI玩家生效</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation type="unfinished">资源</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation type="unfinished">建筑</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation type="unfinished">生物</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation type="unfinished">计时事件</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation type="unfinished">添加</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation type="unfinished">移除</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation type="unfinished">新事件</translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation type="unfinished">魔法</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation type="unfinished">自定义魔法</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation type="unfinished">1级</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation type="unfinished">2级</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation type="unfinished">3级</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation type="unfinished">4级</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation type="unfinished">5级</translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>
@@ -1698,18 +1900,6 @@
         <source>Width</source>
         <translation>宽度</translation>
     </message>
-    <message>
-        <source>S (36x36)</source>
-        <translation type="vanished">小(36x36)</translation>
-    </message>
-    <message>
-        <source>M (72x72)</source>
-        <translation type="vanished">中(72x72)</translation>
-    </message>
-    <message>
-        <source>L (108x108)</source>
-        <translation type="vanished">大(108x108)</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="179"/>
         <source>XL (144x144)</source>

+ 211 - 21
mapeditor/translation/czech.ts

@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation>Nelze umístit objekt</translation>
     </message>
@@ -889,38 +889,38 @@
         <translation>Expert</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation>Ochotná</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation>Přátelská</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation>Agresivní</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation>Nepřátelská</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation>Brutální</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation>neutrální</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation>NEOZNAČITELNÝ</translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation>Budovy</translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation type="unfinished">Druh</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation type="unfinished">Název události</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation type="unfinished">Zadejte text zprávy události</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation type="unfinished">Den prvního výskytu</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation type="unfinished">Opakovat po (0 = bez opak.)</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation type="unfinished">Ovlivnění hráči</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation type="unfinished">ovlivňuje lidi</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation type="unfinished">ovlivňuje AI</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation type="unfinished">Zdroje</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation type="unfinished">Budovy</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation type="unfinished">Jednotky</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation type="unfinished">Načasované události</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation type="unfinished">Přidat</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation type="unfinished">Odebrat</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation type="unfinished">Nová událost</translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation type="unfinished">Kouzla</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation type="unfinished">Přizpůsobit kouzla</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation type="unfinished">Úroveň 1</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation type="unfinished">Úroveň 2</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation type="unfinished">Úroveň 3</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation type="unfinished">Úroveň 4</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation type="unfinished">Úroveň 5</translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>
@@ -1698,18 +1900,6 @@
         <source>Width</source>
         <translation>Šířka</translation>
     </message>
-    <message>
-        <source>S (36x36)</source>
-        <translation type="vanished">S (36x36)</translation>
-    </message>
-    <message>
-        <source>M (72x72)</source>
-        <translation type="vanished">M (72x72)</translation>
-    </message>
-    <message>
-        <source>L (108x108)</source>
-        <translation type="vanished">L (108x108)</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="179"/>
         <source>XL (144x144)</source>

+ 211 - 9
mapeditor/translation/english.ts

@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation type="unfinished"></translation>
     </message>
@@ -889,38 +889,38 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>

+ 213 - 23
mapeditor/translation/french.ts

@@ -560,8 +560,8 @@
     </message>
     <message>
         <location filename="../mainwindow.cpp" line="280"/>
-        <source>Unsaved changes will be lost, are you sur?</source>
-        <translation>Des modifications non sauvegardées vont être perdues. Êtes-vous sûr ?</translation>
+        <source>Unsaved changes will be lost, are you sure?</source>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location filename="../mainwindow.cpp" line="406"/>
@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation>Impossible de placer l&apos;objet</translation>
     </message>
@@ -889,38 +889,38 @@
         <translation>Expert</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation>Compérhensif</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation>Amical</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation>Aggressif</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation>Hostile</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation>Sauvage</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation>neutre</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation>INCLASSABLE</translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation>Bâtiments</translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation type="unfinished">Type</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation type="unfinished">Général</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation type="unfinished">Nom de l&apos;évènement</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation type="unfinished">Taper le message d&apos;évènement</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation type="unfinished">Jour de la première occurrence</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation type="unfinished">Récurrence (0 = pas de récurrence)</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation type="unfinished">Joueurs affectés</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation type="unfinished">afttecte les joueurs</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation type="unfinished">affecte l&apos;ordinateur</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation type="unfinished">Resources</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation type="unfinished">Bâtiments</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation type="unfinished">Créatures</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation type="unfinished">Evenements timés</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation type="unfinished">Ajouter</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation type="unfinished">Supprimer</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation type="unfinished">Nouvel évènement</translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation type="unfinished">Sorts</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>
@@ -1698,18 +1900,6 @@
         <source>Width</source>
         <translation>Largeur</translation>
     </message>
-    <message>
-        <source>S (36x36)</source>
-        <translation type="vanished">Petite (36x36)</translation>
-    </message>
-    <message>
-        <source>M (72x72)</source>
-        <translation type="vanished">Moyenne (72x72)</translation>
-    </message>
-    <message>
-        <source>L (108x108)</source>
-        <translation type="vanished">Grande (108x108)</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="179"/>
         <source>XL (144x144)</source>

+ 211 - 21
mapeditor/translation/german.ts

@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation>Objekt kann nicht platziert werden</translation>
     </message>
@@ -889,38 +889,38 @@
         <translation>Experte</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation>Konform</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation>Freundlich</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation>Aggressiv</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation>Feindlich</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation>Wild</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation>neutral</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation>UNFLAGGBAR</translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation>Gebäude</translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation type="unfinished">Typ</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation type="unfinished">Allgemein</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation type="unfinished">Name des Ereignisses</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation type="unfinished">Ereignistext eingeben</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation type="unfinished">Tag des ersten Auftretens</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation type="unfinished">Wiederholung nach (0 = keine Wiederholung)</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation type="unfinished">Betroffene Spieler</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation type="unfinished">beeinflusst Menschen</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation type="unfinished">beeinflusst KI</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation type="unfinished">Ressourcen</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation type="unfinished">Gebäude</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation type="unfinished">Kreaturen</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation type="unfinished">Zeitlich begrenzte Ereignisse</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation type="unfinished">Hinzufügen</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation type="unfinished">Entfernen</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation type="unfinished">Neues Ereignis</translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation type="unfinished">Zaubersprüche</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation type="unfinished">Zaubersprüche anpassen</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation type="unfinished">Level 1</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation type="unfinished">Level 2</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation type="unfinished">Level 3</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation type="unfinished">Level 4</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation type="unfinished">Level 5</translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>
@@ -1698,18 +1900,6 @@
         <source>Width</source>
         <translation>Breite</translation>
     </message>
-    <message>
-        <source>S (36x36)</source>
-        <translation type="vanished">S (36x36)</translation>
-    </message>
-    <message>
-        <source>M (72x72)</source>
-        <translation type="vanished">M (72x72)</translation>
-    </message>
-    <message>
-        <source>L (108x108)</source>
-        <translation type="vanished">L (108x108)</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="179"/>
         <source>XL (144x144)</source>

+ 211 - 21
mapeditor/translation/polish.ts

@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation>Nie można umieścić obiektu</translation>
     </message>
@@ -889,38 +889,38 @@
         <translation>Ekspert</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation>Przyjazny</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation>Przychylny</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation>Agresywny</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation>Wrogi</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation>Nienawistny</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation>neutralny</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation>NIEOFLAGOWYWALNY</translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation>Budynki</translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation>Zbuduj wsyzstkie</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation>Zburz wszystkie</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation>Włącz wszystkie</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation>Wyłącz wszystkie</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation>Typ</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation>Włączony</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation>Zbudowany</translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation>Zdarzenie miasta</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation>Ogólne</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation>Nazwa zdarzenia</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation>Wpisz treść komunikatu zdarzenia</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation>Dzień pierwszego wystąpienia</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation>Powtórz po... (0 = nigdy)</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation>Dotyczy graczy</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation>dotyczy graczy ludzkich</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation>dotyczy graczy AI</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation>Zasoby</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation>Budynki</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation>Stworzenia</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation>OK</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation>Stworzenie poziomu %1 / Ulepszone stworzenie poziomu %1</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation>Dzień %1 - %2</translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation>Zdarzenia miasta</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation>Zdarzenia czasowe</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation>Dodaj</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation>Usuń</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation>Dzień %1 - %2</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation>Nowe zdarzenie</translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation>Zaklęcia</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation>Własne zaklęcia</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation>Poziom 1</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation>Zaklecia, które mogą pojawić się w gildii magów</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation>Zaklecia, które muszą pojawić się w gildii magów</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation>Poziom 2</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation>Poziom 3</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation>Poziom 4</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation>Poziom 5</translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>
@@ -1698,18 +1900,6 @@
         <source>Width</source>
         <translation>Szerokość</translation>
     </message>
-    <message>
-        <source>S (36x36)</source>
-        <translation type="vanished">S (36x36)</translation>
-    </message>
-    <message>
-        <source>M (72x72)</source>
-        <translation type="vanished">M (72x72)</translation>
-    </message>
-    <message>
-        <source>L (108x108)</source>
-        <translation type="vanished">L (108x108)</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="179"/>
         <source>XL (144x144)</source>

+ 211 - 21
mapeditor/translation/portuguese.ts

@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation>Não é possível colocar objeto</translation>
     </message>
@@ -889,38 +889,38 @@
         <translation>Experiente</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation>Conformista</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation>Amigável</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation>Agressivo</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation>Hostil</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation>Selvagem</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation>neutro</translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation>NÃO TEM BANDEIRA</translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation>Estruturas</translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation type="unfinished">Tipo</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation type="unfinished">Geral</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation type="unfinished">Nome do evento</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation type="unfinished">Introduza o texto da mensagem do evento</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation type="unfinished">Dia da primeira ocorrência</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation type="unfinished">Repetir após (0 = não repetir)</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation type="unfinished">Jogadores afetados</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation type="unfinished">afeta humano</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation type="unfinished">afeta IA</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation type="unfinished">Recursos</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation type="unfinished">Estruturas</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation type="unfinished">Criaturas</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation type="unfinished">Eventos Temporizados</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation type="unfinished">Adicionar</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation type="unfinished">Remover</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation type="unfinished">Novo Evento</translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation type="unfinished">Feitiços</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation type="unfinished">Personalizar feitiços</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation type="unfinished">Nível 1</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation type="unfinished">Nível 2</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation type="unfinished">Nível 3</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation type="unfinished">Nível 4</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation type="unfinished">Nível 5</translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>
@@ -1698,18 +1900,6 @@
         <source>Width</source>
         <translation>Largura</translation>
     </message>
-    <message>
-        <source>S (36x36)</source>
-        <translation type="vanished">P (36x36)</translation>
-    </message>
-    <message>
-        <source>M (72x72)</source>
-        <translation type="vanished">M (72x72)</translation>
-    </message>
-    <message>
-        <source>L (108x108)</source>
-        <translation type="vanished">G (108x108)</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="179"/>
         <source>XL (144x144)</source>

+ 211 - 21
mapeditor/translation/russian.ts

@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation type="unfinished"></translation>
     </message>
@@ -889,38 +889,38 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation>Постройки</translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation type="unfinished">Тип</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation type="unfinished">Общее</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation type="unfinished">Постройки</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation type="unfinished">Заклинания</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>
@@ -1698,18 +1900,6 @@
         <source>Width</source>
         <translation>Ширина</translation>
     </message>
-    <message>
-        <source>S (36x36)</source>
-        <translation type="vanished">Мал. (36x36)</translation>
-    </message>
-    <message>
-        <source>M (72x72)</source>
-        <translation type="vanished">Ср. (72x72)</translation>
-    </message>
-    <message>
-        <source>L (108x108)</source>
-        <translation type="vanished">Бол. (108x108)</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="179"/>
         <source>XL (144x144)</source>

+ 211 - 21
mapeditor/translation/spanish.ts

@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation type="unfinished"></translation>
     </message>
@@ -889,38 +889,38 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation>Edificios</translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation type="unfinished">Tipo</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation type="unfinished">General</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation type="unfinished">Edificios</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation type="unfinished">Hechizos</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>
@@ -1698,18 +1900,6 @@
         <source>Width</source>
         <translation>Ancho</translation>
     </message>
-    <message>
-        <source>S (36x36)</source>
-        <translation type="vanished">S (36x36)</translation>
-    </message>
-    <message>
-        <source>M (72x72)</source>
-        <translation type="vanished">M (72x72)</translation>
-    </message>
-    <message>
-        <source>L (108x108)</source>
-        <translation type="vanished">L (108x108)</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="179"/>
         <source>XL (144x144)</source>

+ 211 - 21
mapeditor/translation/ukrainian.ts

@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation type="unfinished"></translation>
     </message>
@@ -889,38 +889,38 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation>Будівлі</translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation type="unfinished">Тип</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation type="unfinished">Загальний</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation type="unfinished">Будівлі</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation type="unfinished">Закляття</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>
@@ -1698,18 +1900,6 @@
         <source>Width</source>
         <translation>Ширина</translation>
     </message>
-    <message>
-        <source>S (36x36)</source>
-        <translation type="vanished">М (36x36)</translation>
-    </message>
-    <message>
-        <source>M (72x72)</source>
-        <translation type="vanished">С (72x72)</translation>
-    </message>
-    <message>
-        <source>L (108x108)</source>
-        <translation type="vanished">В (108x108)</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="179"/>
         <source>XL (144x144)</source>

+ 211 - 21
mapeditor/translation/vietnamese.ts

@@ -715,7 +715,7 @@
 <context>
     <name>MapView</name>
     <message>
-        <location filename="../mapview.cpp" line="625"/>
+        <location filename="../mapview.cpp" line="626"/>
         <source>Can&apos;t place object</source>
         <translation>Không thể đặt vật thể</translation>
     </message>
@@ -889,38 +889,38 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="36"/>
+        <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Compliant</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="37"/>
+        <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Friendly</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="38"/>
+        <location filename="../inspector/inspector.cpp" line="40"/>
         <source>Aggressive</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="39"/>
+        <location filename="../inspector/inspector.cpp" line="41"/>
         <source>Hostile</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="40"/>
+        <location filename="../inspector/inspector.cpp" line="42"/>
         <source>Savage</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="843"/>
-        <location filename="../inspector/inspector.cpp" line="932"/>
+        <location filename="../inspector/inspector.cpp" line="847"/>
+        <location filename="../inspector/inspector.cpp" line="936"/>
         <source>neutral</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../inspector/inspector.cpp" line="841"/>
+        <location filename="../inspector/inspector.cpp" line="845"/>
         <source>UNFLAGGABLE</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1435,6 +1435,208 @@
         <source>Buildings</source>
         <translation>Công trình</translation>
     </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="53"/>
+        <source>Build all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="60"/>
+        <source>Demolish all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="67"/>
+        <source>Enable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.ui" line="74"/>
+        <source>Disable all</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Type</source>
+        <translation type="unfinished">Loại</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Enabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
+        <source>Built</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventDialog</name>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="23"/>
+        <source>Town event</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="42"/>
+        <source>General</source>
+        <translation type="unfinished">Chung</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="57"/>
+        <source>Event name</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="64"/>
+        <source>Type event message text</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="85"/>
+        <source>Day of first occurrence</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="99"/>
+        <source>Repeat after (0 = no repeat)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="123"/>
+        <source>Affected players</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="146"/>
+        <source>affects human</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="155"/>
+        <source>affects AI</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="166"/>
+        <source>Resources</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="198"/>
+        <source>Buildings</source>
+        <translation type="unfinished">Công trình</translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="216"/>
+        <source>Creatures</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.ui" line="255"/>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="177"/>
+        <source>Creature level %1 / Creature level %1 Upgrade</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventdialog.cpp" line="219"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownEventsWidget</name>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="29"/>
+        <source>Town events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="37"/>
+        <source>Timed events</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="63"/>
+        <source>Add</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.ui" line="76"/>
+        <source>Remove</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="105"/>
+        <source>Day %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/towneventswidget.cpp" line="126"/>
+        <source>New event</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>TownSpellsWidget</name>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="29"/>
+        <source>Spells</source>
+        <translation type="unfinished">Phép</translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="47"/>
+        <source>Customize spells</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="76"/>
+        <source>Level 1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="93"/>
+        <location filename="../inspector/townspellswidget.ui" line="139"/>
+        <location filename="../inspector/townspellswidget.ui" line="185"/>
+        <location filename="../inspector/townspellswidget.ui" line="231"/>
+        <location filename="../inspector/townspellswidget.ui" line="277"/>
+        <source>Spell that may appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="100"/>
+        <location filename="../inspector/townspellswidget.ui" line="146"/>
+        <location filename="../inspector/townspellswidget.ui" line="192"/>
+        <location filename="../inspector/townspellswidget.ui" line="238"/>
+        <location filename="../inspector/townspellswidget.ui" line="284"/>
+        <source>Spell that must appear in mage guild</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="122"/>
+        <source>Level 2</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="168"/>
+        <source>Level 3</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="214"/>
+        <source>Level 4</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../inspector/townspellswidget.ui" line="260"/>
+        <source>Level 5</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>Translations</name>
@@ -1698,18 +1900,6 @@
         <source>Width</source>
         <translation>Rộng</translation>
     </message>
-    <message>
-        <source>S (36x36)</source>
-        <translation type="vanished">Nhỏ (36x36)</translation>
-    </message>
-    <message>
-        <source>M (72x72)</source>
-        <translation type="vanished">Vừa (72x72)</translation>
-    </message>
-    <message>
-        <source>L (108x108)</source>
-        <translation type="vanished">Lớn (108x108)</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="179"/>
         <source>XL (144x144)</source>

+ 29 - 4
server/CGameHandler.cpp

@@ -669,6 +669,19 @@ void CGameHandler::onPlayerTurnEnded(PlayerColor which)
 		heroPool->onNewWeek(which);
 }
 
+void CGameHandler::addStatistics()
+{
+	for (auto & elem : gs->players)
+	{
+		if (elem.first == PlayerColor::NEUTRAL || !elem.first.isValidPlayer())
+			continue;
+
+		auto data = StatisticDataSet::createEntry(&elem.second, gs);
+
+		gameState()->statistic.add(data);
+	}
+}
+
 void CGameHandler::onNewTurn()
 {
 	logGlobal->trace("Turn %d", gs->day+1);
@@ -1013,6 +1026,8 @@ void CGameHandler::onNewTurn()
 	}
 
 	synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that
+
+	addStatistics();
 }
 
 void CGameHandler::start(bool resume)
@@ -1345,6 +1360,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme
 
 		turnTimerHandler->setEndTurnAllowed(h->getOwner(), !movingOntoWater && !movingOntoObstacle);
 		doMove(TryMoveHero::SUCCESS, lookForGuards, visitDest, LEAVING_TILE);
+		gs->statistic.accumulatedValues[asker].movementPointsUsed += tmh.movePoints;
 		return true;
 	}
 }
@@ -2457,7 +2473,10 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID,
 
 	//Take cost
 	if(!force)
+	{
 		giveResources(t->tempOwner, -requestedBuilding->resources);
+		gs->statistic.accumulatedValues[t->tempOwner].spentResourcesForBuildings += requestedBuilding->resources;
+	}
 
 	//We know what has been built, apply changes. Do this as final step to properly update town window
 	sendAndApply(&ns);
@@ -2559,7 +2578,9 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst
 	}
 
 	//recruit
-	giveResources(army->tempOwner, -(c->getFullRecruitCost() * cram));
+	TResources cost = (c->getFullRecruitCost() * cram);
+	giveResources(army->tempOwner, -cost);
+	gs->statistic.accumulatedValues[army->tempOwner].spentResourcesForArmy += cost;
 
 	SetAvailableCreatures sac;
 	sac.tid = objid;
@@ -2612,6 +2633,7 @@ bool CGameHandler::upgradeCreature(ObjectInstanceID objid, SlotID pos, CreatureI
 
 	//take resources
 	giveResources(player, -totalCost);
+	gs->statistic.accumulatedValues[player].spentResourcesForArmy += totalCost;
 
 	//upgrade creature
 	changeStackType(StackLocation(obj, pos), upgID.toCreature());
@@ -3236,6 +3258,9 @@ bool CGameHandler::tradeResources(const IMarket *market, ui32 amountToSell, Play
 	giveResource(player, toSell, -b1 * amountToBoy);
 	giveResource(player, toBuy, b2 * amountToBoy);
 
+	gs->statistic.accumulatedValues[player].tradeVolume[toSell] += -b1 * amountToBoy;
+	gs->statistic.accumulatedValues[player].tradeVolume[toBuy] += b2 * amountToBoy;
+
 	return true;
 }
 
@@ -3424,7 +3449,7 @@ void CGameHandler::handleTimeEvents()
 
 void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n)
 {
-	town->events.sort(evntCmp);
+	std::sort(town->events.begin(), town->events.end(), evntCmp);
 	while(town->events.size() && town->events.front().firstOccurrence == gs->day)
 	{
 		PlayerColor player = town->tempOwner;
@@ -3485,7 +3510,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n)
 
 		if (ev.nextOccurrence)
 		{
-			town->events.pop_front();
+			town->events.erase(town->events.begin());
 
 			ev.firstOccurrence += ev.nextOccurrence;
 			auto it = town->events.begin();
@@ -3495,7 +3520,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n)
 		}
 		else
 		{
-			town->events.pop_front();
+			town->events.erase(town->events.begin());
 		}
 	}
 

+ 1 - 0
server/CGameHandler.h

@@ -226,6 +226,7 @@ public:
 	void onPlayerTurnStarted(PlayerColor which);
 	void onPlayerTurnEnded(PlayerColor which);
 	void onNewTurn();
+	void addStatistics();
 
 	void handleTimeEvents();
 	void handleTownEvents(CGTownInstance *town, NewTurn &n);

+ 32 - 10
server/battles/BattleResultProcessor.cpp

@@ -447,16 +447,16 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle)
 					addArtifactToTransfer(packCommander, artSlot.first, artSlot.second.getArt());
 				sendArtifacts(packCommander);
 			}
-		}
-		auto armyObj = battle.battleGetArmyObject(battle.otherSide(battleResult->winner));
-		for(const auto & armySlot : armyObj->stacks)
-		{
-			BulkMoveArtifacts packsArmy(finishingBattle->winnerHero->getOwner(), finishingBattle->loserHero->id, finishingBattle->winnerHero->id, false);
-			packsArmy.srcArtHolder = armyObj->id;
-			packsArmy.srcCreature = armySlot.first;
-			for(const auto & artSlot : armySlot.second->artifactsWorn)
-				addArtifactToTransfer(packsArmy, artSlot.first, armySlot.second->getArt(artSlot.first));
-			sendArtifacts(packsArmy);
+			auto armyObj = battle.battleGetArmyObject(battle.otherSide(battleResult->winner));
+			for(const auto & armySlot : armyObj->stacks)
+			{
+				BulkMoveArtifacts packsArmy(finishingBattle->winnerHero->getOwner(), finishingBattle->loserHero->id, finishingBattle->winnerHero->id, false);
+				packsArmy.srcArtHolder = armyObj->id;
+				packsArmy.srcCreature = armySlot.first;
+				for(const auto & artSlot : armySlot.second->artifactsWorn)
+					addArtifactToTransfer(packsArmy, artSlot.first, armySlot.second->getArt(artSlot.first));
+				sendArtifacts(packsArmy);
+			}
 		}
 		// Display loot
 		if(!arts.empty())
@@ -497,6 +497,22 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle)
 		gameHandler->sendAndApply(&ro);
 	}
 
+	// add statistic
+	if(battle.sideToPlayer(0) == PlayerColor::NEUTRAL || battle.sideToPlayer(1) == PlayerColor::NEUTRAL)
+	{
+		gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(0)].numBattlesNeutral++;
+		gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(1)].numBattlesNeutral++;
+		if(!finishingBattle->isDraw())
+			gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(finishingBattle->winnerSide)].numWinBattlesNeutral++;
+	}
+	else
+	{
+		gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(0)].numBattlesPlayer++;
+		gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(1)].numBattlesPlayer++;
+		if(!finishingBattle->isDraw())
+			gameHandler->gameState()->statistic.accumulatedValues[battle.sideToPlayer(finishingBattle->winnerSide)].numWinBattlesPlayer++;
+	}
+
 	BattleResultAccepted raccepted;
 	raccepted.battleID = battle.getBattle()->getBattleID();
 	raccepted.heroResult[0].army = const_cast<CArmedInstance*>(battle.battleGetArmyObject(BattleSide::ATTACKER));
@@ -556,10 +572,16 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleID & battleID, const
 	gameHandler->checkVictoryLossConditions(playerColors);
 
 	if (result.result == EBattleResult::SURRENDER)
+	{
+		gameHandler->gameState()->statistic.accumulatedValues[finishingBattle->loser].numHeroSurrendered++;
 		gameHandler->heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero);
+	}
 
 	if (result.result == EBattleResult::ESCAPE)
+	{
+		gameHandler->gameState()->statistic.accumulatedValues[finishingBattle->loser].numHeroEscaped++;
 		gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero);
+	}
 
 	if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty()
 		&& (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive))

+ 21 - 0
server/processors/PlayerMessageProcessor.cpp

@@ -29,6 +29,7 @@
 #include "../../lib/networkPacks/PacksForClient.h"
 #include "../../lib/networkPacks/StackLocation.h"
 #include "../../lib/serializer/Connection.h"
+#include "../lib/VCMIDirs.h"
 
 PlayerMessageProcessor::PlayerMessageProcessor(CGameHandler * gameHandler)
 	:gameHandler(gameHandler)
@@ -133,12 +134,30 @@ void PlayerMessageProcessor::commandCheaters(PlayerColor player, const std::vect
 		broadcastSystemMessage("No cheaters registered!");
 }
 
+void PlayerMessageProcessor::commandStatistic(PlayerColor player, const std::vector<std::string> & words)
+{
+	bool isHost = gameHandler->gameLobby()->isPlayerHost(player);
+	if(!isHost)
+		return;
+
+	const boost::filesystem::path outPath = VCMIDirs::get().userCachePath() / "statistic";
+	boost::filesystem::create_directories(outPath);
+
+	const boost::filesystem::path filePath = outPath / (vstd::getDateTimeISO8601Basic(std::time(nullptr)) + ".csv");
+	std::ofstream file(filePath.c_str());
+	std::string csv = gameHandler->gameState()->statistic.toCsv();
+	file << csv;
+
+	broadcastSystemMessage("Statistic files can be found in " + outPath.string() + " directory\n");
+}
+
 void PlayerMessageProcessor::commandHelp(PlayerColor player, const std::vector<std::string> & words)
 {
 	broadcastSystemMessage("Available commands to host:");
 	broadcastSystemMessage("'!exit' - immediately ends current game");
 	broadcastSystemMessage("'!kick <player>' - kick specified player from the game");
 	broadcastSystemMessage("'!save <filename>' - save game under specified filename");
+	broadcastSystemMessage("'!statistic' - save game statistics as csv file");
 	broadcastSystemMessage("Available commands to all players:");
 	broadcastSystemMessage("'!help' - display this help");
 	broadcastSystemMessage("'!cheaters' - list players that entered cheat command during game");
@@ -319,6 +338,8 @@ void PlayerMessageProcessor::handleCommand(PlayerColor player, const std::string
 		commandSave(player, words);
 	if(words[0] == "!cheaters")
 		commandCheaters(player, words);
+	if(words[0] == "!statistic")
+		commandStatistic(player, words);
 }
 
 void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroInstance * hero)

+ 1 - 0
server/processors/PlayerMessageProcessor.h

@@ -62,6 +62,7 @@ class PlayerMessageProcessor
 	void commandKick(PlayerColor player, const std::vector<std::string> & words);
 	void commandSave(PlayerColor player, const std::vector<std::string> & words);
 	void commandCheaters(PlayerColor player, const std::vector<std::string> & words);
+	void commandStatistic(PlayerColor player, const std::vector<std::string> & words);
 	void commandHelp(PlayerColor player, const std::vector<std::string> & words);
 	void commandVote(PlayerColor player, const std::vector<std::string> & words);
 

+ 205 - 0
test/battle/CBattleInfoCallbackTest.cpp

@@ -40,11 +40,32 @@ public:
 		bonusFake.addNewBonus(b);
 	}
 
+	void addCreatureAbility(BonusType bonusType)
+	{
+		addNewBonus(
+			std::make_shared<Bonus>(
+				BonusDuration::PERMANENT,
+				bonusType,
+				BonusSource::CREATURE_ABILITY,
+				0,
+				CreatureID(0)));
+	}
+
 	void makeAlive()
 	{
 		EXPECT_CALL(*this, alive()).WillRepeatedly(Return(true));
 	}
 
+	void setupPoisition(BattleHex pos)
+	{
+		EXPECT_CALL(*this, getPosition()).WillRepeatedly(Return(pos));
+	}
+
+	void makeDoubleWide()
+	{
+		EXPECT_CALL(*this, doubleWide()).WillRepeatedly(Return(true));
+	}
+
 	void makeWarMachine()
 	{
 		addNewBonus(std::make_shared<Bonus>(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, BonusSourceID()));
@@ -183,6 +204,190 @@ public:
 	}
 };
 
+class AttackableHexesTest : public CBattleInfoCallbackTest
+{
+public:
+	UnitFake & addRegularMelee(BattleHex hex, uint8_t side)
+	{
+		auto & unit = unitsFake.add(side);
+
+		unit.makeAlive();
+		unit.setDefaultState();
+		unit.setupPoisition(hex);
+		unit.redirectBonusesToFake();
+
+		return unit;
+	}
+
+	UnitFake & addDragon(BattleHex hex, uint8_t side)
+	{
+		auto & unit = addRegularMelee(hex, side);
+
+		unit.addCreatureAbility(BonusType::TWO_HEX_ATTACK_BREATH);
+		unit.makeDoubleWide();
+
+		return unit;
+	}
+
+	Units getAttackedUnits(UnitFake & attacker, UnitFake & defender, BattleHex defenderHex)
+	{
+		startBattle();
+		redirectUnitsToFake();
+
+		return subject.getAttackedBattleUnits(
+			&attacker, &defender,
+			defenderHex, false,
+			attacker.getPosition(),
+			defender.getPosition());
+	}
+};
+
+TEST_F(AttackableHexesTest, DragonRightRegular_RightHorithontalBreath)
+{
+	// X A D #
+	UnitFake & attacker = addDragon(35, 0);
+	UnitFake & defender = addRegularMelee(36, 1);
+	UnitFake & next = addRegularMelee(37, 1);
+
+	auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+	EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
+TEST_F(AttackableHexesTest, DragonDragonBottomRightHead_BottomRightBreathFromHead)
+{
+	// X A
+	//    D X		target D
+	//     #
+	UnitFake & attacker = addDragon(35, 0);
+	UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1);
+	UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1);
+	
+	auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+	EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
+TEST_F(AttackableHexesTest, DragonDragonVerticalDownHead_VerticalDownBreathFromHead)
+{
+	// X A
+	//  D X		target D
+	//   #
+	UnitFake & attacker = addDragon(35, 0);
+	UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), 1);
+	UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1);
+
+	auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+	EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
+TEST_F(AttackableHexesTest, DragonDragonVerticalDownHeadReverse_VerticalDownBreathFromHead)
+{
+	//  A X
+	// X D		target D
+	//  #
+	UnitFake & attacker = addDragon(36, 1);
+	UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 0);
+	UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), 0);
+
+	auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+	EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
+TEST_F(AttackableHexesTest, DragonDragonVerticalDownBack_VerticalDownBreath)
+{
+	//  X A
+	// D X		target X
+	//  #
+	UnitFake & attacker = addDragon(37, 0);
+	UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT), 1);
+	UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1);
+
+	auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex());
+
+	EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
+TEST_F(AttackableHexesTest, DragonDragonHeadBottomRight_BottomRightBreathFromHead)
+{
+	//  X A
+	// D X		target D
+	//  #
+	UnitFake & attacker = addDragon(37, 0);
+	UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT), 1);
+	UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1);
+
+	auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+	EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
+TEST_F(AttackableHexesTest, DragonVerticalDownDragonBackReverse_VerticalDownBreath)
+{
+	// A X
+	//  X D		target X
+	//   #
+	UnitFake & attacker = addDragon(36, 1);
+	UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_RIGHT), 0);
+	UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), 0);
+
+	auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex());
+
+	EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
+TEST_F(AttackableHexesTest, DragonRightBottomDragonHeadReverse_RightBottomBreathFromHeadHex)
+{
+	// A X
+	//  X D		target D
+	UnitFake & attacker = addDragon(36, 1);
+	UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_RIGHT), 0);
+	UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_LEFT), 0);
+
+	auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
+
+	EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
+TEST_F(AttackableHexesTest, DragonLeftBottomDragonBackToBack_LeftBottomBreathFromBackHex)
+{
+	//    X A
+	// D X		target X
+	//  #
+	UnitFake & attacker = addDragon(8, 0);
+	UnitFake & defender = addDragon(attacker.occupiedHex().cloneInDirection(BattleHex::BOTTOM_LEFT).cloneInDirection(BattleHex::LEFT), 1);
+	UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), 1);
+
+	auto attacked = getAttackedUnits(attacker, defender, defender.occupiedHex());
+
+	EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
+TEST_F(AttackableHexesTest, DefenderPositionOverride_BreathCountsHypoteticDefenderPosition)
+{
+	//  # N
+	// X D		target D
+	//  A X
+	UnitFake & attacker = addDragon(35, 1);
+	UnitFake & defender = addDragon(8, 0);
+	UnitFake & next = addDragon(2, 0);
+
+	startBattle();
+	redirectUnitsToFake();
+
+	auto attacked = subject.getAttackedBattleUnits(
+		&attacker,
+		&defender,
+		19,
+		false,
+		attacker.getPosition(),
+		19);
+
+	EXPECT_TRUE(vstd::contains(attacked, &next));
+}
+
 class BattleFinishedTest : public CBattleInfoCallbackTest
 {
 public: