Переглянути джерело

Merge pull request #4502 from IvanSavenko/town_building_config

Moddable town buildings improvements
Ivan Savenko 1 рік тому
батько
коміт
247be94015

+ 1 - 1
client/CPlayerInterface.cpp

@@ -1673,7 +1673,7 @@ void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroI
 	auto onWindowClosed = [this, queryID](){
 		cb->selectionMade(0, queryID);
 	};
-	GH.windows().createAndPushWindow<CUniversityWindow>(visitor, market, onWindowClosed);
+	GH.windows().createAndPushWindow<CUniversityWindow>(visitor, BuildingID::NONE, market, onWindowClosed);
 }
 
 void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor)

+ 101 - 90
client/windows/CCastleInterface.cpp

@@ -145,7 +145,7 @@ void CBuildingRect::clickPressed(const Point & cursorPosition)
 	if(getBuilding() && area && (parent->selectedBuilding==this))
 	{
 		auto building = getBuilding();
-		parent->buildingClicked(building->bid, building->subId, building->upgrade);
+		parent->buildingClicked(building->bid);
 	}
 }
 
@@ -681,18 +681,76 @@ const CGHeroInstance * CCastleBuildings::getHero()
 		return town->garrisonHero;
 }
 
-void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades)
+void CCastleBuildings::buildingClicked(BuildingID building)
 {
-	logGlobal->trace("You've clicked on %d", (int)building.toEnum());
-	const CBuilding *b = town->town->buildings.find(building)->second;
+	BuildingID buildingToEnter = building;
+	for(;;)
+	{
+		const CBuilding *b = town->town->buildings.find(buildingToEnter)->second;
+
+		if (buildingTryActivateCustomUI(buildingToEnter, building))
+			return;
+
+		if (!b->upgrade.hasValue())
+		{
+			enterBuilding(building);
+			return;
+		}
+
+		buildingToEnter = b->upgrade;
+	}
+}
 
-	if (building >= BuildingID::DWELL_FIRST)
+bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, BuildingID buildingTarget)
+{
+	logGlobal->trace("You've clicked on %d", (int)buildingToTest.toEnum());
+	const CBuilding *b = town->town->buildings.at(buildingToTest);
+
+	if (town->getWarMachineInBuilding(buildingToTest).hasValue())
 	{
-		enterDwelling((BuildingID::getLevelFromDwelling(building)));
+		enterBlacksmith(buildingTarget, town->getWarMachineInBuilding(buildingToTest));
+		return true;
+	}
+
+	if (!b->marketModes.empty())
+	{
+		switch (*b->marketModes.begin())
+		{
+			case EMarketMode::CREATURE_UNDEAD:
+				GH.windows().createAndPushWindow<CTransformerWindow>(town, getHero(), nullptr);
+				return true;
+
+			case EMarketMode::RESOURCE_SKILL:
+				if (getHero())
+					GH.windows().createAndPushWindow<CUniversityWindow>(getHero(), buildingTarget, town, nullptr);
+				return true;
+
+			case EMarketMode::RESOURCE_RESOURCE:
+				// can't use allied marketplace
+				if (town->getOwner() == LOCPLINT->playerID)
+				{
+					GH.windows().createAndPushWindow<CMarketWindow>(town, getHero(), nullptr, *b->marketModes.begin());
+					return true;
+				}
+				else
+					return false;
+			default:
+				if(getHero())
+					GH.windows().createAndPushWindow<CMarketWindow>(town, getHero(), nullptr, *b->marketModes.begin());
+				else
+					LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
+				return true;
+		}
+	}
+
+	if (buildingToTest >= BuildingID::DWELL_FIRST)
+	{
+		enterDwelling((BuildingID::getLevelFromDwelling(buildingToTest)));
+		return true;
 	}
 	else
 	{
-		switch(building)
+		switch(buildingToTest)
 		{
 		case BuildingID::MAGES_GUILD_1:
 		case BuildingID::MAGES_GUILD_2:
@@ -700,139 +758,91 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil
 		case BuildingID::MAGES_GUILD_4:
 		case BuildingID::MAGES_GUILD_5:
 				enterMagesGuild();
-				break;
+				return true;
 
 		case BuildingID::TAVERN:
 				LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE);
-				break;
+				return true;
 
 		case BuildingID::SHIPYARD:
 				if(town->shipyardStatus() == IBoatGenerator::GOOD)
+				{
 					LOCPLINT->showShipyardDialog(town);
+					return true;
+				}
 				else if(town->shipyardStatus() == IBoatGenerator::BOAT_ALREADY_BUILT)
+				{
 					LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]);
-				break;
+					return true;
+				}
+				return false;
 
 		case BuildingID::FORT:
 		case BuildingID::CITADEL:
 		case BuildingID::CASTLE:
 				GH.windows().createAndPushWindow<CFortScreen>(town);
