Browse Source

Reworked & fixed DARKNESS bonuses and lookout tower / skyship logic

Ivan Savenko 5 months ago
parent
commit
e0de65d56c

+ 13 - 2
config/factions/tower.json

@@ -171,7 +171,12 @@
 				"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" ] },
+				"special2":       { 
+					"requires" : [ "fort" ],
+					"bonuses": [
+						{ "type": "SIGHT_RADIUS", "val": 15 }, // 5 base + 15 bonus = 20 tiles range
+					]
+				},
 				"special3":       { "type" : "library", "requires" : [ "mageGuild1" ] },
 				"special4":       {
 					"requires" : [ "mageGuild1" ],
@@ -185,7 +190,13 @@
 						]
 					}
 				},
-				"grail":          { "height" : "skyship",  "produce" : { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.knowledge", "val": 15 } ] },
+				"grail":          { 
+					"produce" : { "gold": 5000 },
+					"bonuses": [
+						{ "type": "PRIMARY_SKILL", "subtype": "primarySkill.knowledge", "val": 15 },
+						{ "type": "FULL_MAP_SCOUTING" }
+					]
+				},
 
 				"dwellingLvl1":   { "id" : 30, "requires" : [ "fort" ] },
 				"dwellingLvl2":   { "id" : 31, "requires" : [ "dwellingLvl1" ] },

+ 7 - 7
config/gameConfig.json

@@ -314,6 +314,9 @@
 			"movementPointsLand" : [ 1500, 1500, 1500, 1500, 1560, 1630, 1700, 1760, 1830, 1900, 1960, 2000 ],
 			/// movement points hero can get on start of the turn when on sea, depending on speed of slowest creature (0-based list)
 			"movementPointsSea" : [ 1500 ]
+			
+			/// Base scouting range for hero without any range modifiers
+			"baseScoutingRange" : 5,
 		},
 
 		"towns":
@@ -335,7 +338,10 @@
 			// How much researchs/skips per day are possible? (array index is spell tier)
 			"spellResearchPerDay": [ 2, 2, 2, 2, 1 ],
 			// Exponent for increasing cost for each research (factor 1 disables this; array index is spell tier)
-			"spellResearchCostExponentPerResearch": [ 1.25, 1.25, 1.25, 1.25, 1.25 ]
+			"spellResearchCostExponentPerResearch": [ 1.25, 1.25, 1.25, 1.25, 1.25 ],
+			
+			// Base scouting range for town without any range modifiers
+			"baseScoutingRange" : 5,
 		},
 
 		"combat":
@@ -581,12 +587,6 @@
 					"val" : 1,
 					"valueType" : "BASE_NUMBER"
 				},
-				"sightRadius" :
-				{
-					"type" : "SIGHT_RADIUS", //default sight radius
-					"val" : 5,
-					"valueType" : "BASE_NUMBER"
-				},
 				"experienceGain" : 
 				{
 					"type" : "HERO_EXPERIENCE_GAIN_PERCENT", //default hero xp

+ 0 - 5
config/schemas/townBuilding.json

@@ -44,11 +44,6 @@
 			"enum" : [ "normal", "auto", "special", "grail" ],
 			"description" : "Mode in which this building will be built"
 		},
