1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-25 22:42:04 +02:00

Reworked & fixed DARKNESS bonuses and lookout tower / skyship logic

This commit is contained in:
Ivan Savenko
2025-06-09 13:50:45 +03:00
parent a305ed28bb
commit e0de65d56c
19 changed files with 102 additions and 87 deletions

View File

@@ -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" ] },

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -151,15 +151,6 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config
// 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,

View File

@@ -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" },

View File

@@ -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

View File

@@ -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 */

View File

@@ -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

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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

View File

@@ -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);

View File

@@ -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) {};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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)

View File

@@ -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())
{
const auto * t = gameState().getTown(townID);
if(t->hasBonusOfType(BonusType::DARKNESS))
for (const auto & object : gameState().getMap().getObjects<CArmedInstance>())
{
if(!object->hasBonus(darknessSelector) && !object->hasBonus(fullMapDarknessSelector))
continue;
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())