-				break;
+				return true;
 
 		case BuildingID::VILLAGE_HALL:
 		case BuildingID::CITY_HALL:
 		case BuildingID::TOWN_HALL:
 		case BuildingID::CAPITOL:
 				enterTownHall();
-				break;
-
-		case BuildingID::MARKETPLACE:
-				// can't use allied marketplace
-				if (town->getOwner() == LOCPLINT->playerID)
-					GH.windows().createAndPushWindow<CMarketWindow>(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE);
-				else
-					enterBuilding(building);
-				break;
-
-		case BuildingID::BLACKSMITH:
-				enterBlacksmith(town->town->warMachine);
-				break;
+				return true;
 
 		case BuildingID::SHIP:
 			LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); //Cannot build another boat
-			break;
+			return true;
 
 		case BuildingID::SPECIAL_1:
 		case BuildingID::SPECIAL_2:
 		case BuildingID::SPECIAL_3:
 		case BuildingID::SPECIAL_4:
-				switch (subID)
+				switch (b->subId)
 				{
-				case BuildingSubID::NONE:
-						enterBuilding(building);
-						break;
-
 				case BuildingSubID::MYSTIC_POND:
-						enterFountain(building, subID, upgrades);
-						break;
-
-				case BuildingSubID::ARTIFACT_MERCHANT:
-						if(town->visitingHero)
-							GH.windows().createAndPushWindow<CMarketWindow>(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_ARTIFACT);
-						else
-							LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
-						break;
-
-				case BuildingSubID::FOUNTAIN_OF_FORTUNE:
-						enterFountain(building, subID, upgrades);
-					break;
-
-				case BuildingSubID::FREELANCERS_GUILD:
-						if(getHero())
-							GH.windows().createAndPushWindow<CMarketWindow>(town, getHero(), nullptr, EMarketMode::CREATURE_RESOURCE);
-						else
-							LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
-						break;
-
-				case BuildingSubID::MAGIC_UNIVERSITY:
-						if (getHero())
-							GH.windows().createAndPushWindow<CUniversityWindow>(getHero(), town, nullptr);
-						else
-							enterBuilding(building);
-						break;
+						enterFountain(buildingToTest, b->subId, buildingTarget);
+						return true;
 
 				case BuildingSubID::CASTLE_GATE:
 						if (LOCPLINT->makingTurn)
+						{
 							enterCastleGate();
-						else
-							enterBuilding(building);
-						break;
-
-				case BuildingSubID::CREATURE_TRANSFORMER: //Skeleton Transformer
-						GH.windows().createAndPushWindow<CTransformerWindow>(town, getHero(), nullptr);
-						break;
+							return true;
+						}
+						return false;
 
 				case BuildingSubID::PORTAL_OF_SUMMONING:
 						if (town->creatures[town->town->creatures.size()].second.empty())//No creatures
 							LOCPLINT->showInfoDialog(CGI->generaltexth->tcommands[30]);
 						else
 							enterDwelling(town->town->creatures.size());
-						break;
-
-				case BuildingSubID::BALLISTA_YARD:
-						enterBlacksmith(ArtifactID::BALLISTA);
-						break;
-
-				case BuildingSubID::THIEVES_GUILD:
-						enterAnyThievesGuild();
-						break;
+						return true;
 
 				case BuildingSubID::BANK:
 						enterBank();
-						break;
-
-				default:
-					if(upgrades == BuildingID::TAVERN)
-						LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE);
-					else
-						enterBuilding(building);
-					break;
+						return true;
 				}
-				break;
+		}
+	}
 
-		default:
-				enterBuilding(building);
-				break;
+	for (auto const & bonus : b->buildingBonuses)
+	{
+		if (bonus->type == BonusType::THIEVES_GUILD_ACCESS)
+		{
+			enterAnyThievesGuild();
+			return true;
 		}
 	}
+	return false;
 }
 
-void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
+void CCastleBuildings::enterBlacksmith(BuildingID building, ArtifactID artifactID)
 {
 	const CGHeroInstance *hero = town->visitingHero;
 	if(!hero)
 	{
-		LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->town->buildings.find(BuildingID::BLACKSMITH)->second->getNameTranslated()));
+		LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->town->buildings.find(building)->second->getNameTranslated()));
 		return;
 	}
 	auto art = artifactID.toArtifact();
@@ -843,7 +853,7 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
 	{
 		for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO))
 		{
-			if(hero->getArt(slot) == nullptr)
+			if(hero->getArt(slot) == nullptr || hero->getArt(slot)->getTypeId() != artifactID)
 			{
 				possible = true;
 				break;
@@ -854,8 +864,9 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
 			}
 		}
 	}