-		"height" : {
-			"type" : "string",
-			"enum" : [ "skyship", "high", "average", "low"],
-			"description" : "Height for lookout towers and some grails"
-		},
 		"requires" : {
 			"$ref" : "#/definitions/buildingRequirement",
 			"description" : "List of town buildings that must be built before this one"

+ 21 - 6
docs/modders/Bonus/Bonus_Types.md

@@ -27,12 +27,6 @@ Changes mastery level of spells of affected heroes and units. Examples are magic
 - subtype: school of magic
 - val: level
 
-### DARKNESS
-
-On each turn, hides area in fog of war around affected town for all players other than town owner. Currently does not work for any entities other than towns.
-
-- val: radius in tiles
-
 ## Player bonuses
 
 Intended to be setup as global effect, AI cheat etc.
@@ -99,6 +93,27 @@ Reveal area of fog of war around affected heroes when hero is recruited or moves
 
 - val: radius in tiles
 
+### DARKNESS
+
+On each turn, hides area in fog of war around affected objects for all players other than town owner. Areas within scouting range of owned objects are not affected
+
+NOTE: when used by heroes, effect would still activate only on new turn, and not on every hero movement
+
+- val: radius in tiles
+- addInfo: optional, activation period (e.g. 7 = weekly, 28 = monthly)
+
+### FULL_MAP_SCOUTING
+
+On each turn, reveals entire map for owner of the bonus
+
+- addInfo: optional, activation period (e.g. 7 = weekly, 28 = monthly)
+
+### FULL_MAP_DARKNESS
+
+On each turn, hides entire map in fog of war for all players other than town owner. Areas within scouting range of owned objects are not affected
+
+- addInfo: optional, activation period (e.g. 7 = weekly, 28 = monthly)
+
 ### MANA_REGENERATION
 
 Restores specific amount of mana points for affected heroes on new turn

+ 1 - 10
docs/modders/Entities_Format/Town_Building_Format.md

@@ -150,16 +150,7 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config
 	// Generally only needs to be specified for "special" buildings
 	// See 'List of unique town buildings' section below for detailed description of this field
 	"type" : "",
-	
-	// If set, building will have Lookout Tower logic - extend sight radius of a town.
-	// Possible values: 
-	// low - increases town sight radius by 5 tiles
-	// average - sight radius extended by 15 tiles
-	// high - sight radius extended by 20 tiles
-	// skyship - entire map will be revealed
-	// If not set, building will not affect sight radius of a town
-	"height" : "average"
-	
+
 	// Resources produced each day by this building
 	"produce" : { 
 		"sulfur" : 1,

+ 2 - 0
lib/GameSettings.cpp

@@ -77,6 +77,7 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED,                  "dwellings", "accumulateWhenOwned"                  },
 		{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT,                       "dwellings", "mergeOnRecruit"                       },
 		{EGameSettings::HEROES_BACKPACK_CAP,                              "heroes",    "backpackSize"                         },
+		{EGameSettings::HEROES_BASE_SCOUNTING_RANGE,                      "heroes",    "baseScoutingRange"                    },
 		{EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS,                    "heroes",    "minimalPrimarySkills"                 },
 		{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP,                     "heroes",    "perPlayerOnMapCap"                    },
 		{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP,                      "heroes",    "perPlayerTotalCap"                    },
@@ -116,6 +117,7 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::TEXTS_ROAD,                                       "textData",  "road"                                 },
 		{EGameSettings::TEXTS_SPELL,                                      "textData",  "spell"                                },
 		{EGameSettings::TEXTS_TERRAIN,                                    "textData",  "terrain"                              },
+		{EGameSettings::TOWNS_BASE_SCOUNTING_RANGE,                       "towns",     "baseScoutingRange"                    },
 		{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP,                     "towns",     "buildingsPerTurnCap"                  },
 		{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES,                  "towns",     "startingDwellingChances"              },
 		{EGameSettings::TOWNS_SPELL_RESEARCH,                             "towns",     "spellResearch"                        },

+ 4 - 3
lib/IGameSettings.h

@@ -50,6 +50,7 @@ enum class EGameSettings
 	DWELLINGS_ACCUMULATE_WHEN_OWNED,
 	DWELLINGS_MERGE_ON_RECRUIT,
 	HEROES_BACKPACK_CAP,
+	HEROES_BASE_SCOUNTING_RANGE,
 	HEROES_MINIMAL_PRIMARY_SKILLS,
 	HEROES_PER_PLAYER_ON_MAP_CAP,
 	HEROES_PER_PLAYER_TOTAL_CAP,
@@ -59,6 +60,7 @@ enum class EGameSettings
 	HEROES_MOVEMENT_COST_BASE,
 	HEROES_MOVEMENT_POINTS_LAND,
 	HEROES_MOVEMENT_POINTS_SEA,
+	INTERFACE_PLAYER_COLORED_BACKGROUND,
 	MAP_FORMAT_ARMAGEDDONS_BLADE,
 	MAP_FORMAT_CHRONICLES,
 	MAP_FORMAT_HORN_OF_THE_ABYSS,
@@ -91,14 +93,13 @@ enum class EGameSettings
 	TEXTS_TERRAIN,
 	TOWNS_BUILDINGS_PER_TURN_CAP,
 	TOWNS_STARTING_DWELLING_CHANCES,
-	INTERFACE_PLAYER_COLORED_BACKGROUND,
+	TOWNS_BASE_SCOUNTING_RANGE,
 	TOWNS_SPELL_RESEARCH,
 	TOWNS_SPELL_RESEARCH_COST,
 	TOWNS_SPELL_RESEARCH_PER_DAY,
 	TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH,
 