-	CreatureID cre = art->getWarMachine();
-	GH.windows().createAndPushWindow<CBlacksmithDialog>(possible, cre, artifactID, hero->id);
+
+	CreatureID creatureID = artifactID.toArtifact()->getWarMachine();
+	GH.windows().createAndPushWindow<CBlacksmithDialog>(possible, creatureID, artifactID, hero->id);
 }
 
 void CCastleBuildings::enterBuilding(BuildingID building)

+ 3 - 2
client/windows/CCastleInterface.h

@@ -150,7 +150,7 @@ class CCastleBuildings : public CIntObject
 
 	const CGHeroInstance* getHero();//Select hero for buildings usage
 
-	void enterBlacksmith(ArtifactID artifactID);//support for blacksmith + ballista yard
+	void enterBlacksmith(BuildingID building, ArtifactID artifactID);//support for blacksmith + ballista yard
 	void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages
 	void enterCastleGate();
 	void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains
@@ -173,7 +173,8 @@ public:
 	void enterBank();
 	void enterToTheQuickRecruitmentWindow();
 
-	void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE);
+	bool buildingTryActivateCustomUI(BuildingID buildingToTest, BuildingID buildingTarget);
+	void buildingClicked(BuildingID building);
 	void addBuilding(BuildingID building);
 	void removeBuilding(BuildingID building);//FIXME: not tested!!!
 };

+ 2 - 3
client/windows/GUIClasses.cpp

@@ -946,7 +946,7 @@ void CUniversityWindow::CItem::hover(bool on)
 		GH.statusbar()->clear();
 }
 
-CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function<void()> & onWindowClosed)
+CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, BuildingID building, const IMarket * _market, const std::function<void()> & onWindowClosed)
 	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("UNIVERS1")),
 	hero(_hero),
 	onWindowClosed(onWindowClosed),
@@ -961,8 +961,7 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket
 	if(auto town = dynamic_cast<const CGTownInstance *>(_market))
 	{
 		auto faction = town->town->faction->getId();
-		auto bid = town->town->getSpecialBuilding(BuildingSubID::MAGIC_UNIVERSITY)->bid;
-		titlePic = std::make_shared<CAnimImage>((*CGI->townh)[faction]->town->clientInfo.buildingsIcons, bid);
+		titlePic = std::make_shared<CAnimImage>((*CGI->townh)[faction]->town->clientInfo.buildingsIcons, building);
 	}
 	else if(auto uni = dynamic_cast<const CGUniversity *>(_market); uni->appearance)
 	{

+ 1 - 1
client/windows/GUIClasses.h

@@ -391,7 +391,7 @@ class CUniversityWindow final : public CStatusbarWindow, public IMarketHolder
 	std::function<void()> onWindowClosed;
 
 public:
-	CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function<void()> & onWindowClosed);
+	CUniversityWindow(const CGHeroInstance * _hero, BuildingID building, const IMarket * _market, const std::function<void()> & onWindowClosed);
 
 	void makeDeal(SecondarySkill skill);
 	void close() override;

+ 43 - 1
config/buildingsLibrary.json

@@ -10,6 +10,11 @@
 			{
 				"type": "MORALE",
 				"val": 1
+			},
+			{
+				"propagator": "PLAYER_PROPAGATOR",
+				"type": "THIEVES_GUILD_ACCESS",
+				"val": 1
 			}
 		]
 	},
@@ -43,7 +48,10 @@
 		"produce": { "gold": 4000 }
 	},
 
-	"marketplace":    { "id" : 14 },
+	"marketplace":    { 
+		"id" : 14,
+		"marketModes" : ["resource-resource", "resource-player"]
+	},
 	"resourceSilo":   { "id" : 15, "requires" : [ "marketplace" ] },
 	"blacksmith":     { "id" : 16 },
 
@@ -198,5 +206,39 @@
 				}
 			]
 		}
+	},
+	
+	// Section 3 - markets
+	"artifactMerchant" : {
+		"requires" : [ "marketplace" ],
+		"marketModes" : ["resource-artifact", "artifact-resource"]
+	},
+	
+	"freelancersGuild" : {
+		"requires" : [ "marketplace" ],
+		"marketModes" : ["creature-resource"]
+	},
+	
+	"magicUniversity" : {
+		"marketModes" : ["resource-skill"]
+	},
+	
+	"creatureTransformer" : {
+		"marketModes" : ["creature-undead"]
+	},
+	
+	// Section 4 - buildings that now have dedicated mechanics
+	"ballistaYard": {
+		"blacksmith" : "ballista"
+	},
+	
+	"thievesGuild" : {
+		"bonuses": [
+			{
+				"propagator": "PLAYER_PROPAGATOR",
+				"type": "THIEVES_GUILD_ACCESS",
+				"val": 2
+			}
+		]
 	}
 }

+ 2 - 3
config/factions/castle.json

@@ -147,7 +147,6 @@
 			],
 			"horde" : [ 2, -1 ],
 			"mageGuild" : 4,
-			"warMachine" : "ballista",
 			"moatAbility" : "castleMoat",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
@@ -166,9 +165,9 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "ore": 1, "wood": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ballista" },
 
 				"special1":       { 
 					"bonuses": [

+ 4 - 5
config/factions/conflux.json

@@ -152,7 +152,6 @@
 			"horde" : [ 0, -1 ],
 			"mageGuild" : 5,
 			"primaryResource" : "mercury",
-			"warMachine" : "ballista",
 			"moatAbility" : "castleMoat",
 
 			"buildings" :
@@ -171,15 +170,15 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "mercury": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ballista" },
 
-				"special1":       { "type" : "artifactMerchant", "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
+				"special1":       { "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
 				"ship":           { "id" : 20, "upgrades" : "shipyard" },
-				"special2":       { "type" : "magicUniversity", "requires" : [ "mageGuild1" ] },
+				"special2":       { "requires" : [ "mageGuild1" ], "marketModes" : ["resource-skill"] },
 				"grail":          { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }},
 				"extraTownHall":  { "id" : 27, "requires" : [ "townHall" ], "mode" : "auto" },
 				"extraCityHall":  { "id" : 28, "requires" : [ "cityHall" ], "mode" : "auto" },

+ 3 - 5
config/factions/dungeon.json

@@ -148,10 +148,8 @@
 			"horde" : [ 0, -1 ],
 			"mageGuild" : 5,
 			"primaryResource" : "sulfur",
-			"warMachine" : "ballista",
 			"moatAbility" : "dungeonMoat",
 
-
 			"buildings" :
 			{
 				"mageGuild1":     { },
@@ -167,11 +165,11 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "sulfur": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ballista" },
 
-				"special1":       { "type" : "artifactMerchant", "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
+				"special1":       { "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
 				"special2":       {

+ 2 - 3
config/factions/fortress.json

@@ -147,7 +147,6 @@
 			],
 			"horde" : [ 0, -1 ],
 			"mageGuild" : 3,
-			"warMachine" : "firstAidTent",
 			"moatAbility" : "fortressMoat",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
@@ -165,9 +164,9 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "wood": 1, "ore": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "firstAidTent" },
 
 				"special1":       {
 					"requires" : [ "allOf", [ "townHall" ], [ "special2" ] ],

+ 2 - 3
config/factions/inferno.json

@@ -149,7 +149,6 @@
 			"horde" : [ 0, 2 ],
 			"mageGuild" : 5,
 			"primaryResource" : "mercury",
-			"warMachine" : "ammoCart",
 			"moatAbility" : "infernoMoat",
 
 			"buildings" :
@@ -167,9 +166,9 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "mercury": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ammoCart" },
 
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },

+ 3 - 4
config/factions/necropolis.json

@@ -152,7 +152,6 @@
 			],
 			"horde" : [ 0, -1 ],
 			"mageGuild" : 5,
-			"warMachine" : "firstAidTent",
 			"moatAbility" : "necropolisMoat",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
@@ -172,9 +171,9 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "ore": 1, "wood": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "firstAidTent" },
 
 				"special1":       { "requires" : [ "fort" ], "bonuses": [ { "type": "DARKNESS", "val": 20  } ] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1", "requires" : [ "special3" ] },
@@ -182,7 +181,7 @@
 				"ship":           { "id" : 20, "upgrades" : "shipyard" },
 				"special2":       { "requires" : [ "mageGuild1" ],
 					"bonuses": [ { "type": "UNDEAD_RAISE_PERCENTAGE", "val": 10, "propagator": "PLAYER_PROPAGATOR" } ] },
-				"special3":       { "type" : "creatureTransformer", "requires" : [ "dwellingLvl1" ], "marketModes" : ["creature-undead"] },
+				"special3":       { "requires" : [ "dwellingLvl1" ], "marketModes" : ["creature-undead"] },
 				"grail":          { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 },
 					"bonuses": [ { "type": "UNDEAD_RAISE_PERCENTAGE", "val": 20, "propagator": "PLAYER_PROPAGATOR" } ] },
 

+ 2 - 3
config/factions/rampart.json

@@ -152,7 +152,6 @@
 			"horde" : [ 1, 4 ],
 			"mageGuild" : 5,
 			"primaryResource" : "crystal",
-			"warMachine" : "firstAidTent",
 			"moatAbility" : "rampartMoat",
 
 			"buildings" :
@@ -170,9 +169,9 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "crystal": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "firstAidTent" },
 
 				"special1":       { "type" : "mysticPond" },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl2" },

+ 4 - 5
config/factions/stronghold.json

@@ -145,7 +145,6 @@
 			],
 			"horde" : [ 0, -1 ],
 			"mageGuild" : 3,
-			"warMachine" : "ammoCart",
 			"moatAbility" : "strongholdMoat",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
@@ -162,15 +161,15 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "ore": 1, "wood": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ammoCart" },
 
 				"special1":       { "type" : "escapeTunnel", "requires" : [ "fort" ] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
-				"special2":       { "type" : "freelancersGuild", "requires" : [ "marketplace" ], "marketModes" : ["creature-resource"] },
-				"special3":       { "type" : "ballistaYard", "requires" : [ "blacksmith" ] },
+				"special2":       { "requires" : [ "marketplace" ], "marketModes" : ["creature-resource"] },
+				"special3":       { "warMachine" : "ballista", "requires" : [ "blacksmith" ] },
 				"special4":       { 
 					"requires" : [ "fort" ],
 					"configuration" : {

+ 3 - 4
config/factions/tower.json

@@ -147,7 +147,6 @@
 			"horde" : [ 1, -1 ],
 			"primaryResource" : "gems",
 			"mageGuild" : 5,
-			"warMachine" : "ammoCart",
 			"moatAbility" : "towerMoat",
 
 			"buildings" :
@@ -165,11 +164,11 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce" : { "gems": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ammoCart" },
 
-				"special1":       { "type" : "artifactMerchant", "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
+				"special1":       { "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl2" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl2", "requires" : [ "horde1" ], "mode" : "auto" },
 				"special2":       { "height" : "high", "requires" : [ "fort" ] },

+ 1 - 5
config/schemas/faction.json

@@ -89,7 +89,7 @@
 			"additionalProperties" : false,
 			"required" : [
 				"mapObject", "buildingsIcons", "buildings", "creatures", "guildWindow", "names",
-				"hallBackground", "hallSlots", "horde", "mageGuild", "moatAbility", "defaultTavern", "tavernVideo", "guildBackground", "musicTheme", "siege", "structures", "townBackground", "warMachine"
+				"hallBackground", "hallSlots", "horde", "mageGuild", "moatAbility", "defaultTavern", "tavernVideo", "guildBackground", "musicTheme", "siege", "structures", "townBackground"
 			],
 			"description" : "town",
 			"properties" : {
@@ -133,10 +133,6 @@
 					"type" : "string",
 					"description" : "Primary resource for this town. Produced by Silo and offered as starting bonus"
 				},
-				"warMachine" : {
-					"type" : "string",
-					"description" : "Identifier of war machine produced by blacksmith in town"
-				},
 				"horde" : {
 					"type" : "array",
 					"maxItems" : 2,

+ 9 - 2
config/schemas/townBuilding.json

@@ -36,7 +36,7 @@
 		},
 		"type" : {
 			"type" : "string",
-			"enum" : [ "mysticPond", "artifactMerchant", "freelancersGuild", "magicUniversity", "castleGate", "creatureTransformer", "portalOfSummoning", "ballistaYard", "library", "escapeTunnel", "treasury", "thievesGuild", "bank" ],
+			"enum" : [ "mysticPond", "castleGate", "portalOfSummoning", "library", "escapeTunnel", "treasury", "bank" ],
 			"description" : "Subtype for some special buildings"
 		},
 		"mode" : {
@@ -93,6 +93,10 @@
 				"gems" :    { "type" : "number"}
 			}
 		},
+		"warMachine" : {
+			"type" : "string",
+			"description" : "Artifact ID of a war machine that can be purchased in this building, if any"
+		},
 		"bonuses" : {
 			"type" : "array",
 			"description" : "Bonuses that are provided by this building in any town where this building has been built. Only affects town itself (including siege), to propagate effect to player or team please use bonus propagators",
@@ -100,7 +104,10 @@
 		},
 		"marketModes" : {
 			"type" : "array",
-			"enum" : [ "resource-resource", "resource-player", "creature-resource", "resource-artifact", "artifact-resource", "artifact-experience", "creature-experience", "creature-undead", "resource-skill"],
+			"items" : {
+				"type" : "string",
+				"enum" : [ "resource-resource", "resource-player", "creature-resource", "resource-artifact", "artifact-resource", "artifact-experience", "creature-experience", "creature-undead", "resource-skill"],
+			},
 			"description" : "List of modes available in this market"
 		}
 	}

+ 4 - 2
docs/modders/Bonus/Bonus_Types.md

@@ -1010,9 +1010,11 @@ Dummy bonus that acts as marker for Dendroid's Bind ability
 
 Dummy skill for alternative upgrades mod
 
-### TOWN_MAGIC_WELL
+### THIEVES_GUILD_ACCESS
 
-Internal bonus, do not use
+Increases amount of information available in affected thieves guild (in town or in adventure map tavern). Does not affects adventure map object "Den of Thieves". You may want to use PLAYER_PROPAGATOR with this bonus to make its effect player wide.
+
+- val: additional number of 'levels' of information to grant access to
 
 ### LEVEL_COUNTER
 

+ 0 - 3
docs/modders/Entities_Format/Faction_Format.md

@@ -247,9 +247,6 @@ Each town requires a set of buildings (Around 30-45 buildings)
 	// maximum level of mage guild
 	"mageGuild" : 4,
 
-	// war machine produced in town
-	"warMachine" : "ballista"
-	
 	// Identifier of spell that will create effects for town moat during siege
 	"moatAbility" : "castleMoat"
 }

+ 3 - 2
docs/modders/Entities_Format/Town_Building_Format.md

@@ -136,6 +136,9 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config
 		"gold" : 10000
 	}, 
 	
+	// Artifact ID of a war machine produced in this town building, if any
+	"warMachine" : "ballista",
+	
 	// Allows to define additional functionality of this building, usually using logic of one of original H3 town building
 	// Generally only needs to be specified for "special" buildings
 	// See 'List of unique town buildings' section below for detailed description of this field
@@ -207,14 +210,12 @@ Following Heroes III buildings can be used as unique buildings for a town. Their
 - `castleGate`
 - `creatureTransformer`
 - `portalOfSummoning`
-- `ballistaYard`
 - `library`
 - `escapeTunnel`
 - `treasury`
 
 #### Buildings from other Heroes III mods
 Following HotA buildings can be used as unique building for a town. Functionality should match corresponding HotA building:
-- `thievesGuild`
 - `bank`
 
 #### Custom buildings

+ 1 - 1
lib/CArtHandler.cpp

@@ -460,7 +460,7 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
 	}
 
 	const JsonNode & warMachine = node["warMachine"];