-	OPTIONS_COUNT,
-	OPTIONS_BEGIN = BONUSES_GLOBAL
+	OPTIONS_COUNT
 };
 
 class DLL_LINKAGE IGameSettings

+ 2 - 0
lib/bonuses/BonusEnum.h

@@ -190,6 +190,8 @@ class JsonNode;
 	BONUS_NAME(MULTIHEX_ANIMATION) /*eg. dragons*/	\
 	BONUS_NAME(STACK_EXPERIENCE_GAIN_PERCENT) /*modifies all stack experience gains*/\
 	BONUS_NAME(VULNERABLE_FROM_BACK) /*bonus damage for attacks from behind*/\
+	BONUS_NAME(FULL_MAP_SCOUTING) /*Skyship*/\
+	BONUS_NAME(FULL_MAP_DARKNESS) /*opposite to Skyship*/\
 	/* end of list */
 
 

+ 8 - 5
lib/bonuses/IBonusBearer.cpp

@@ -15,10 +15,10 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-int IBonusBearer::valOfBonuses(const CSelector &selector, const std::string &cachingStr) const
+int IBonusBearer::valOfBonuses(const CSelector &selector, const std::string &cachingStr, int baseValue) const
 {
 	TConstBonusListPtr hlp = getAllBonuses(selector, nullptr, cachingStr);
-	return hlp->totalValue();
+	return hlp->totalValue(baseValue);
 }
 
 bool IBonusBearer::hasBonus(const CSelector &selector, const std::string &cachingStr) const
@@ -63,14 +63,17 @@ TConstBonusListPtr IBonusBearer::getBonusesOfType(BonusType type, BonusSubtypeID
 	return getBonuses(s, cachingStr);
 }
 
-int IBonusBearer::valOfBonuses(BonusType type) const
+int IBonusBearer::applyBonuses(BonusType type, int baseValue) const
 {
 	//This part is performance-critical
 	std::string cachingStr = "type_" + std::to_string(static_cast<int>(type));
-
 	CSelector s = Selector::type()(type);
+	return valOfBonuses(s, cachingStr, baseValue);
+}
 
-	return valOfBonuses(s, cachingStr);
+int IBonusBearer::valOfBonuses(BonusType type) const
+{
+	return applyBonuses(type, 0);
 }
 
 bool IBonusBearer::hasBonusOfType(BonusType type) const

+ 2 - 1
lib/bonuses/IBonusBearer.h

@@ -21,7 +21,7 @@ public:
 	IBonusBearer() = default;
 	virtual ~IBonusBearer() = default;
 	virtual TConstBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = {}) const = 0;
-	int valOfBonuses(const CSelector &selector, const std::string &cachingStr = {}) const;
+	int valOfBonuses(const CSelector &selector, const std::string &cachingStr = {}, int baseValue = 0) const;
 	bool hasBonus(const CSelector &selector, const std::string &cachingStr = {}) const;
 	bool hasBonus(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = {}) const;
 	TConstBonusListPtr getBonuses(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = {}) const;
@@ -30,6 +30,7 @@ public:
 	std::shared_ptr<const Bonus> getBonus(const CSelector &selector) const; //returns any bonus visible on node that matches (or nullptr if none matches)
 
 	//Optimized interface (with auto-caching)
+	int applyBonuses(BonusType type, int baseValue) const; //subtype -> subtype of bonus;
 	int valOfBonuses(BonusType type) const; //subtype -> subtype of bonus;
 	bool hasBonusOfType(BonusType type) const;//determines if hero has a bonus of given type (and optionally subtype)
 	int valOfBonuses(BonusType type, BonusSubtypeID subtype) const; //subtype -> subtype of bonus;

+ 1 - 1
lib/callback/CGameInfoCallback.cpp

@@ -903,7 +903,7 @@ void CGameInfoCallback::getTilesInRange(std::unordered_set<int3> & tiles,
 		logGlobal->error("Illegal call to getTilesInRange!");
 		return;
 	}