-	if(warMachine.getType() == JsonNode::JsonType::DATA_STRING && !warMachine.String().empty())
+	if(!warMachine.isNull())
 	{
 		VLC->identifiers()->requestIdentifier("creature", warMachine, [=](si32 id)
 		{

+ 1 - 9
lib/CGameInfoCallback.cpp

@@ -236,15 +236,7 @@ void CGameInfoCallback::getThievesGuildInfo(SThievesGuildInfo & thi, const CGObj
 
 	if(obj->ID == Obj::TOWN || obj->ID == Obj::TAVERN)
 	{
-		int taverns = 0;
-		for(auto town : gs->players[*getPlayerID()].getTowns())
-		{
-			if(town->hasBuilt(BuildingID::TAVERN))
-				taverns++;
-			
-			if(town->hasBuilt(BuildingSubID::THIEVES_GUILD))
-				taverns += 2;
-		}
+		int taverns = gs->players[*getPlayerID()].valOfBonuses(BonusType::THIEVES_GUILD_ACCESS);
 		gs->obtainPlayersStats(thi, taverns);
 	}
 	else if(obj->ID == Obj::DEN_OF_THIEVES)

+ 1 - 1
lib/bonuses/BonusEnum.h

@@ -150,7 +150,7 @@ class JsonNode;
 	BONUS_NAME(GARGOYLE) /* gargoyle is special than NON_LIVING, cannot be rised or healed */ \
 	BONUS_NAME(SPECIAL_ADD_VALUE_ENCHANT) /*specialty spell like Aenin has, increased effect of spell, additionalInfo = value to add*/\
 	BONUS_NAME(SPECIAL_FIXED_VALUE_ENCHANT) /*specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix.*/\
-	BONUS_NAME(TOWN_MAGIC_WELL) /*one-time pseudo-bonus to implement Magic Well in the town*/\
+	BONUS_NAME(THIEVES_GUILD_ACCESS) \
 	BONUS_NAME(LIMITED_SHOOTING_RANGE) /*limits range of shooting creatures, doesn't adjust any other mechanics (half vs full damage etc). val - range in hexes, additional info - optional new range for broken arrow mechanic */\
 	BONUS_NAME(LEARN_BATTLE_SPELL_CHANCE) /*skill-agnostic eagle eye chance. subtype = 0 - from enemy, 1 - TODO: from entire battlefield*/\
 	BONUS_NAME(LEARN_BATTLE_SPELL_LEVEL_LIMIT) /*skill-agnostic eagle eye limit, subtype - school (-1 for all), others TODO*/\

+ 0 - 7
lib/constants/Enumerations.h

@@ -26,18 +26,11 @@ namespace BuildingSubID
 		DEFAULT = -50,
 		NONE = -1,
 		CASTLE_GATE,
-		CREATURE_TRANSFORMER,
 		MYSTIC_POND,
-		FOUNTAIN_OF_FORTUNE,
-		ARTIFACT_MERCHANT,
 		LIBRARY,
 		PORTAL_OF_SUMMONING,
 		ESCAPE_TUNNEL,
-		FREELANCERS_GUILD,
-		BALLISTA_YARD,
-		MAGIC_UNIVERSITY,
 		TREASURY,
-		THIEVES_GUILD,
 		BANK
 	};
 }

+ 0 - 7
lib/constants/StringConstants.h

@@ -178,18 +178,11 @@ namespace MappedKeys
 	static const std::map<std::string, BuildingSubID::EBuildingSubID> SPECIAL_BUILDINGS =
 	{
 		{ "mysticPond", BuildingSubID::MYSTIC_POND },
-		{ "artifactMerchant", BuildingSubID::ARTIFACT_MERCHANT },
-		{ "freelancersGuild", BuildingSubID::FREELANCERS_GUILD },
-		{ "magicUniversity", BuildingSubID::MAGIC_UNIVERSITY },
 		{ "castleGate", BuildingSubID::CASTLE_GATE },
-		{ "creatureTransformer", BuildingSubID::CREATURE_TRANSFORMER },//only skeleton transformer yet
 		{ "portalOfSummoning", BuildingSubID::PORTAL_OF_SUMMONING },
-		{ "ballistaYard", BuildingSubID::BALLISTA_YARD },
 		{ "library", BuildingSubID::LIBRARY },
-		{ "fountainOfFortune", BuildingSubID::FOUNTAIN_OF_FORTUNE },//luck garrison bonus
 		{ "escapeTunnel", BuildingSubID::ESCAPE_TUNNEL },
 		{ "treasury", BuildingSubID::TREASURY },
-		{ "thievesGuild", BuildingSubID::THIEVES_GUILD },
 		{ "bank", BuildingSubID::BANK }
 	};
 

+ 1 - 0
lib/entities/building/CBuilding.h

@@ -34,6 +34,7 @@ public:
 	TResources resources;
 	TResources produce;
 	TRequired requirements;
+	ArtifactID warMachine;
 	std::set<EMarketMode> marketModes;
 
 	BuildingID bid; //structure ID

+ 1 - 1
lib/entities/faction/CTown.h

@@ -69,7 +69,7 @@ public:
 	std::map<int,int> hordeLvl; //[0] - first horde building creature level; [1] - second horde building (-1 if not present)
 	ui32 mageLevel; //max available mage guild level
 	GameResID primaryRes;
-	ArtifactID warMachine;
+	CreatureID warMachineDeprecated;
 	SpellID moatAbility;
 
 	// default chance for hero of specific class to appear in tavern, if field "tavern" was not set

+ 15 - 23
lib/entities/faction/CTownHandler.cpp

@@ -324,6 +324,14 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 	}
 	loadBuildingRequirements(ret, source["requires"], requirementsToLoad);
 
+	if (!source["warMachine"].isNull())
+	{
+		VLC->identifiers()->requestIdentifier("artifact", source["warMachine"], [=](si32 identifier)
+		{
+			ret->warMachine = ArtifactID(identifier);
+		});
+	}
+
 	if (!source["upgrades"].isNull())
 	{
 		// building id and upgrades can't be the same
@@ -552,7 +560,13 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source)
 	else
 		town->primaryRes = GameResID(resIter - std::begin(GameConstants::RESOURCE_NAMES));
 
-	warMachinesToLoad[town] = source["warMachine"];
+	if (!source["warMachine"].isNull())
+	{
+		VLC->identifiers()->requestIdentifier( "creature", source["warMachine"], [=](si32 creatureID)
+		{
+			town->warMachineDeprecated = creatureID;
+		});
+	}
 
 	town->mageLevel = static_cast<ui32>(source["mageGuild"].Float());
 
@@ -848,7 +862,6 @@ void CTownHandler::beforeValidate(JsonNode & object)
 void CTownHandler::afterLoadFinalization()
 {
 	initializeRequirements();
-	initializeWarMachines();
 }
 
 void CTownHandler::initializeRequirements()
@@ -878,27 +891,6 @@ void CTownHandler::initializeRequirements()
 	requirementsToLoad.clear();
 }
 