-	if(radious == CBuilding::HEIGHT_SKYSHIP) //reveal entire map
+	if(radious == GameConstants::FULL_MAP_RANGE)
 		getAllTiles (tiles, player, -1, [](auto * tile){return true;});
 	else
 	{

+ 2 - 0
lib/constants/NumericConstants.h

@@ -54,6 +54,8 @@ namespace GameConstants
 	constexpr int TOURNAMENT_RULES_DD_MAP_TILES_THRESHOLD = 144*144*2; //map tiles count threshold for 2 dimension door casts with tournament rules
 	constexpr int KINGDOM_WINDOW_HEROES_SLOTS = 4;
 	constexpr int INFO_WINDOW_ARTIFACTS_MAX_ITEMS = 14;
+
+	constexpr int FULL_MAP_RANGE = std::numeric_limits<int>::max();
 }
 
 VCMI_LIB_NAMESPACE_END

+ 0 - 8
lib/entities/building/CBuilding.cpp

@@ -25,14 +25,6 @@ const std::map<std::string, CBuilding::EBuildMode> CBuilding::MODES =
 		{ "grail", CBuilding::BUILD_GRAIL }
 };
 
-const std::map<std::string, CBuilding::ETowerHeight> CBuilding::TOWER_TYPES =
-	{
-		{ "low", CBuilding::HEIGHT_LOW },
-		{ "average", CBuilding::HEIGHT_AVERAGE },
-		{ "high", CBuilding::HEIGHT_HIGH },
-		{ "skyship", CBuilding::HEIGHT_SKYSHIP }
-};
-
 BuildingTypeUniqueID CBuilding::getUniqueTypeID() const
 {
 	return BuildingTypeUniqueID(town->faction->getId(), bid);

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

@@ -60,17 +60,7 @@ public:
 		BUILD_GRAIL    // 3 - grail - building requires grail to be built
 	} mode;
 
-	enum ETowerHeight // for lookup towers and some grails
-	{
-		HEIGHT_NO_TOWER = 5, // building has not 'lookout tower' ability
-		HEIGHT_LOW = 10,     // low lookout tower, but castle without lookout tower gives radius 5
-		HEIGHT_AVERAGE = 15,
-		HEIGHT_HIGH = 20,    // such tower is in the Tower town
-		HEIGHT_SKYSHIP = std::numeric_limits<int>::max()  // grail, open entire map
-	} height;
-
 	static const std::map<std::string, CBuilding::EBuildMode> MODES;
-	static const std::map<std::string, CBuilding::ETowerHeight> TOWER_TYPES;
 
 	CBuilding() : town(nullptr), mode(BUILD_NORMAL) {};
 

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

@@ -283,8 +283,6 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 		? CBuilding::BUILD_GRAIL
 		: vstd::find_or(CBuilding::MODES, source["mode"].String(), CBuilding::BUILD_NORMAL);
 
-	ret->height = vstd::find_or(CBuilding::TOWER_TYPES, source["height"].String(), CBuilding::HEIGHT_NO_TOWER);
-
 	ret->identifier = stringID;
 	ret->modScope = source.getModScope();
 	ret->town = town;

+ 3 - 0
lib/json/JsonBonus.cpp

@@ -220,6 +220,9 @@ static void loadBonusAddInfo(CAddInfo & var, BonusType type, const JsonNode & no
 		case BonusType::PRIMARY_SKILL:
 		case BonusType::ENCHANTER:
 		case BonusType::SPECIAL_PECULIAR_ENCHANT:
+		case BonusType::DARKNESS:
+		case BonusType::FULL_MAP_SCOUTING:
+		case BonusType::FULL_MAP_DARKNESS:
 			// 1 number
 			var = getFirstValue(value).Integer();
 			break;

+ 2 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -1062,7 +1062,8 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b
 
 int CGHeroInstance::getSightRadius() const
 {
-	return valOfBonuses(BonusType::SIGHT_RADIUS); // scouting gives SIGHT_RADIUS bonus
+	int baseValue = LIBRARY->engineSettings()->getInteger(EGameSettings::HEROES_BASE_SCOUNTING_RANGE);
+	return applyBonuses(BonusType::SIGHT_RADIUS, baseValue);
 }
 
 si32 CGHeroInstance::manaRegain() const

+ 4 - 10
lib/mapObjects/CGTownInstance.cpp

@@ -11,6 +11,7 @@
 #include "StdInc.h"
 #include "CGTownInstance.h"
 
+#include "IGameSettings.h"
 #include "TownBuildingInstance.h"
 #include "../spells/CSpellHandler.h"
 #include "../bonuses/Bonus.h"
@@ -42,17 +43,10 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-int CGTownInstance::getSightRadius() const //returns sight distance
+int CGTownInstance::getSightRadius() const
 {
-	auto ret = CBuilding::HEIGHT_NO_TOWER;
-
-	for(const auto & bid : builtBuildings)
-	{
-		auto height = getTown()->buildings.at(bid)->height;
-		if(ret < height)
-			ret = height;
-	}
-	return ret;
+	int baseValue = LIBRARY->engineSettings()->getInteger(EGameSettings::TOWNS_BASE_SCOUNTING_RANGE);
+	return applyBonuses(BonusType::SIGHT_RADIUS, baseValue);
 }
 
 void CGTownInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier)