-void CTownHandler::initializeWarMachines()
-{
-	// must be done separately after all objects are loaded
-	for(auto & p : warMachinesToLoad)
-	{
-		CTown * t = p.first;
-		JsonNode creatureKey = p.second;
-
-		auto ret = VLC->identifiers()->getIdentifier("creature", creatureKey, false);
-
-		if(ret)
-		{
-			const CCreature * creature = CreatureID(*ret).toCreature();
-
-			t->warMachine = creature->warMachine;
-		}
-	}
-
-	warMachinesToLoad.clear();
-}
-
 std::set<FactionID> CTownHandler::getDefaultAllowed() const
 {
 	std::set<FactionID> allowedFactions;

+ 0 - 2
lib/entities/faction/CTownHandler.h

@@ -34,14 +34,12 @@ class DLL_LINKAGE CTownHandler : public CHandlerBase<FactionID, Faction, CFactio
 		CTown * town;
 	};
 
-	std::map<CTown *, JsonNode> warMachinesToLoad;
 	std::vector<BuildingRequirementsHelper> requirementsToLoad;
 	std::vector<BuildingRequirementsHelper> overriddenBidsToLoad; //list of buildings, which bonuses should be overridden.
 
 	static const TPropagatorPtr & emptyPropagator();
 
 	void initializeRequirements();