+ 30 - 16
server/CGameHandler.cpp

@@ -654,6 +654,19 @@ void CGameHandler::onNewTurn()
 		addStatistics(*statistics); // write at end of turn
 	}
 
+	const auto & currentDaySelector = [day = gameState().day+1](const Bonus * bonus)
+	{
+		if (bonus->additionalInfo[0] <= 0)
+			return true;
+		if ((day % bonus->additionalInfo[0]) == 0)
+			return true;
+		return false;
+	};
+
+	const auto & fullMapScoutingSelector = Selector::type()(BonusType::FULL_MAP_SCOUTING).And(currentDaySelector);
+	const auto & fullMapDarknessSelector = Selector::type()(BonusType::FULL_MAP_DARKNESS).And(currentDaySelector);
+	const auto & darknessSelector = Selector::type()(BonusType::DARKNESS).And(currentDaySelector);
+
 	for (const auto & townID : gameState().getMap().getAllTowns())
 	{
 		const auto * t = gameState().getTown(townID);
@@ -661,25 +674,27 @@ void CGameHandler::onNewTurn()
 
 		// Skyship, probably easier to handle same as Veil of darkness
 		// do it every new day before veils
-		if(t->hasBuilt(BuildingID::GRAIL)
-		   && player.isValidPlayer()
-			&& t->getTown()->buildings.at(BuildingID::GRAIL)->height == CBuilding::HEIGHT_SKYSHIP)
-		{
-			changeFogOfWar(t->getSightCenter(), t->getSightRadius(), player, ETileVisibility::REVEALED);
-		}
+		if(t->hasBonus(fullMapScoutingSelector) && player.isValidPlayer())
+			changeFogOfWar(t->getSightCenter(), GameConstants::FULL_MAP_RANGE, player, ETileVisibility::REVEALED);
 	}
 
-	for (const auto & townID : gameState().getMap().getAllTowns())
+	for (const auto & object : gameState().getMap().getObjects<CArmedInstance>())
 	{
-		const auto * t = gameState().getTown(townID);
-		if(t->hasBonusOfType(BonusType::DARKNESS))
+		if(!object->hasBonus(darknessSelector) && !object->hasBonus(fullMapDarknessSelector))
+			continue;
+
+		for(const auto & player : gameState().players)
 		{
-			for(const auto & player : gameState().players)
-			{
-				if (gameInfo().getPlayerStatus(player.first) == EPlayerStatus::INGAME &&
-					gameInfo().getPlayerRelations(player.first, t->tempOwner) == PlayerRelations::ENEMIES)
-					changeFogOfWar(t->getSightCenter(), t->valOfBonuses(BonusType::DARKNESS), player.first, ETileVisibility::HIDDEN);
-			}
+			if (gameInfo().getPlayerStatus(player.first) != EPlayerStatus::INGAME)
+				continue;
+
+			if (gameInfo().getPlayerRelations(player.first, object->getOwner()) != PlayerRelations::ENEMIES)
+				continue;
+
+			if (object->hasBonus(fullMapDarknessSelector))
+				changeFogOfWar(object->getSightCenter(), GameConstants::FULL_MAP_RANGE, player.first, ETileVisibility::HIDDEN);
+			else
+				changeFogOfWar(object->getSightCenter(), object->valOfBonuses(darknessSelector), player.first, ETileVisibility::HIDDEN);
 		}
 	}
 
@@ -4146,7 +4161,6 @@ void CGameHandler::changeFogOfWar(const std::unordered_set<int3> &tiles, PlayerC
 	if (mode == ETileVisibility::HIDDEN)
 	{
 		// do not hide tiles observed by owned objects. May lead to disastrous AI problems
-		// FIXME: this leads to a bug - shroud of darkness from Necropolis does can not override Skyship from Tower
 		std::unordered_set<int3> observedTiles;
 		const auto * p = gameInfo().getPlayerState(player);
 		for (const auto * obj : p->getOwnedObjects())