-	void initializeWarMachines();
 
 	/// loads CBuilding's into town
 	void loadBuildingRequirements(CBuilding * building, const JsonNode & source, std::vector<BuildingRequirementsHelper> & bidsToLoad) const;

+ 28 - 6
lib/mapObjects/CGTownInstance.cpp

@@ -44,13 +44,10 @@ int CGTownInstance::getSightRadius() const //returns sight distance
 
 	for(const auto & bid : builtBuildings)
 	{
-		if(bid.IsSpecialOrGrail())
-		{
-			auto height = town->buildings.at(bid)->height;
-			if(ret < height)
-				ret = height;
+		auto height = town->buildings.at(bid)->height;
+		if(ret < height)
+			ret = height;
 	}
-}
 	return ret;
 }
 
@@ -1176,6 +1173,31 @@ TerrainId CGTownInstance::getNativeTerrain() const
 	return town->faction->getNativeTerrain();
 }
 
+ArtifactID CGTownInstance::getWarMachineInBuilding(BuildingID building) const
+{
+	if (builtBuildings.count(building) == 0)
+		return ArtifactID::NONE;
+
+	if (building == BuildingID::BLACKSMITH && town->warMachineDeprecated.hasValue())
+		return town->warMachineDeprecated.toCreature()->warMachine;
+
+	return town->buildings.at(building)->warMachine;
+}
+
+bool CGTownInstance::isWarMachineAvailable(ArtifactID warMachine) const
+{
+	for (auto const & buildingID : builtBuildings)
+		if (town->buildings.at(buildingID)->warMachine == warMachine)
+			return true;
+
+	if (builtBuildings.count(BuildingID::BLACKSMITH) &&
+	   town->warMachineDeprecated.hasValue() &&
+	   town->warMachineDeprecated.toCreature()->warMachine == warMachine)
+		return true;
+
+	return false;
+}
+
 GrowthInfo::Entry::Entry(const std::string &format, int _count)
 	: count(_count)
 {

+ 5 - 0
lib/mapObjects/CGTownInstance.h

@@ -208,6 +208,11 @@ public:
 	FactionID getFaction() const override;
 	TerrainId getNativeTerrain() const override;
 
+	/// Returns ID of war machine that is produced by specified building or NONE if this is not built or if building does not produce war machines
+	ArtifactID getWarMachineInBuilding(BuildingID) const;
+	/// Returns true if provided war machine is available in any of built buildings of this town
+	bool isWarMachineAvailable(ArtifactID) const;
+
 	CGTownInstance(IGameCallback *cb);
 	virtual ~CGTownInstance();
 

+ 12 - 2
server/CGameHandler.cpp

@@ -2798,9 +2798,19 @@ bool CGameHandler::buyArtifact(ObjectInstanceID hid, ArtifactID aid)
 		const int price = art->getPrice();
 		COMPLAIN_RET_FALSE_IF(getPlayerState(hero->getOwner())->resources[EGameResID::GOLD] < price, "Not enough gold!");
 
-		if ((town->hasBuilt(BuildingID::BLACKSMITH) && town->town->warMachine == aid)
-		 || (town->hasBuilt(BuildingSubID::BALLISTA_YARD) && aid == ArtifactID::BALLISTA))
+		if(town->isWarMachineAvailable(aid))
 		{
+			bool hasFreeSlot = false;
+			for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO))
+				if (hero->getArt(slot) == nullptr)
+					hasFreeSlot = true;
+
+			if (!hasFreeSlot)
+			{
+				auto slot = art->getPossibleSlots().at(ArtBearer::HERO).front();
+				removeArtifact(ArtifactLocation(hero->id, slot));
+			}
+
 			giveResource(hero->getOwner(),EGameResID::GOLD,-price);
 			return giveHeroNewArtifact(hero, art);
 		}