diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 1888c876a..eda7e7faa 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -528,13 +528,16 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons } } -float RewardEvaluator::evaluateWitchHutSkillScore(const CGWitchHut * hut, const CGHeroInstance * hero, HeroRole role) const +float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const { + auto rewardable = dynamic_cast(hut); + assert(rewardable); + + auto skill = SecondarySkill(*rewardable->configuration.getVariable("secondarySkill", "gainedSkill")); + if(!hut->wasVisited(hero->tempOwner)) return role == HeroRole::SCOUT ? 2 : 0; - auto skill = SecondarySkill(hut->ability); - if(hero->getSecSkillLevel(skill) != SecSkillLevel::NONE || hero->secSkills.size() >= GameConstants::SKILL_PER_HERO) return 0; @@ -575,7 +578,7 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH case Obj::LIBRARY_OF_ENLIGHTENMENT: return 8; case Obj::WITCH_HUT: - return evaluateWitchHutSkillScore(dynamic_cast(target), hero, role); + return evaluateWitchHutSkillScore(target, hero, role); case Obj::PANDORAS_BOX: //Can contains experience, spells, or skills (only on custom maps) return 2.5f; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 4853e4aed..beccbef62 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -18,8 +18,6 @@ VCMI_LIB_NAMESPACE_BEGIN -class CGWitchHut; - VCMI_LIB_NAMESPACE_END namespace NKAI @@ -43,7 +41,7 @@ public: float getResourceRequirementStrength(int resType) const; float getStrategicalValue(const CGObjectInstance * target) const; float getTotalResourceRequirementStrength(int resType) const; - float evaluateWitchHutSkillScore(const CGWitchHut * hut, const CGHeroInstance * hero, HeroRole role) const; + float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const; float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const; int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; diff --git a/AI/VCAI/ResourceManager.cpp b/AI/VCAI/ResourceManager.cpp index c8c3f3a33..8a97e430f 100644 --- a/AI/VCAI/ResourceManager.cpp +++ b/AI/VCAI/ResourceManager.cpp @@ -90,7 +90,7 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o { auto allResources = cb->getResourceAmount(); auto income = estimateIncome(); - GameResID resourceType = EGameResID::INVALID; + GameResID resourceType = EGameResID::NONE; TResource amountToCollect = 0; using resPair = std::pair; @@ -129,7 +129,7 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o break; } } - if (resourceType == EGameResID::INVALID) //no needed resources has 0 income, + if (resourceType == EGameResID::NONE) //no needed resources has 0 income, { //find the one which takes longest to collect using timePair = std::pair; diff --git a/Global.h b/Global.h index 2eb54db45..1ba7846e4 100644 --- a/Global.h +++ b/Global.h @@ -475,6 +475,19 @@ namespace vstd } } + template + void erase_if(std::unordered_set &setContainer, Predicate pred) + { + auto itr = setContainer.begin(); + auto endItr = setContainer.end(); + while(itr != endItr) + { + auto tmpItr = itr++; + if(pred(*tmpItr)) + setContainer.erase(tmpItr); + } + } + //works for map and std::map, maybe something else template void erase_if(std::map &container, Predicate pred) diff --git a/client/Client.h b/client/Client.h index b9c98768b..86538827c 100644 --- a/client/Client.h +++ b/client/Client.h @@ -209,8 +209,8 @@ public: void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {}; void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}; - void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} - void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, bool hide) override {} + void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {} + void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, ETileVisibility mode) override {} void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {} diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 0af992ec6..483bb1126 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -177,7 +177,7 @@ void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack) } if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES) { - if(pack.mode) + if(pack.mode == ETileVisibility::REVEALED) i.second->tileRevealed(pack.tiles); else i.second->tileHidden(pack.tiles); diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index f015c779b..320aabef8 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -351,10 +351,23 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, } else { + std::vector components; + if (settings["general"]["enableUiEnhancements"].Bool()) + { + if(LOCPLINT->localState->getCurrentHero()) + components = obj->getPopupComponents(LOCPLINT->localState->getCurrentHero()); + else + components = obj->getPopupComponents(LOCPLINT->playerID); + } + + std::vector> guiComponents; + for (auto & component : components) + guiComponents.push_back(std::make_shared(component)); + if(LOCPLINT->localState->getCurrentHero()) - CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->localState->getCurrentHero())); + CRClickPopup::createAndPush(obj->getPopupText(LOCPLINT->localState->getCurrentHero()), guiComponents); else - CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->playerID)); + CRClickPopup::createAndPush(obj->getPopupText(LOCPLINT->playerID), guiComponents); } } diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 8c2a6a274..b00149829 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -81,7 +81,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapObjectConstructors/DwellingInstanceConstructor.cpp ${MAIN_LIB_DIR}/mapObjectConstructors/HillFortInstanceConstructor.cpp ${MAIN_LIB_DIR}/mapObjectConstructors/ShipyardInstanceConstructor.cpp - ${MAIN_LIB_DIR}/mapObjectConstructors/ShrineInstanceConstructor.cpp ${MAIN_LIB_DIR}/mapObjects/CArmedInstance.cpp ${MAIN_LIB_DIR}/mapObjects/CBank.cpp @@ -425,7 +424,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapObjectConstructors/IObjectInfo.h ${MAIN_LIB_DIR}/mapObjectConstructors/RandomMapInfo.h ${MAIN_LIB_DIR}/mapObjectConstructors/ShipyardInstanceConstructor.h - ${MAIN_LIB_DIR}/mapObjectConstructors/ShrineInstanceConstructor.h ${MAIN_LIB_DIR}/mapObjectConstructors/SObjectSounds.h ${MAIN_LIB_DIR}/mapObjects/CArmedInstance.h diff --git a/config/gameConfig.json b/config/gameConfig.json index f80ff5a43..df6092966 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -48,15 +48,23 @@ "objects" : [ - "config/objects/generic.json", - "config/objects/moddables.json", + "config/objects/cartographer.json", + "config/objects/coverOfDarkness.json", "config/objects/creatureBanks.json", "config/objects/dwellings.json", - "config/objects/rewardableOncePerWeek.json", - "config/objects/rewardablePickable.json", - "config/objects/rewardableOnceVisitable.json", + "config/objects/generic.json", + "config/objects/magicSpring.json", + "config/objects/magicWell.json", + "config/objects/moddables.json", + "config/objects/observatory.json", + "config/objects/rewardableBonusing.json", "config/objects/rewardableOncePerHero.json", - "config/objects/rewardableBonusing.json" + "config/objects/rewardableOncePerWeek.json", + "config/objects/rewardableOnceVisitable.json", + "config/objects/rewardablePickable.json", + "config/objects/scholar.json", + "config/objects/shrine.json", + "config/objects/witchHut.json" ], "artifacts" : diff --git a/config/objects/cartographer.json b/config/objects/cartographer.json new file mode 100644 index 000000000..4df883c5c --- /dev/null +++ b/config/objects/cartographer.json @@ -0,0 +1,97 @@ +{ + "cartographer" : { + "index" :13, + "handler": "configurable", + "lastReservedIndex" : 2, + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "cartographerWater" : { + "index" : 0, + "aiValue" : 5000, + "rmg" : { + "zoneLimit" : 1, + "value" : 5000, + "rarity" : 20 + }, + "compatibilityIdentifiers" : [ "water" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 25, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "water" : 1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + }, + "cartographerLand" : { + "index" : 1, + "aiValue": 10000, + "rmg" : { + "zoneLimit" : 1, + "value" : 10000, + "rarity" : 2 + }, + "compatibilityIdentifiers" : [ "land" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 26, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "surface" : 1, + "water" : -1, + "rock" : -1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + }, + "cartographerSubterranean" : { + "index" : 2, + "aiValue" : 7500, + "rmg" : { + "zoneLimit" : 1, + "value" : 7500, + "rarity" : 20 + }, + "compatibilityIdentifiers" : [ "subterra" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 27, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "subterra" : 1, + "water" : -1, + "rock" : -1, + "surface" : -1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + } + } + } +} \ No newline at end of file diff --git a/config/objects/coverOfDarkness.json b/config/objects/coverOfDarkness.json new file mode 100644 index 000000000..0cb733547 --- /dev/null +++ b/config/objects/coverOfDarkness.json @@ -0,0 +1,35 @@ +{ + "coverOfDarkness" : { + "index" :15, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "coverOfDarkness" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 31, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1, + "hide" : true + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/generic.json b/config/objects/generic.json index a32cb0dda..396ddc374 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -156,70 +156,6 @@ } }, - "redwoodObservatory" : { - "index" :58, - "handler" : "observatory", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 750, - "templates" : - { - "base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] }, - "snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] } - }, - "rmg" : { - "zoneLimit" : 1, - "value" : 750, - "rarity" : 100 - } - } - } - }, - "pillarOfFire" : { - "index" :60, - "handler" : "observatory", - "base" : { - "sounds" : { - "ambient" : ["LOOPFIRE"], - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 750, - "rmg" : { - "zoneLimit" : 1, - "value" : 750, - "rarity" : 100 - } - } - } - }, - "coverOfDarkness" : { - "index" :15, - "handler" : "observatory", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - } - } - } - }, - "whirlpool" : { "index" :111, "handler" : "whirlpool", @@ -294,78 +230,6 @@ } } }, - "shrineOfMagicLevel1" : {//incantation - "index" :88, - "handler" : "shrine", - "base" : { - "sounds" : { - "ambient" : ["LOOPSHRIN"], - "visit" : ["TEMPLE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 500, - "rmg" : { - "value" : 500, - "rarity" : 100 - }, - "visitText" : 127, - "spell" : { - "level" : 1 - } - } - } - }, - "shrineOfMagicLevel2" : {//gesture - "index" :89, - "handler" : "shrine", - "base" : { - "sounds" : { - "ambient" : ["LOOPSHRIN"], - "visit" : ["TEMPLE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 2000, - "rmg" : { - "value" : 2000, - "rarity" : 100 - }, - "visitText" : 128, - "spell" : { - "level" : 2 - } - } - } - }, - "shrineOfMagicLevel3" : {//thinking - "index" :90, - "handler" : "shrine", - "base" : { - "sounds" : { - "ambient" : ["LOOPSHRIN"], - "visit" : ["TEMPLE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 3000, - "rmg" : { - "value" : 3000, - "rarity" : 100 - }, - "visitText" : 129, - "spell" : { - "level" : 3 - } - } - } - }, "eyeOfTheMagi" : { "index" :27, "handler" : "magi", @@ -454,26 +318,6 @@ } } }, - "scholar" : { - "index" :81, - "handler" : "scholar", - "base" : { - "sounds" : { - "visit" : ["GAZEBO"], - "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 1500, - "rmg" : { - "value" : 1500, - "rarity" : 100 - } - } - } - }, "shipyard" : { "index" :87, "handler" : "shipyard", @@ -587,26 +431,6 @@ } } }, - "witchHut" : { - "index" :113, - "handler" : "witch", - "base" : { - "sounds" : { - "visit" : ["GAZEBO"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 1500, - "rmg" : { - "zoneLimit" : 3, - "value" : 1500, - "rarity" : 80 - } - } - } - }, "questGuard" : { "index" :215, "handler" : "questGuard", diff --git a/config/objects/magicSpring.json b/config/objects/magicSpring.json new file mode 100644 index 000000000..6013353c3 --- /dev/null +++ b/config/objects/magicSpring.json @@ -0,0 +1,44 @@ +{ + "magicSpring" : { + "index" : 48, + "handler": "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPFOUN"], + "visit" : ["FAERIE"] + } + }, + "types" : { + "magicSpring" : { + "index" : 0, + "aiValue" : 500, + //banned due to problems with 2 viistable offsets + //"rmg" : { + // "zoneLimit" : 1, + // "value" : 500, + // "rarity" : 50 + //}, + "compatibilityIdentifiers" : [ "object" ], + + "onEmptyMessage" : 76, + "onVisitedMessage" : 75, + "description" : "@core.xtrainfo.15", + "resetParameters" : { + "period" : 7, + "visitors" : true + }, + "visitMode" : "once", + "selectMode" : "selectFirst", + "rewards" : [ + { + "limiter" : { + "noneOf" : [ { "manaPercentage" : 200 } ] + }, + "message" : 74, + "manaPercentage" : 200 + } + ] + } + } + } +} diff --git a/config/objects/magicWell.json b/config/objects/magicWell.json new file mode 100644 index 000000000..4f15e1aa2 --- /dev/null +++ b/config/objects/magicWell.json @@ -0,0 +1,39 @@ +{ + "magicWell" : { + "index" :49, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["FAERIE"] + } + }, + "types" : { + "magicWell" : { + "index" : 0, + "aiValue" : 250, + "rmg" : { + "zoneLimit" : 1, + "value" : 250, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "onEmptyMessage" : 79, + "onVisitedMessage" : 78, + "description" : "@core.xtrainfo.25", + "visitMode" : "bonus", + "selectMode" : "selectFirst", + "rewards" : [ + { + "limiter" : { + "noneOf" : [ { "manaPercentage" : 100 } ] + }, + "bonuses" : [ { "type" : "NONE", "duration" : "ONE_DAY"} ], + "message" : 77, + "manaPercentage" : 100 + } + ] + }, + } + } +} diff --git a/config/objects/moddables.json b/config/objects/moddables.json index da0b7e793..2a2b31371 100644 --- a/config/objects/moddables.json +++ b/config/objects/moddables.json @@ -255,23 +255,6 @@ } }, - // subtype: different revealed areas - "cartographer" : { - "index" :13, - "handler": "cartographer", - "lastReservedIndex" : 2, - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "water" : { "index" : 0, "aiValue" : 5000, "rmg" : { "zoneLimit" : 1, "value" : 5000, "rarity" : 20 } }, - "land" : { "index" : 1, "aiValue": 10000, "rmg" : { "zoneLimit" : 1, "value" : 10000, "rarity" : 20 } }, - "subterra" : { "index" : 2, "aiValue" : 7500, "rmg" : { "zoneLimit" : 1, "value" : 7500, "rarity" : 20 } } - } - }, - // subtype: resource ID "mine" : { "index" :53, diff --git a/config/objects/observatory.json b/config/objects/observatory.json new file mode 100644 index 000000000..c4999b4aa --- /dev/null +++ b/config/objects/observatory.json @@ -0,0 +1,79 @@ +{ + "redwoodObservatory" : { + "index" :58, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "redwoodObservatory" : { + "index" : 0, + "aiValue" : 750, + "templates" : + { + "base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] }, + "snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] } + }, + "rmg" : { + "zoneLimit" : 1, + "value" : 750, + "rarity" : 100 + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 98, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1 + } + } + ] + } + } + }, + + "pillarOfFire" : { + "index" :60, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPFIRE"], + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "pillarOfFire" : { + "index" : 0, + "aiValue" : 750, + "rmg" : { + "zoneLimit" : 1, + "value" : 750, + "rarity" : 100 + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 99, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1 + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/rewardableBonusing.json b/config/objects/rewardableBonusing.json index 64f1855c0..b7ee79698 100644 --- a/config/objects/rewardableBonusing.json +++ b/config/objects/rewardableBonusing.json @@ -23,6 +23,7 @@ "blockedVisitable" : true, "onVisitedMessage" : 22, + "description" : "@core.xtrainfo.0", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -54,6 +55,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 30, + "description" : "@core.xtrainfo.1", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -87,6 +89,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 50, + "description" : "@core.xtrainfo.2", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -119,6 +122,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 56, + "description" : "@core.xtrainfo.3", "visitMode" : "bonus", "selectMode" : "selectFirst", "resetParameters" : { @@ -172,6 +176,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 58, + "description" : "@core.xtrainfo.0", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -204,6 +209,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 63, + "description" : "@core.xtrainfo.22", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -261,6 +267,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 82, + "description" : "@core.xtrainfo.2", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -292,6 +299,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 95, + "description" : "@core.xtrainfo.13", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -383,6 +391,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 141, + "description" : "@core.xtrainfo.23", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -420,6 +429,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 111, + "description" : "@core.xtrainfo.17", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -455,6 +465,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 167, + "description" : "@core.xtrainfo.13", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ diff --git a/config/objects/rewardableOncePerHero.json b/config/objects/rewardableOncePerHero.json index e6bac178a..43065ed8d 100644 --- a/config/objects/rewardableOncePerHero.json +++ b/config/objects/rewardableOncePerHero.json @@ -55,6 +55,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 40, + "description" : "@core.xtrainfo.7", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -86,6 +87,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 60, + "description" : "@core.xtrainfo.4", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -117,6 +119,7 @@ "onVisitedMessage" : 67, "onEmptyMessage" : 68, + "description" : "@core.xtrainfo.6", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -161,6 +164,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 81, + "description" : "@core.xtrainfo.8", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -192,6 +196,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 101, + "description" : "@core.xtrainfo.11", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -233,16 +238,21 @@ } ], "onVisitedMessage" : 147, + "description" : "@core.xtrainfo.18", "visitMode" : "hero", "selectMode" : "selectFirst", "canRefuse" : true, + "showScoutedPreview" : true, + "rewards" : [ { + "description" : "@core.arraytxt.202", "message" : 148, "appearChance" : { "max" : 34 }, "gainedLevels" : 1 }, { + "description" : "@core.arraytxt.203", "message" : 149, "appearChance" : { "min" : 34, "max" : 67 }, "limiter" : { "resources" : { "gold" : 2000 } }, @@ -250,6 +260,7 @@ "gainedLevels" : 1 }, { + "description" : "@core.arraytxt.204", "message" : 151, "appearChance" : { "min" : 67 }, "limiter" : { "resources" : { "gems" : 10 } }, @@ -282,6 +293,7 @@ "onSelectMessage" : 71, "onVisitedMessage" : 72, "onEmptyMessage" : 73, + "description" : "@core.xtrainfo.9", "visitMode" : "hero", "selectMode" : "selectPlayer", "canRefuse" : true, @@ -322,6 +334,7 @@ "onSelectMessage" : 158, "onVisitedMessage" : 159, "onEmptyMessage" : 160, + "description" : "@core.xtrainfo.10", "visitMode" : "hero", "selectMode" : "selectPlayer", "canRefuse" : true, @@ -360,6 +373,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 144, + "description" : "@core.xtrainfo.5", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ diff --git a/config/objects/rewardableOncePerWeek.json b/config/objects/rewardableOncePerWeek.json index e83375b8c..7b2005848 100644 --- a/config/objects/rewardableOncePerWeek.json +++ b/config/objects/rewardableOncePerWeek.json @@ -1,82 +1,4 @@ { - /// These are objects that covered by concept of "configurable object" and have their entire configuration in this config - "magicWell" : { - "index" :49, - "handler" : "configurable", - "base" : { - "sounds" : { - "visit" : ["FAERIE"] - } - }, - "types" : { - "magicWell" : { - "index" : 0, - "aiValue" : 250, - "rmg" : { - "zoneLimit" : 1, - "value" : 250, - "rarity" : 100 - }, - "compatibilityIdentifiers" : [ "object" ], - - "onEmptyMessage" : 79, - "onVisitedMessage" : 78, - "visitMode" : "bonus", - "selectMode" : "selectFirst", - "rewards" : [ - { - "limiter" : { - "noneOf" : [ { "manaPercentage" : 100 } ] - }, - "bonuses" : [ { "type" : "NONE", "duration" : "ONE_DAY"} ], - "message" : 77, - "manaPercentage" : 100 - } - ] - }, - } - }, - "magicSpring" : { - "index" : 48, - "handler": "configurable", - "base" : { - "sounds" : { - "ambient" : ["LOOPFOUN"], - "visit" : ["FAERIE"] - } - }, - "types" : { - "magicSpring" : { - "index" : 0, - "aiValue" : 500, - //banned due to problems with 2 viistable offsets - //"rmg" : { - // "zoneLimit" : 1, - // "value" : 500, - // "rarity" : 50 - //}, - "compatibilityIdentifiers" : [ "object" ], - - "onEmptyMessage" : 76, - "onVisitedMessage" : 75, - "resetParameters" : { - "period" : 7, - "visitors" : true - }, - "visitMode" : "once", - "selectMode" : "selectFirst", - "rewards" : [ - { - "limiter" : { - "noneOf" : [ { "manaPercentage" : 200 } ] - }, - "message" : 74, - "manaPercentage" : 200 - } - ] - } - } - }, "mysticalGarden" : { "index" : 55, "handler": "configurable", diff --git a/config/objects/rewardablePickable.json b/config/objects/rewardablePickable.json index b5756a528..6b01cf3d6 100644 --- a/config/objects/rewardablePickable.json +++ b/config/objects/rewardablePickable.json @@ -235,7 +235,7 @@ }, { "appearChance" : { "max" : 33 }, - "gainedExp" : 1500, + "heroExperience" : 1500, "removeObject" : true, }, { @@ -245,7 +245,7 @@ }, { "appearChance" : { "min" : 33, "max" : 65 }, - "gainedExp" : 1000, + "heroExperience" : 1000, "removeObject" : true, }, { @@ -255,7 +255,7 @@ }, { "appearChance" : { "min" : 65, "max" : 95 }, - "gainedExp" : 500, + "heroExperience" : 500, "removeObject" : true, }, { diff --git a/config/objects/scholar.json b/config/objects/scholar.json new file mode 100644 index 000000000..c6d1ed139 --- /dev/null +++ b/config/objects/scholar.json @@ -0,0 +1,94 @@ +{ + "scholar" : { + "index" :81, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["GAZEBO"], + "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] + } + }, + "types" : { + "scholar" : { + "index" : 0, + "aiValue" : 1500, + "rmg" : { + "value" : 1500, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "unlimited", + "blockedVisitable" : true, + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + } + }, + "secondarySkill" : { + "gainedSkill" : { // Note: this variable name is used by engine for H3M loading + } + }, + "primarySkill" : { + "gainedStat" : { // Note: this variable name is used by engine for H3M loading + } + } + }, + "selectMode" : "selectFirst", + "rewards" : [ + { + "appearChance" : { "min" : 0, "max" : 33 }, + "message" : 115, + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "removeObject" : true + }, + { + "appearChance" : { "min" : 33, "max" : 66 }, + "message" : 115, + "limiter" : { + // Hero does not have this skill at expert + "noneOf" : [ + { + "secondary" : { + "@gainedSkill" : 3 + } + } + ], + // And have either free skill slot or this skill + "anyOf" : [ + { + "canLearnSkills" : true + }, + { + "secondary" : { + "@gainedSkill" : 1 + } + } + ] + }, + "secondary" : { + "@gainedSkill" : 1 + }, + "removeObject" : true + }, + { + // Always present - fallback if hero can't learn secondary / spell + "message" : 115, + "primary" : { + "@gainedStat" : 1 + }, + "removeObject" : true + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/shrine.json b/config/objects/shrine.json new file mode 100644 index 000000000..44bd2a952 --- /dev/null +++ b/config/objects/shrine.json @@ -0,0 +1,209 @@ +{ + "shrineOfMagicLevel1" : {//incantation + "index" :88, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPSHRIN"], + "visit" : ["TEMPLE"] + } + }, + "types" : { + "shrineOfMagicLevel1" : { + "index" : 0, + "aiValue" : 500, + "rmg" : { + "value" : 500, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "@core.xtrainfo.19", + "showScoutedPreview" : true, + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + "level": 1 + } + } + }, + "visitLimiter" : { + "spells" : [ + "@gainedSpell" + ] + }, + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "description" : "@core.genrltxt.355", + "message" : [ 127, "%s." ] // You learn new spell + } + ], + "onVisitedMessage" : [ 127, "%s.", 174 ], // You already known this spell + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellBook" + } + ] + }, + "message" : [ 127, "%s.", 130 ] // No Wisdom + }, + { + "message" : [ 127, "%s.", 131 ] // No spellbook + } + ] + } + } + }, + "shrineOfMagicLevel2" : {//gesture + "index" :89, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPSHRIN"], + "visit" : ["TEMPLE"] + } + }, + "types" : { + "shrineOfMagicLevel2" : { + "index" : 0, + "aiValue" : 2000, + "rmg" : { + "value" : 2000, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "@core.xtrainfo.20", + "showScoutedPreview" : true, + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + "level": 2 + } + } + }, + "visitLimiter" : { + "spells" : [ + "@gainedSpell" + ] + }, + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "description" : "@core.genrltxt.355", + "message" : [ 128, "%s." ] // You learn new spell + } + ], + "onVisitedMessage" : [ 128, "%s.", 174 ], // You already known this spell + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellBook" + } + ] + }, + "message" : [ 128, "%s.", 130 ] // No Wisdom + }, + { + "message" : [ 128, "%s.", 131 ] // No spellbook + } + ] + } + } + }, + "shrineOfMagicLevel3" : {//thinking + "index" :90, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPSHRIN"], + "visit" : ["TEMPLE"] + } + }, + "types" : { + "shrineOfMagicLevel3" : { + "index" : 0, + "aiValue" : 3000, + "rmg" : { + "value" : 3000, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "@core.xtrainfo.21", + "showScoutedPreview" : true, + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + "level": 3 + } + } + }, + "visitLimiter" : { + "spells" : [ + "@gainedSpell" + ] + }, + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "description" : "@core.genrltxt.355", + "message" : [ 129, "%s." ] // You learn new spell + } + ], + "onVisitedMessage" : [ 129, "%s.", 174 ], // You already known this spell + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellBook" + } + ] + }, + "message" : [ 129, "%s.", 130 ] // No Wisdom + }, + { + "message" : [ 129, "%s.", 131 ] // No spellbook + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/witchHut.json b/config/objects/witchHut.json new file mode 100644 index 000000000..ad5d036df --- /dev/null +++ b/config/objects/witchHut.json @@ -0,0 +1,65 @@ +{ + "witchHut" : { + "index" :113, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["GAZEBO"] + } + }, + "types" : { + "witchHut" : { + "index" : 0, + "aiValue" : 1500, + "rmg" : { + "zoneLimit" : 3, + "value" : 1500, + "rarity" : 80 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "@core.xtrainfo.12", + "showScoutedPreview" : true, + + "variables" : { + "secondarySkill" : { + "gainedSkill" : { // Note: this variable name is used by engine for H3M loading and by AI + "noneOf" : [ + "leadership", + "necromancy" + ] + } + } + }, + "visitLimiter" : { + "secondary" : { + "@gainedSkill" : 1 + } + }, + "rewards" : [ + { + "limiter" : { + "canLearnSkills" : true, + "noneOf" : [ + { + "secondary" : { + "@gainedSkill" : 1 + } + } + ] + }, + "description" : 355, + "secondary" : { + "@gainedSkill" : 1 + }, + "message" : 171 // Witch teaches you skill + } + ], + "onVisitedMessage" : 172, // You already known this skill + "onEmptyMessage" : 173 // You know too much (no free slots) + } + } + } +} \ No newline at end of file diff --git a/docs/modders/Map_Objects/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index 9be9b6fcd..245719060 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -4,6 +4,7 @@ - [Base object definition](#base-object-definition) - [Configurable object definition](#configurable-object-definition) - [Base object definition](#base-object-definition) +- [Variables Parameters definition](#variables-parameters-definition) - [Reset Parameters definition](#reset-parameters-definition) - [Appear Chance definition](#appear-chance-definition) - [Configurable Properties](#configurable-properties) @@ -17,12 +18,18 @@ - - [Movement Percentage](#movement-percentage) - - [Primary Skills](#primary-skills) - - [Secondary Skills](#secondary-skills) +- - [Can learn skills](#can-learn-skills) - - [Bonus System](#bonus-system) - - [Artifacts](#artifacts) - - [Spells](#spells) +- - [Can learn spells](#can-learn-spells) - - [Creatures](#creatures) - - [Creatures Change](#creatures-change) - - [Spell cast](#spell-cast) +- - [Fog of War](#fog-of-war) +- - [Player color](#player-color) +- - [Hero types](#hero-types) +- - [Hero classes](#hero-classes) ## Base object definition Rewardable object is defined similarly to other objects, with key difference being `handler`. This field must be set to `"handler" : "configurable"` in order for vcmi to use this mode. @@ -92,20 +99,47 @@ Rewardable object is defined similarly to other objects, with key difference bei // message that will be shown if this is the only available award "message": "{Warehouse of Crystal}" + // Alternative object description that will be used in place of generic description after player visits this object and reveals its content + // For example, Tree of Knowledge will display cost of levelup (gems or gold) only after object has been visited once + "description" : "", + // object will be disappeared after taking reward is set to true - "removeObject": false + "removeObject": false, // See "Configurable Properties" section for additional parameters } ], +/// List of variables shared between all rewards and limiters +/// See "Variables" section for description +"variables" : { +} + // If true, hero can not move to visitable tile of the object and will access this object from adjacent tile (e.g. Treasure Chest) "blockedVisitable" : true, // Message that will be shown if there are no applicable awards "onEmptyMessage": "", +// Object description that will be shown when player right-clicks object +"description" : "", + +// If set to true, right-clicking previously visited object would show preview of its content. For example, Witch Hut will show icon with provided skill +"showScoutedPreview" : true, + +// Text that should be used if hero has not visited this object. If not specified, game will use standard "(Not visited)" text +"notVisitedTooltip" : "", + +// Text that should be used if hero has already visited this object. If not specified, game will use standard "(Already visited)" text +"visitedTooltip" : "", + +// Used only if visitMode is set to "limiter" +// Hero that passes this limiter will be considered to have visited this object +// Note that if player or his allies have never visited this object, it will always show up as "not visited" +"visitLimiter" : { +}, + // Alternatively, rewards for empty state: // Format is identical to "rewards" section, allowing to fine-tune behavior in this case, including giving awards or different messages to explain why object is "empty". For example, Tree of Knowledge will give different messages depending on whether it asks for gold or crystals "onEmpty" : [ @@ -133,6 +167,7 @@ Rewardable object is defined similarly to other objects, with key difference bei // determines who can revisit object before reset // "once", - object can only be visited once. First visitor takes it all. // "hero", - object can be visited if this hero has not visited it before +// "limiter", - object can be visited if hero fails to fulfill provided limiter // "player", - object can be visited if this player has not visited it before // "bonus" - object can be visited if hero no longer has bonus from this object (including any other object of the same type) // "unlimited" - no restriction on revisiting. @@ -147,6 +182,39 @@ Rewardable object is defined similarly to other objects, with key difference bei } ``` +## Variables Parameters definition + +This property allows defining "variables" that are shared between all rewards and limiters of this object. +Variables are randomized only once, so you can use them multiple times for example, to give skill only if hero does not have this skill (e.g. Witch Hut). + +Example of creation of a variable named "gainedSkill" of type "secondarySkill": +```json +"variables" : { + "secondarySkill" : { + "gainedSkill" : { + "noneOf" : [ + "leadership", + "necromancy" + ] + } + } +} +``` + +Possible variable types: +- number: can be used in any place that expects a number +- artifact +- spell +- primarySkill +- secondarySkill + +To reference variable in limiter prepend variable name with '@' symbol: +```json +"secondary" : { + "@gainedSkill" : 1 +}, +``` + ## Reset Parameters definition This property describes how object state should be reset. Objects without this field will never reset its state. - Period describes interval between object resets in day. Periods are counted from game start and not from hero visit, so reset duration of 7 will always reset object on new week & duration of 28 will always reset on new month. @@ -220,7 +288,7 @@ Keep in mind, that all randomization is performed on map load and on object rese ```jsonc "resources": [ { - "list" : [ "wood", "ore" ], + "anyOf" : [ "wood", "ore" ], "amount" : 10 }, { @@ -349,13 +417,21 @@ Keep in mind, that all randomization is performed on map load and on object rese ] ``` +### Can learn skills + +- Can be used as limiter. Hero must have free skill slot to pass limiter + +```json + "canLearnSkills" : true +``` + ### Bonus System - Can be used as reward, to grant bonus to player - if present, MORALE and LUCK bonus will add corresponding image component to UI. - Note that unlike most values, parameter of bonuses can NOT be randomized - Description can be string or number of corresponding string from `arraytxt.txt` -```jsonc +```json "bonuses" : [ { "type" : "MORALE", @@ -414,6 +490,22 @@ Keep in mind, that all randomization is performed on map load and on object rese ], ``` +### Can learn spells + +- Can be used as limiter. Hero must be able to learn spell to pass the limiter +- Hero is considered to not able to learn the spell if: +- - he already has specified spell +- - he does not have a spellbook +- - he does not have sufficient Wisdom level for this spell + +```json + "canLearnSpells" : [ + "magicArrow" +], +``` + +canLearnSpells + ### Creatures - Can be used as limiter - Can be used as reward, to give new creatures to a hero @@ -445,13 +537,32 @@ Keep in mind, that all randomization is performed on map load and on object rese - As reward, instantly casts adventure map spell for visiting hero. All checks for spell book, wisdom or presence of mana will be ignored. It's possible to specify school level at which spell will be casted. If it's necessary to reduce player's mana or do some checks, they shall be introduced as limiters and other rewards - School level possible values: 1 (basic), 2 (advanced), 3 (expert) -```jsonc +```json "spellCast" : { "spell" : "townPortal", "schoolLevel": 3 } ``` +### Fog of War + +- Can NOT be used as limiter +- Can be used as reward, to reveal or hide affected tiles +- If radius is not specified, then all matching tiles on the map will be affected +- It is possible to specify which terrain classes should be affected. Tile will be affected if sum of values its classes is positive. For example, `"water" : 1` will affect all water tiles, while `"surface" : 1, "subterra" : -1` will include terrains that have "surface" flag but do not have "subterra" flag +- If 'hide' is set to true, then instead of revealing terrain, game will hide affected tiles for all other players + +```json +"revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1, + "hide" : true +} +``` + ### Player color - Can be used as limiter - Can NOT be used as reward diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index b991d7dda..b297198f3 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -609,49 +609,60 @@ void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node) ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) { - auto getAllowedArts = [&](std::vector > &out, std::vector *arts, CArtifact::EartClass flag) + std::set potentialPicks; + + // Select artifacts that satisfy provided criterias + for (auto const * artifact : allowedArtifacts) { - if (arts->empty()) //restock available arts - fillList(*arts, flag); + assert(artifact->aClass != CArtifact::ART_SPECIAL); // should be filtered out when allowedArtifacts is initialized - for (auto & arts_i : *arts) - { - if (accepts(arts_i->id)) - { - CArtifact *art = arts_i; - out.emplace_back(art); - } - } - }; + if ((flags & CArtifact::ART_TREASURE) == 0 && artifact->aClass == CArtifact::ART_TREASURE) + continue; - auto getAllowed = [&](std::vector > &out) + if ((flags & CArtifact::ART_MINOR) == 0 && artifact->aClass == CArtifact::ART_MINOR) + continue; + + if ((flags & CArtifact::ART_MAJOR) == 0 && artifact->aClass == CArtifact::ART_MAJOR) + continue; + + if ((flags & CArtifact::ART_RELIC) == 0 && artifact->aClass == CArtifact::ART_RELIC) + continue; + + if (!accepts(artifact->id)) + continue; + + potentialPicks.insert(artifact->id); + } + + return pickRandomArtifact(rand, potentialPicks); +} + +ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, std::set potentialPicks) +{ + // No allowed artifacts at all - give Grail - this can't be banned (hopefully) + // FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior + if (potentialPicks.empty()) { - if (flags & CArtifact::ART_TREASURE) - getAllowedArts (out, &treasures, CArtifact::ART_TREASURE); - if (flags & CArtifact::ART_MINOR) - getAllowedArts (out, &minors, CArtifact::ART_MINOR); - if (flags & CArtifact::ART_MAJOR) - getAllowedArts (out, &majors, CArtifact::ART_MAJOR); - if (flags & CArtifact::ART_RELIC) - getAllowedArts (out, &relics, CArtifact::ART_RELIC); - if(out.empty()) //no artifact of specified rarity, we need to take another one - { - getAllowedArts (out, &treasures, CArtifact::ART_TREASURE); - getAllowedArts (out, &minors, CArtifact::ART_MINOR); - getAllowedArts (out, &majors, CArtifact::ART_MAJOR); - getAllowedArts (out, &relics, CArtifact::ART_RELIC); - } - if(out.empty()) //no arts are available at all - { - out.resize (64); - std::fill_n (out.begin(), 64, objects[2]); //Give Grail - this can't be banned (hopefully) - } - }; + logGlobal->warn("Failed to find artifact that matches requested parameters!"); + return ArtifactID::GRAIL; + } - std::vector > out; - getAllowed(out); - ArtifactID artID = (*RandomGeneratorUtil::nextItem(out, rand))->id; - erasePickedArt(artID); + // Find how many times least used artifacts were picked by randomizer + int leastUsedTimes = std::numeric_limits::max(); + for (auto const & artifact : potentialPicks) + if (allocatedArtifacts[artifact] < leastUsedTimes) + leastUsedTimes = allocatedArtifacts[artifact]; + + // Pick all artifacts that were used least number of times + std::set preferredPicks; + for (auto const & artifact : potentialPicks) + if (allocatedArtifacts[artifact] == leastUsedTimes) + preferredPicks.insert(artifact); + + assert(!preferredPicks.empty()); + + ArtifactID artID = *RandomGeneratorUtil::nextItem(preferredPicks, rand); + allocatedArtifacts[artID] += 1; // record +1 more usage return artID; } @@ -712,16 +723,13 @@ bool CArtHandler::legalArtifact(const ArtifactID & id) void CArtHandler::initAllowedArtifactsList(const std::vector &allowed) { allowedArtifacts.clear(); - treasures.clear(); - minors.clear(); - majors.clear(); - relics.clear(); + allocatedArtifacts.clear(); for (ArtifactID i=ArtifactID::SPELLBOOK; i < ArtifactID(static_cast(objects.size())); i.advance(1)) { if (allowed[i] && legalArtifact(ArtifactID(i))) allowedArtifacts.push_back(objects[i]); - //keep im mind that artifact can be worn by more than one type of bearer + //keep im mind that artifact can be worn by more than one type of bearer } } @@ -734,52 +742,6 @@ std::vector CArtHandler::getDefaultAllowed() const return allowedArtifacts; } -void CArtHandler::erasePickedArt(const ArtifactID & id) -{ - CArtifact *art = objects[id]; - - std::vector * artifactList = nullptr; - switch(art->aClass) - { - case CArtifact::ART_TREASURE: - artifactList = &treasures; - break; - case CArtifact::ART_MINOR: - artifactList = &minors; - break; - case CArtifact::ART_MAJOR: - artifactList = &majors; - break; - case CArtifact::ART_RELIC: - artifactList = &relics; - break; - default: - logMod->warn("Problem: cannot find list for artifact %s, strange class. (special?)", art->getNameTranslated()); - return; - } - - if(artifactList->empty()) - fillList(*artifactList, art->aClass); - - auto itr = vstd::find(*artifactList, art); - if(itr != artifactList->end()) - { - artifactList->erase(itr); - } - else - logMod->warn("Problem: cannot erase artifact %s from list, it was not present", art->getNameTranslated()); -} - -void CArtHandler::fillList( std::vector &listToBeFilled, CArtifact::EartClass artifactClass ) -{ - assert(listToBeFilled.empty()); - for (auto & elem : allowedArtifacts) - { - if (elem->aClass == artifactClass) - listToBeFilled.push_back(elem); - } -} - void CArtHandler::afterLoadFinalization() { //All artifacts have their id, so we can properly update their bonuses' source ids. diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index 9f592a5f3..b3846e304 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -172,21 +172,21 @@ public: class DLL_LINKAGE CArtHandler : public CHandlerBase { public: - std::vector treasures, minors, majors, relics; //tmp vectors!!! do not touch if you don't know what you are doing!!! + /// Stores number of times each artifact was placed on map via randomization + std::map allocatedArtifacts; + /// List of artifacts allowed on the map std::vector allowedArtifacts; - std::set growingArtifacts; void addBonuses(CArtifact *art, const JsonNode &bonusList); - void fillList(std::vector &listToBeFilled, CArtifact::EartClass artifactClass); //fills given empty list with allowed artifacts of given class. No side effects - static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor /// Gets a artifact ID randomly and removes the selected artifact from this handler. ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function accepts); ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts); + ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::set filtered); bool legalArtifact(const ArtifactID & id); void initAllowedArtifactsList(const std::vector &allowed); //allowed[art_id] -> 0 if not allowed, 1 if allowed @@ -207,11 +207,7 @@ public: { h & objects; h & allowedArtifacts; - h & treasures; - h & minors; - h & majors; - h & relics; - h & growingArtifacts; + h & allocatedArtifacts; } protected: @@ -224,8 +220,6 @@ private: void loadClass(CArtifact * art, const JsonNode & node) const; void loadType(CArtifact * art, const JsonNode & node) const; void loadComponents(CArtifact * art, const JsonNode & node); - - void erasePickedArt(const ArtifactID & id); }; struct DLL_LINKAGE ArtSlotInfo diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 077f862c5..2be582779 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -607,7 +607,7 @@ EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, Bu { const TerrainTile *tile = getTile(t->bestLocation(), false); - if(!tile || tile->terType->isLand()) + if(!tile || !tile->terType->isWater()) return EBuildingState::NO_WATER; //lack of water } @@ -942,7 +942,7 @@ bool CGameInfoCallback::isInTheMap(const int3 &pos) const void CGameInfoCallback::getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula) const { - gs->getTilesInRange(tiles, pos, radious, *getPlayerID(), -1, distanceFormula); + gs->getTilesInRange(tiles, pos, radious, ETileVisibility::REVEALED, *getPlayerID(), distanceFormula); } void CGameInfoCallback::calculatePaths(const std::shared_ptr & config) @@ -955,7 +955,6 @@ void CGameInfoCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo & gs->calculatePaths(hero, out); } - const CArtifactInstance * CGameInfoCallback::getArtInstance( ArtifactInstanceID aid ) const { return gs->map->artInstances[aid.num]; diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index f68d6b3d4..ea0faea4a 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -475,6 +475,7 @@ CGeneralTextHandler::CGeneralTextHandler(): readToVector("core.cmpmusic", "DATA/CMPMUSIC.TXT" ); readToVector("core.minename", "DATA/MINENAME.TXT" ); readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); + readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" ); static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS))) diff --git a/lib/CSkillHandler.h b/lib/CSkillHandler.h index da5c8777b..72750fe8c 100644 --- a/lib/CSkillHandler.h +++ b/lib/CSkillHandler.h @@ -48,7 +48,7 @@ private: std::string identifier; public: - CSkill(const SecondarySkill & id = SecondarySkill::DEFAULT, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false); + CSkill(const SecondarySkill & id = SecondarySkill::NONE, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false); ~CSkill() = default; enum class Obligatory : ui8 diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 4efa55cab..673f8cfa7 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -78,8 +78,8 @@ void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, const int3 & pos, int radious, + ETileVisibility mode, std::optional player, - int mode, int3::EDistanceFormula distanceFormula) const { if(!!player && !player->isValidPlayer()) @@ -88,7 +88,7 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, return; } if(radious == CBuilding::HEIGHT_SKYSHIP) //reveal entire map - getAllTiles (tiles, player, -1, MapTerrainFilterMode::NONE); + getAllTiles (tiles, player, -1, [](auto * tile){return true;}); else { const TeamState * team = !player ? nullptr : gs->getPlayerTeam(*player); @@ -97,13 +97,13 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, for (int yd = std::max(pos.y - radious, 0); yd <= std::min(pos.y + radious, gs->map->height - 1); yd++) { int3 tilePos(xd,yd,pos.z); - double distance = pos.dist(tilePos, distanceFormula); + int distance = pos.dist(tilePos, distanceFormula); if(distance <= radious) { if(!player - || (mode == 1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 0) - || (mode == -1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 1) + || (mode == ETileVisibility::HIDDEN && (*team->fogOfWarMap)[pos.z][xd][yd] == 0) + || (mode == ETileVisibility::REVEALED && (*team->fogOfWarMap)[pos.z][xd][yd] == 1) ) tiles.insert(int3(xd,yd,pos.z)); } @@ -112,7 +112,7 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, } } -void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std::optional Player, int level, MapTerrainFilterMode tileFilterMode) const +void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std::optional Player, int level, std::function filter) const { if(!!Player && !Player->isValidPlayer()) { @@ -137,29 +137,9 @@ void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std: { for(int yd = 0; yd < gs->map->height; yd++) { - bool isTileEligible = false; - - switch(tileFilterMode) - { - case MapTerrainFilterMode::NONE: - isTileEligible = true; - break; - case MapTerrainFilterMode::WATER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isWater(); - break; - case MapTerrainFilterMode::LAND: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isLand(); - break; - case MapTerrainFilterMode::LAND_CARTOGRAPHER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isSurfaceCartographerCompatible(); - break; - case MapTerrainFilterMode::UNDERGROUND_CARTOGRAPHER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isUndergroundCartographerCompatible(); - break; - } - - if(isTileEligible) - tiles.insert(int3(xd, yd, zd)); + int3 coordinates(xd, yd, zd); + if (filter(getTile(coordinates))) + tiles.insert(coordinates); } } } diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index 03e96dbeb..33d7737bf 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -43,15 +43,6 @@ namespace scripting class DLL_LINKAGE CPrivilegedInfoCallback : public CGameInfoCallback { public: - enum class MapTerrainFilterMode - { - NONE = 0, - LAND = 1, - WATER = 2, - LAND_CARTOGRAPHER = 3, - UNDERGROUND_CARTOGRAPHER = 4 - }; - CGameState *gameState(); //used for random spawns @@ -60,14 +51,13 @@ public: //mode 1 - only unrevealed tiles; mode 0 - all, mode -1 - only revealed void getTilesInRange(std::unordered_set & tiles, const int3 & pos, - int radious, + int radius, + ETileVisibility mode, std::optional player = std::optional(), - int mode = 0, int3::EDistanceFormula formula = int3::DIST_2D) const; //returns all tiles on given level (-1 - both levels, otherwise number of level) - void getAllTiles(std::unordered_set &tiles, std::optional player = std::optional(), - int level = -1, MapTerrainFilterMode tileFilterMode = MapTerrainFilterMode::NONE) const; + void getAllTiles(std::unordered_set &tiles, std::optional player, int level, std::function filter) const; //gives 3 treasures, 3 minors, 1 major -> used by Black Market and Artifact Merchant void pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const; @@ -135,8 +125,8 @@ public: virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0; virtual void sendAndApply(CPackForClient * pack) = 0; virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map - virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) = 0; - virtual void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) = 0; + virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) = 0; + virtual void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) = 0; virtual void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) = 0; }; diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 1c2e9c0e8..249b36060 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -753,10 +753,7 @@ std::shared_ptr JsonUtils::parseBonus(const JsonNode &ability) { // caller code can not handle this case and presumes that returned bonus is always valid logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toJson()); - b->type = BonusType::NONE; - assert(0); // or throw? Game *should* work with dummy bonus - return b; } return b; diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 95ce1f4f4..2e78df61f 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -32,157 +32,102 @@ VCMI_LIB_NAMESPACE_BEGIN namespace JsonRandom { - si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue) + si32 loadVariable(std::string variableGroup, const std::string & value, const Variables & variables, si32 defaultValue) + { + if (value.empty() || value[0] != '@') + { + logMod->warn("Invalid syntax in load value! Can not load value from '%s'", value); + return defaultValue; + } + + std::string variableID = variableGroup + value; + + if (variables.count(variableID) == 0) + { + logMod->warn("Invalid syntax in load value! Unknown variable '%s'", value); + return defaultValue; + } + return variables.at(variableID); + } + + si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue) { if(value.isNull()) return defaultValue; if(value.isNumber()) return static_cast(value.Float()); + if (value.isString()) + return loadVariable("number", value.String(), variables, defaultValue); + if(value.isVector()) { const auto & vector = value.Vector(); size_t index= rng.getIntRange(0, vector.size()-1)(); - return loadValue(vector[index], rng, 0); + return loadValue(vector[index], rng, variables, 0); } if(value.isStruct()) { if (!value["amount"].isNull()) - return static_cast(loadValue(value["amount"], rng, defaultValue)); - si32 min = static_cast(loadValue(value["min"], rng, 0)); - si32 max = static_cast(loadValue(value["max"], rng, 0)); + return static_cast(loadValue(value["amount"], rng, variables, defaultValue)); + si32 min = static_cast(loadValue(value["min"], rng, variables, 0)); + si32 max = static_cast(loadValue(value["max"], rng, variables, 0)); return rng.getIntRange(min, max)(); } return defaultValue; } - std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set & valuesSet) + template + IdentifierType decodeKey(const std::string & modScope, const std::string & value, const Variables & variables) { - if(value.isString()) - return value.String(); - - if(value.isStruct()) - { - if(!value["type"].isNull()) - return value["type"].String(); - - if(!value["anyOf"].isNull()) - return RandomGeneratorUtil::nextItem(value["anyOf"].Vector(), rng)->String(); - - if(!value["noneOf"].isNull()) - { - auto copyValuesSet = valuesSet; - for(auto & s : value["noneOf"].Vector()) - copyValuesSet.erase(s.String()); - - if(!copyValuesSet.empty()) - return *RandomGeneratorUtil::nextItem(copyValuesSet, rng); - } - } - - return valuesSet.empty() ? "" : *RandomGeneratorUtil::nextItem(valuesSet, rng); + if (value.empty() || value[0] != '@') + return IdentifierType(*VLC->identifiers()->getIdentifier(modScope, IdentifierType::entityType(), value)); + else + return loadVariable(IdentifierType::entityType(), value, variables, IdentifierType::NONE); } - TResources loadResources(const JsonNode & value, CRandomGenerator & rng) + template + IdentifierType decodeKey(const JsonNode & value, const Variables & variables) { - TResources ret; - - if (value.isVector()) - { - for (const auto & entry : value.Vector()) - ret += loadResource(entry, rng); - return ret; - } - - for (size_t i=0; iidentifiers()->getIdentifier(IdentifierType::entityType(), value)); + else + return loadVariable(IdentifierType::entityType(), value.String(), variables, IdentifierType::NONE); } - TResources loadResource(const JsonNode & value, CRandomGenerator & rng) + template<> + PlayerColor decodeKey(const JsonNode & value, const Variables & variables) { - std::set defaultResources(std::begin(GameConstants::RESOURCE_NAMES), std::end(GameConstants::RESOURCE_NAMES) - 1); //except mithril - - std::string resourceName = loadKey(value, rng, defaultResources); - si32 resourceAmount = loadValue(value, rng, 0); - si32 resourceID(VLC->identifiers()->getIdentifier(value.meta, "resource", resourceName).value()); - - TResources ret; - ret[resourceID] = resourceAmount; - return ret; + return PlayerColor(*VLC->identifiers()->getIdentifier("playerColor", value)); } - - std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng) + template<> + PrimarySkill decodeKey(const JsonNode & value, const Variables & variables) { - std::vector ret; - if(value.isStruct()) - { - for(const auto & name : NPrimarySkill::names) - { - ret.push_back(loadValue(value[name], rng)); - } - } - if(value.isVector()) - { - ret.resize(GameConstants::PRIMARY_SKILLS, 0); - std::set defaultStats(std::begin(NPrimarySkill::names), std::end(NPrimarySkill::names)); - for(const auto & element : value.Vector()) - { - auto key = loadKey(element, rng, defaultStats); - defaultStats.erase(key); - int id = vstd::find_pos(NPrimarySkill::names, key); - if(id != -1) - ret[id] += loadValue(element, rng); - } - } - return ret; + return PrimarySkill(*VLC->identifiers()->getIdentifier("primarySkill", value)); } - std::map loadSecondary(const JsonNode & value, CRandomGenerator & rng) + template<> + PrimarySkill decodeKey(const std::string & modScope, const std::string & value, const Variables & variables) { - std::map ret; - if(value.isStruct()) - { - for(const auto & pair : value.Struct()) - { - SecondarySkill id(VLC->identifiers()->getIdentifier(pair.second.meta, "skill", pair.first).value()); - ret[id] = loadValue(pair.second, rng); - } - } - if(value.isVector()) - { - std::set defaultSkills; - for(const auto & skill : VLC->skillh->objects) - { - IObjectInterface::cb->isAllowed(2, skill->getIndex()); - auto scopeAndName = vstd::splitStringToPair(skill->getJsonKey(), ':'); - if(scopeAndName.first == ModScope::scopeBuiltin() || scopeAndName.first == value.meta) - defaultSkills.insert(scopeAndName.second); - else - defaultSkills.insert(skill->getJsonKey()); - } - - for(const auto & element : value.Vector()) - { - auto key = loadKey(element, rng, defaultSkills); - defaultSkills.erase(key); //avoid dupicates - if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "skill", key)) - { - SecondarySkill id(identifier.value()); - ret[id] = loadValue(element, rng); - } - } - } - return ret; + if (value.empty() || value[0] != '@') + return PrimarySkill(*VLC->identifiers()->getIdentifier(modScope, "primarySkill", value)); + else + return PrimarySkill(loadVariable("primarySkill", value, variables, static_cast(PrimarySkill::NONE))); } - ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng) + /// Method that allows type-specific object filtering + /// Default implementation is to accept all input objects + template + std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) { - if (value.getType() == JsonNode::JsonType::DATA_STRING) - return ArtifactID(VLC->identifiers()->getIdentifier("artifact", value).value()); + return valuesSet; + } + + template<> + std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) + { + assert(value.isStruct()); std::set allowedClasses; std::set allowedPositions; @@ -201,55 +146,54 @@ namespace JsonRandom for(const auto & entry : value["slot"].Vector()) allowedPositions.insert(ArtifactPosition::decode(entry.String())); - if (!value["minValue"].isNull()) minValue = static_cast(value["minValue"].Float()); - if (!value["maxValue"].isNull()) maxValue = static_cast(value["maxValue"].Float()); + if (!value["minValue"].isNull()) + minValue = static_cast(value["minValue"].Float()); + if (!value["maxValue"].isNull()) + maxValue = static_cast(value["maxValue"].Float()); - return VLC->arth->pickRandomArtifact(rng, [=](const ArtifactID & artID) -> bool + std::set result; + + for (auto const & artID : valuesSet) { CArtifact * art = VLC->arth->objects[artID]; if(!vstd::iswithin(art->getPrice(), minValue, maxValue)) - return false; + continue; if(!allowedClasses.empty() && !allowedClasses.count(art->aClass)) - return false; - + continue; + if(!IObjectInterface::cb->isAllowed(1, art->getIndex())) - return false; + continue; if(!allowedPositions.empty()) { + bool positionAllowed = false; for(const auto & pos : art->getPossibleSlots().at(ArtBearer::HERO)) { if(allowedPositions.count(pos)) - return true; + positionAllowed = true; } - return false; + + if (!positionAllowed) + continue; } - return true; - }); - } - std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng) - { - std::vector ret; - for (const JsonNode & entry : value.Vector()) - { - ret.push_back(loadArtifact(entry, rng)); + result.insert(artID); } - return ret; + return result; } - SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector spells) + template<> + std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) { - if (value.getType() == JsonNode::JsonType::DATA_STRING) - return SpellID(VLC->identifiers()->getIdentifier("spell", value).value()); + std::set result = valuesSet; if (!value["level"].isNull()) { - int32_t spellLevel = value["level"].Float(); + int32_t spellLevel = value["level"].Integer(); - vstd::erase_if(spells, [=](const SpellID & spell) + vstd::erase_if(result, [=](const SpellID & spell) { return VLC->spellh->getById(spell)->getLevel() != spellLevel; }); @@ -259,47 +203,244 @@ namespace JsonRandom { int32_t schoolID = VLC->identifiers()->getIdentifier("spellSchool", value["school"]).value(); - vstd::erase_if(spells, [=](const SpellID & spell) + vstd::erase_if(result, [=](const SpellID & spell) { return !VLC->spellh->getById(spell)->hasSchool(SpellSchool(schoolID)); }); } - - if (spells.empty()) - { - logMod->warn("Failed to select suitable random spell!"); - return SpellID::NONE; - } - return SpellID(*RandomGeneratorUtil::nextItem(spells, rng)); + return result; } - std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector & spells) + template + std::set filterKeys(const JsonNode & value, const std::set & valuesSet, const Variables & variables) { - std::vector ret; - for (const JsonNode & entry : value.Vector()) + if(value.isString()) + return { decodeKey(value, variables) }; + + assert(value.isStruct()); + + if(value.isStruct()) { - ret.push_back(loadSpell(entry, rng, spells)); + if(!value["type"].isNull()) + return filterKeys(value["type"], valuesSet, variables); + + std::set filteredTypes = filterKeysTyped(value, valuesSet); + + if(!value["anyOf"].isNull()) + { + std::set filteredAnyOf; + for (auto const & entry : value["anyOf"].Vector()) + { + std::set subset = filterKeys(entry, valuesSet, variables); + filteredAnyOf.insert(subset.begin(), subset.end()); + } + + vstd::erase_if(filteredTypes, [&](const IdentifierType & value) + { + return filteredAnyOf.count(value) == 0; + }); + } + + if(!value["noneOf"].isNull()) + { + for (auto const & entry : value["noneOf"].Vector()) + { + std::set subset = filterKeys(entry, valuesSet, variables); + for (auto bannedEntry : subset ) + filteredTypes.erase(bannedEntry); + } + } + + return filteredTypes; + } + return valuesSet; + } + + TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + TResources ret; + + if (value.isVector()) + { + for (const auto & entry : value.Vector()) + ret += loadResource(entry, rng, variables); + return ret; + } + + for (size_t i=0; i loadColors(const JsonNode & value, CRandomGenerator & rng) + TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set defaultResources{ + GameResID::WOOD, + GameResID::MERCURY, + GameResID::ORE, + GameResID::SULFUR, + GameResID::CRYSTAL, + GameResID::GEMS, + GameResID::GOLD + }; + + std::set potentialPicks = filterKeys(value, defaultResources, variables); + GameResID resourceID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + si32 resourceAmount = loadValue(value, rng, variables, 0); + + TResources ret; + ret[resourceID] = resourceAmount; + return ret; + } + + PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set defaultSkills{ + PrimarySkill::ATTACK, + PrimarySkill::DEFENSE, + PrimarySkill::SPELL_POWER, + PrimarySkill::KNOWLEDGE + }; + std::set potentialPicks = filterKeys(value, defaultSkills, variables); + return *RandomGeneratorUtil::nextItem(potentialPicks, rng); + } + + std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::vector ret(GameConstants::PRIMARY_SKILLS, 0); + std::set defaultSkills{ + PrimarySkill::ATTACK, + PrimarySkill::DEFENSE, + PrimarySkill::SPELL_POWER, + PrimarySkill::KNOWLEDGE + }; + + if(value.isStruct()) + { + for(const auto & pair : value.Struct()) + { + PrimarySkill id = decodeKey(pair.second.meta, pair.first, variables); + ret[static_cast(id)] += loadValue(pair.second, rng, variables); + } + } + if(value.isVector()) + { + for(const auto & element : value.Vector()) + { + std::set potentialPicks = filterKeys(element, defaultSkills, variables); + PrimarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + + defaultSkills.erase(skillID); + ret[static_cast(skillID)] += loadValue(element, rng, variables); + } + } + return ret; + } + + SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set defaultSkills; + for(const auto & skill : VLC->skillh->objects) + if (IObjectInterface::cb->isAllowed(2, skill->getIndex())) + defaultSkills.insert(skill->getId()); + + std::set potentialPicks = filterKeys(value, defaultSkills, variables); + return *RandomGeneratorUtil::nextItem(potentialPicks, rng); + } + + std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::map ret; + if(value.isStruct()) + { + for(const auto & pair : value.Struct()) + { + SecondarySkill id = decodeKey(pair.second.meta, pair.first, variables); + ret[id] = loadValue(pair.second, rng, variables); + } + } + if(value.isVector()) + { + std::set defaultSkills; + for(const auto & skill : VLC->skillh->objects) + if (IObjectInterface::cb->isAllowed(2, skill->getIndex())) + defaultSkills.insert(skill->getId()); + + for(const auto & element : value.Vector()) + { + std::set potentialPicks = filterKeys(element, defaultSkills, variables); + SecondarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + + defaultSkills.erase(skillID); //avoid dupicates + ret[skillID] = loadValue(element, rng, variables); + } + } + return ret; + } + + ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set allowedArts; + for (auto const * artifact : VLC->arth->allowedArtifacts) + allowedArts.insert(artifact->getId()); + + std::set potentialPicks = filterKeys(value, allowedArts, variables); + + return VLC->arth->pickRandomArtifact(rng, potentialPicks); + } + + std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::vector ret; + for (const JsonNode & entry : value.Vector()) + { + ret.push_back(loadArtifact(entry, rng, variables)); + } + return ret; + } + + SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set defaultSpells; + for(const auto & spell : VLC->spellh->objects) + if (IObjectInterface::cb->isAllowed(0, spell->getIndex())) + defaultSpells.insert(spell->getId()); + + std::set potentialPicks = filterKeys(value, defaultSpells, variables); + + if (potentialPicks.empty()) + { + logMod->warn("Failed to select suitable random spell!"); + return SpellID::NONE; + } + return *RandomGeneratorUtil::nextItem(potentialPicks, rng); + } + + std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::vector ret; + for (const JsonNode & entry : value.Vector()) + { + ret.push_back(loadSpell(entry, rng, variables)); + } + return ret; + } + + std::vector loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; - std::set def; - - for(auto & color : GameConstants::PLAYER_COLOR_NAMES) - def.insert(color); - + std::set defaultPlayers; + for(size_t i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + defaultPlayers.insert(PlayerColor(i)); + for(auto & entry : value.Vector()) { - auto key = loadKey(entry, rng, def); - auto pos = vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, key); - if(pos < 0) - logMod->warn("Unable to determine player color %s", key); - else - ret.emplace_back(pos); + std::set potentialPicks = filterKeys(entry, defaultPlayers, variables); + ret.push_back(*RandomGeneratorUtil::nextItem(potentialPicks, rng)); } + return ret; } @@ -323,11 +464,25 @@ namespace JsonRandom return ret; } - CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng) + CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { CStackBasicDescriptor stack; - stack.type = VLC->creh->objects[VLC->identifiers()->getIdentifier("creature", value["type"]).value()]; - stack.count = loadValue(value, rng); + + std::set defaultCreatures; + for(const auto & creature : VLC->creh->objects) + if (!creature->special) + defaultCreatures.insert(creature->getId()); + + std::set potentialPicks = filterKeys(value, defaultCreatures, variables); + CreatureID pickedCreature; + + if (!potentialPicks.empty()) + pickedCreature = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + else + logMod->warn("Failed to select suitable random creature!"); + + stack.type = VLC->creh->objects[pickedCreature]; + stack.count = loadValue(value, rng, variables); if (!value["upgradeChance"].isNull() && !stack.type->upgrades.empty()) { if (int(value["upgradeChance"].Float()) > rng.nextInt(99)) // select random upgrade @@ -338,17 +493,17 @@ namespace JsonRandom return stack; } - std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng) + std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; for (const JsonNode & node : value.Vector()) { - ret.push_back(loadCreature(node, rng)); + ret.push_back(loadCreature(node, rng, variables)); } return ret; } - std::vector evaluateCreatures(const JsonNode & value) + std::vector evaluateCreatures(const JsonNode & value, const Variables & variables) { std::vector ret; for (const JsonNode & node : value.Vector()) @@ -374,13 +529,6 @@ namespace JsonRandom return ret; } - //std::vector loadComponents(const JsonNode & value) - //{ - // std::vector ret; - // return ret; - // //TODO - //} - std::vector DLL_LINKAGE loadBonuses(const JsonNode & value) { std::vector ret; diff --git a/lib/JsonRandom.h b/lib/JsonRandom.h index ef066d14e..804c87e3e 100644 --- a/lib/JsonRandom.h +++ b/lib/JsonRandom.h @@ -24,6 +24,8 @@ class CStackBasicDescriptor; namespace JsonRandom { + using Variables = std::map; + struct DLL_LINKAGE RandomStackInfo { std::vector allowedCreatures; @@ -31,29 +33,30 @@ namespace JsonRandom si32 maxAmount; }; - DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue = 0); - DLL_LINKAGE std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set & valuesSet = {}); - DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::map loadSecondary(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue = 0); - DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector spells = {}); - DLL_LINKAGE std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector & spells = {}); + DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector evaluateCreatures(const JsonNode & value); + DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE std::vector loadColors(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector evaluateCreatures(const JsonNode & value, const Variables & variables); + + DLL_LINKAGE std::vector loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); DLL_LINKAGE std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadBonuses(const JsonNode & value); - //DLL_LINKAGE std::vector loadComponents(const JsonNode & value); } VCMI_LIB_NAMESPACE_END diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 132e2a94e..6bc6e3f9d 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -348,7 +348,7 @@ struct DLL_LINKAGE FoWChange : public CPackForClient std::unordered_set tiles; PlayerColor player; - ui8 mode = 0; //mode==0 - hide, mode==1 - reveal + ETileVisibility mode; bool waitForDialogs = false; virtual void visitTyped(ICPackVisitor & visitor) override; @@ -556,17 +556,14 @@ struct DLL_LINKAGE AddQuest : public CPackForClient struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient { - std::vector treasures, minors, majors, relics; + std::map allocatedArtifacts; void applyGs(CGameState * gs) const; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { - h & treasures; - h & minors; - h & majors; - h & relics; + h & allocatedArtifacts; } }; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index c37000719..817565efa 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -853,10 +853,7 @@ void AddQuest::applyGs(CGameState * gs) const void UpdateArtHandlerLists::applyGs(CGameState * gs) const { - VLC->arth->minors = minors; - VLC->arth->majors = majors; - VLC->arth->treasures = treasures; - VLC->arth->relics = relics; + VLC->arth->allocatedArtifacts = allocatedArtifacts; } void UpdateMapEvents::applyGs(CGameState * gs) const @@ -932,8 +929,9 @@ void FoWChange::applyGs(CGameState *gs) TeamState * team = gs->getPlayerTeam(player); auto fogOfWarMap = team->fogOfWarMap; for(const int3 & t : tiles) - (*fogOfWarMap)[t.z][t.x][t.y] = mode; - if (mode == 0) //do not hide too much + (*fogOfWarMap)[t.z][t.x][t.y] = mode != ETileVisibility::HIDDEN; + + if (mode == ETileVisibility::HIDDEN) //do not hide too much { std::unordered_set tilesRevealed; for (auto & elem : gs->map->objects) @@ -948,7 +946,7 @@ void FoWChange::applyGs(CGameState *gs) case Obj::TOWN: case Obj::ABANDONED_MINE: if(vstd::contains(team->players, o->tempOwner)) //check owned observators - gs->getTilesInRange(tilesRevealed, o->getSightCenter(), o->getSightRadius(), o->tempOwner, 1); + gs->getTilesInRange(tilesRevealed, o->getSightCenter(), o->getSightRadius(), ETileVisibility::HIDDEN, o->tempOwner); break; } } diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index 2c33519d1..f7a3eb96f 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -155,9 +155,14 @@ bool TerrainType::isWater() const return passabilityType & PassabilityType::WATER; } +bool TerrainType::isRock() const +{ + return passabilityType & PassabilityType::ROCK; +} + bool TerrainType::isPassable() const { - return !(passabilityType & PassabilityType::ROCK); + return !isRock(); } bool TerrainType::isSurface() const @@ -170,16 +175,6 @@ bool TerrainType::isUnderground() const return passabilityType & PassabilityType::SUBTERRANEAN; } -bool TerrainType::isSurfaceCartographerCompatible() const -{ - return isSurface(); -} - -bool TerrainType::isUndergroundCartographerCompatible() const -{ - return isLand() && isPassable() && !isSurface(); -} - bool TerrainType::isTransitionRequired() const { return transitionRequired; diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index 60d649d3c..b94e4427d 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -84,12 +84,13 @@ public: bool isLand() const; bool isWater() const; + bool isRock() const; + bool isPassable() const; + bool isSurface() const; bool isUnderground() const; bool isTransitionRequired() const; - bool isSurfaceCartographerCompatible() const; - bool isUndergroundCartographerCompatible() const; template void serialize(Handler &h, const int version) { diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index eaa790c9b..4c8e86d15 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -323,4 +323,14 @@ const ObstacleInfo * Obstacle::getInfo() const return VLC->obstacles()->getById(*this); } +std::string GameResID::entityType() +{ + return "resource"; +} + +std::string SecondarySkill::entityType() +{ + return "secondarySkill"; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 7ca9a1333..f48be1217 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -308,8 +308,7 @@ class SecondarySkillBase : public IdentifierBase public: enum Type : int32_t { - WRONG = -2, - DEFAULT = -1, + NONE = -1, PATHFINDING = 0, ARCHERY, LOGISTICS, @@ -347,6 +346,7 @@ class SecondarySkill : public IdentifierWithEnum::IdentifierWithEnum; + static std::string entityType(); }; class DLL_LINKAGE FactionID : public Identifier @@ -937,7 +937,7 @@ public: COUNT, WOOD_AND_ORE = 127, // special case for town bonus resource - INVALID = -1 + NONE = -1 }; }; @@ -945,6 +945,8 @@ class GameResID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; + + static std::string entityType(); }; // Deprecated diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index 1c2cd7b06..c594cc271 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -250,4 +250,10 @@ enum class EBattleResult : int8_t SURRENDER = 2, }; +enum class ETileVisibility : int8_t // Fog of war change +{ + HIDDEN, + REVEALED +}; + VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 183c6e4ec..6fc187834 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -942,7 +942,7 @@ void CGameState::initFogOfWar() if(!obj || !vstd::contains(elem.second.players, obj->tempOwner)) continue; //not a flagged object std::unordered_set tiles; - getTilesInRange(tiles, obj->getSightCenter(), obj->getSightRadius(), obj->tempOwner, 1); + getTilesInRange(tiles, obj->getSightCenter(), obj->getSightRadius(), ETileVisibility::HIDDEN, obj->tempOwner); for(const int3 & tile : tiles) { (*elem.second.fogOfWarMap)[tile.z][tile.x][tile.y] = 1; diff --git a/lib/int3.h b/lib/int3.h index aa060703a..014c5c8fa 100644 --- a/lib/int3.h +++ b/lib/int3.h @@ -110,7 +110,7 @@ public: switch(formula) { case DIST_2D: - return static_cast(dist2d(o)); + return std::round(dist2d(o)); case DIST_MANHATTAN: return static_cast(mandist2d(o)); case DIST_CHEBYSHEV: diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp index ed10802c3..f61641a85 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp @@ -37,17 +37,15 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input) BankConfig CBankInstanceConstructor::generateConfig(const JsonNode & level, CRandomGenerator & rng) const { BankConfig bc; + JsonRandom::Variables emptyVariables; bc.chance = static_cast(level["chance"].Float()); - bc.guards = JsonRandom::loadCreatures(level["guards"], rng); - - std::vector spells; - IObjectInterface::cb->getAllowedSpells(spells); + bc.guards = JsonRandom::loadCreatures(level["guards"], rng, emptyVariables); bc.resources = ResourceSet(level["reward"]["resources"]); - bc.creatures = JsonRandom::loadCreatures(level["reward"]["creatures"], rng); - bc.artifacts = JsonRandom::loadArtifacts(level["reward"]["artifacts"], rng); - bc.spells = JsonRandom::loadSpells(level["reward"]["spells"], rng, spells); + bc.creatures = JsonRandom::loadCreatures(level["reward"]["creatures"], rng, emptyVariables); + bc.artifacts = JsonRandom::loadArtifacts(level["reward"]["artifacts"], rng, emptyVariables); + bc.spells = JsonRandom::loadSpells(level["reward"]["spells"], rng, emptyVariables); return bc; } @@ -105,10 +103,12 @@ static void addStackToArmy(IObjectInfo::CArmyStructure & army, const CCreature * IObjectInfo::CArmyStructure CBankInfo::minGuards() const { + JsonRandom::Variables emptyVariables; + std::vector armies; for(auto configEntry : config) { - auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"]); + auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"], emptyVariables); IObjectInfo::CArmyStructure army; for(auto & stack : stacks) { @@ -126,10 +126,12 @@ IObjectInfo::CArmyStructure CBankInfo::minGuards() const IObjectInfo::CArmyStructure CBankInfo::maxGuards() const { + JsonRandom::Variables emptyVariables; + std::vector armies; for(auto configEntry : config) { - auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"]); + auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"], emptyVariables); IObjectInfo::CArmyStructure army; for(auto & stack : stacks) { @@ -147,12 +149,13 @@ IObjectInfo::CArmyStructure CBankInfo::maxGuards() const TPossibleGuards CBankInfo::getPossibleGuards() const { + JsonRandom::Variables emptyVariables; TPossibleGuards out; for(const JsonNode & configEntry : config) { const JsonNode & guardsInfo = configEntry["guards"]; - auto stacks = JsonRandom::evaluateCreatures(guardsInfo); + auto stacks = JsonRandom::evaluateCreatures(guardsInfo, emptyVariables); IObjectInfo::CArmyStructure army; @@ -187,12 +190,13 @@ std::vector> CBankInfo::getPossibleResourcesReward() std::vector> CBankInfo::getPossibleCreaturesReward() const { + JsonRandom::Variables emptyVariables; std::vector> aproximateReward; for(const JsonNode & configEntry : config) { const JsonNode & guardsInfo = configEntry["reward"]["creatures"]; - auto stacks = JsonRandom::evaluateCreatures(guardsInfo); + auto stacks = JsonRandom::evaluateCreatures(guardsInfo, emptyVariables); for(auto stack : stacks) { diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 7b95aee49..e7fd552b4 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -26,7 +26,6 @@ #include "../mapObjectConstructors/DwellingInstanceConstructor.h" #include "../mapObjectConstructors/HillFortInstanceConstructor.h" #include "../mapObjectConstructors/ShipyardInstanceConstructor.h" -#include "../mapObjectConstructors/ShrineInstanceConstructor.h" #include "../mapObjects/CGCreature.h" #include "../mapObjects/CGPandoraBox.h" #include "../mapObjects/CQuest.h" @@ -54,7 +53,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER_CLASS("bank", CBankInstanceConstructor); SET_HANDLER_CLASS("boat", BoatInstanceConstructor); SET_HANDLER_CLASS("market", MarketInstanceConstructor); - SET_HANDLER_CLASS("shrine", ShrineInstanceConstructor); SET_HANDLER_CLASS("hillFort", HillFortInstanceConstructor); SET_HANDLER_CLASS("shipyard", ShipyardInstanceConstructor); SET_HANDLER_CLASS("monster", CreatureInstanceConstructor); @@ -71,7 +69,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("randomDwelling", CGDwelling); SET_HANDLER("generic", CGObjectInstance); - SET_HANDLER("cartographer", CCartographer); SET_HANDLER("artifact", CGArtifact); SET_HANDLER("borderGate", CGBorderGate); SET_HANDLER("borderGuard", CGBorderGuard); @@ -84,18 +81,15 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("magi", CGMagi); SET_HANDLER("mine", CGMine); SET_HANDLER("obelisk", CGObelisk); - SET_HANDLER("observatory", CGObservatory); SET_HANDLER("pandora", CGPandoraBox); SET_HANDLER("prison", CGHeroInstance); SET_HANDLER("questGuard", CGQuestGuard); - SET_HANDLER("scholar", CGScholar); SET_HANDLER("seerHut", CGSeerHut); SET_HANDLER("sign", CGSignBottle); SET_HANDLER("siren", CGSirens); SET_HANDLER("monolith", CGMonolith); SET_HANDLER("subterraneanGate", CGSubterraneanGate); SET_HANDLER("whirlpool", CGWhirlpool); - SET_HANDLER("witch", CGWitchHut); SET_HANDLER("terrain", CGTerrainPatch); #undef SET_HANDLER_CLASS diff --git a/lib/mapObjectConstructors/CRewardableConstructor.h b/lib/mapObjectConstructors/CRewardableConstructor.h index aeb8d6b62..0461148fa 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.h +++ b/lib/mapObjectConstructors/CRewardableConstructor.h @@ -35,8 +35,7 @@ public: { AObjectTypeHandler::serialize(h, version); - if (version >= 816) - h & objectInfo; + h & objectInfo; } }; diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index 7cea90465..800e930af 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -256,9 +256,11 @@ void MarketInstanceConstructor::initializeObject(CGMarket * market) const void MarketInstanceConstructor::randomizeObject(CGMarket * object, CRandomGenerator & rng) const { + JsonRandom::Variables emptyVariables; + if(auto * university = dynamic_cast(object)) { - for(auto skill : JsonRandom::loadSecondary(predefinedOffer, rng)) + for(auto skill : JsonRandom::loadSecondaries(predefinedOffer, rng, emptyVariables)) university->skills.push_back(skill.first.getNum()); } } diff --git a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp index ccf3a7895..6a137b976 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp @@ -93,7 +93,8 @@ void DwellingInstanceConstructor::randomizeObject(CGDwelling * object, CRandomGe } else if(guards.getType() == JsonNode::JsonType::DATA_VECTOR) //custom guards (eg. Elemental Conflux) { - for(auto & stack : JsonRandom::loadCreatures(guards, rng)) + JsonRandom::Variables emptyVariables; + for(auto & stack : JsonRandom::loadCreatures(guards, rng, emptyVariables)) { dwelling->putStack(SlotID(dwelling->stacksCount()), new CStackInstance(stack.type->getId(), stack.count)); } diff --git a/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp b/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp deleted file mode 100644 index 84ef806e8..000000000 --- a/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/* -* ShrineInstanceConstructor.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 "ShrineInstanceConstructor.h" - -#include "../mapObjects/MiscObjects.h" -#include "../JsonRandom.h" -#include "../IGameCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void ShrineInstanceConstructor::initTypeData(const JsonNode & config) -{ - parameters = config; -} - -void ShrineInstanceConstructor::randomizeObject(CGShrine * shrine, CRandomGenerator & rng) const -{ - auto visitTextParameter = parameters["visitText"]; - - if (visitTextParameter.isNumber()) - shrine->visitText.appendLocalString(EMetaText::ADVOB_TXT, static_cast(visitTextParameter.Float())); - else - shrine->visitText.appendRawString(visitTextParameter.String()); - - if(shrine->spell == SpellID::NONE) // shrine has no predefined spell - { - std::vector possibilities; - shrine->cb->getAllowedSpells(possibilities); - - shrine->spell =JsonRandom::loadSpell(parameters["spell"], rng, possibilities); - } -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/ShrineInstanceConstructor.h b/lib/mapObjectConstructors/ShrineInstanceConstructor.h deleted file mode 100644 index 0ceb22690..000000000 --- a/lib/mapObjectConstructors/ShrineInstanceConstructor.h +++ /dev/null @@ -1,34 +0,0 @@ -/* -* ShrineInstanceConstructor.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 "CDefaultObjectTypeHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGShrine; - -class ShrineInstanceConstructor final : public CDefaultObjectTypeHandler -{ - JsonNode parameters; - -protected: - void initTypeData(const JsonNode & config) override; - void randomizeObject(CGShrine * object, CRandomGenerator & rng) const override; - -public: - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & parameters; - } -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index b2c1d3261..7ff668945 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -18,6 +18,7 @@ #include "../CGeneralTextHandler.h" #include "../CSoundBase.h" #include "../GameSettings.h" +#include "../CPlayerState.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjectConstructors/CBankInstanceConstructor.h" #include "../IGameCallback.h" @@ -51,8 +52,37 @@ bool CBank::isCoastVisitable() const std::string CBank::getHoverText(PlayerColor player) const { - // TODO: record visited players - return getObjectName() + " " + visitedTxt(bc == nullptr); + if (!wasVisited(player)) + return getObjectName(); + + return getObjectName() + "\n" + visitedTxt(bc == nullptr); +} + +std::vector CBank::getPopupComponents(PlayerColor player) const +{ + if (!wasVisited(player)) + return {}; + + if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) + return {}; + + std::map guardsAmounts; + std::vector result; + + for (auto const & slot : Slots()) + if (slot.second) + guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount(); + + for (auto const & guard : guardsAmounts) + { + Component comp; + comp.id = Component::EComponentType::CREATURE; + comp.subtype = guard.first.getNum(); + comp.val = guard.second; + + result.push_back(comp); + } + return result; } void CBank::setConfig(const BankConfig & config) @@ -98,11 +128,14 @@ void CBank::newTurn(CRandomGenerator & rand) const bool CBank::wasVisited (PlayerColor player) const { - return !bc; //FIXME: player A should not know about visit done by player B + return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id)); } void CBank::onHeroVisit(const CGHeroInstance * h) const { + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); + cb->sendAndApply(&cov); + int banktext = 0; switch (ID) { @@ -130,28 +163,10 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const bd.player = h->getOwner(); bd.soundID = soundBase::invalid; // Sound is handled in json files, else two sounds are played bd.text.appendLocalString(EMetaText::ADVOB_TXT, banktext); + bd.components = getPopupComponents(h->getOwner()); if (banktext == 32) bd.text.replaceRawString(getObjectName()); - if (VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) - { - std::map guardsAmounts; - - for (auto const & slot : Slots()) - if (slot.second) - guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount(); - - for (auto const & guard : guardsAmounts) - { - Component comp; - comp.id = Component::EComponentType::CREATURE; - comp.subtype = guard.first.getNum(); - comp.val = guard.second; - - bd.components.push_back(comp); - } - } - cb->showBlockingDialog(&bd); } diff --git a/lib/mapObjects/CBank.h b/lib/mapObjects/CBank.h index 7d62f30a2..8f7105689 100644 --- a/lib/mapObjects/CBank.h +++ b/lib/mapObjects/CBank.h @@ -41,6 +41,8 @@ public: void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + std::vector getPopupComponents(PlayerColor player) const override; + template void serialize(Handler &h, const int version) { h & static_cast(*this); diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index aaa2a180f..0d53467ff 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -272,7 +272,7 @@ CGHeroInstance::CGHeroInstance(): { setNodeType(HERO); ID = Obj::HERO; - secSkills.emplace_back(SecondarySkill::DEFAULT, -1); + secSkills.emplace_back(SecondarySkill::NONE, -1); blockVisit = true; } @@ -334,7 +334,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) pushPrimSkill(static_cast(g), type->heroClass->primarySkillInitial[g]); } } - if(secSkills.size() == 1 && secSkills[0] == std::pair(SecondarySkill::DEFAULT, -1)) //set secondary skills to default + if(secSkills.size() == 1 && secSkills[0] == std::pair(SecondarySkill::NONE, -1)) //set secondary skills to default secSkills = type->secSkillsInit; if (gender == EHeroGender::DEFAULT) @@ -789,9 +789,9 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const } } -bool CGHeroInstance::canLearnSpell(const spells::Spell * spell) const +bool CGHeroInstance::canLearnSpell(const spells::Spell * spell, bool allowBanned) const { - if(!hasSpellbook()) + if(!hasSpellbook()) return false; if(spell->getLevel() > maxSpellLevel()) //not enough wisdom @@ -812,7 +812,7 @@ bool CGHeroInstance::canLearnSpell(const spells::Spell * spell) const return false;//creature abilities can not be learned } - if(!IObjectInterface::cb->isAllowed(0, spell->getIndex())) + if(!allowBanned && !IObjectInterface::cb->isAllowed(0, spell->getIndex())) { logGlobal->warn("Hero %s try to learn banned spell %s", nodeName(), spell->getNameTranslated()); return false;//banned spells should not be learned @@ -1598,7 +1598,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) bool normalSkills = false; for(const auto & p : secSkills) { - if(p.first == SecondarySkill(SecondarySkill::DEFAULT)) + if(p.first == SecondarySkill(SecondarySkill::NONE)) defaultSkills = true; else normalSkills = true; @@ -1636,7 +1636,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) secSkills.clear(); if(secondarySkills.getType() == JsonNode::JsonType::DATA_NULL) { - secSkills.emplace_back(SecondarySkill::DEFAULT, -1); + secSkills.emplace_back(SecondarySkill::NONE, -1); } else { diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index ab19d02f9..a8610b41e 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -175,7 +175,7 @@ public: int getCurrentLuck(int stack=-1, bool town=false) const; int32_t getSpellCost(const spells::Spell * sp) const; //do not use during battles -> bonuses from army would be ignored - bool canLearnSpell(const spells::Spell * spell) const; + bool canLearnSpell(const spells::Spell * spell, bool allowBanned = false) const; bool canCastThisSpell(const spells::Spell * spell) const; //determines if this hero can cast given spell; takes into account existing spell in spellbook, existing spellbook and artifact bonuses /// convert given position between map position (CGObjectInstance::pos) and visitable position used for hero interactions diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 1b2f0f92a..e162e64c1 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -271,6 +271,25 @@ std::string CGObjectInstance::getHoverText(const CGHeroInstance * hero) const return getHoverText(hero->tempOwner); } +std::string CGObjectInstance::getPopupText(PlayerColor player) const +{ + return getHoverText(player); +} +std::string CGObjectInstance::getPopupText(const CGHeroInstance * hero) const +{ + return getHoverText(hero); +} + +std::vector CGObjectInstance::getPopupComponents(PlayerColor player) const +{ + return {}; +} + +std::vector CGObjectInstance::getPopupComponents(const CGHeroInstance * hero) const +{ + return getPopupComponents(hero->getOwner()); +} + void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const { switch(ID) diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index fdbefa545..98f6dceba 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -112,6 +112,12 @@ public: /// Returns hero-specific hover name, including visited/not visited info. Default = player-specific name virtual std::string getHoverText(const CGHeroInstance * hero) const; + virtual std::string getPopupText(PlayerColor player) const; + virtual std::string getPopupText(const CGHeroInstance * hero) const; + + virtual std::vector getPopupComponents(PlayerColor player) const; + virtual std::vector getPopupComponents(const CGHeroInstance * hero) const; + /** OVERRIDES OF IObjectInterface **/ void initObj(CRandomGenerator & rand) override; diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 0a8f952a6..72d998d7f 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -396,6 +396,8 @@ bool CTownRewardableBuilding::wasVisitedBefore(const CGHeroInstance * contextHer return contextHero->hasBonusFrom(BonusSource::TOWN_STRUCTURE, Bonus::getSid32(town->town->faction->getIndex(), bID)); case Rewardable::VISIT_HERO: return visitors.find(contextHero->id) != visitors.end(); + case Rewardable::VISIT_LIMITER: + return configuration.visitLimiter.heroAllowed(contextHero); default: return false; } diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 7c9d97a7b..36543e448 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1066,11 +1066,7 @@ void CGTownInstance::battleFinished(const CGHeroInstance * hero, const BattleRes void CGTownInstance::onTownCaptured(const PlayerColor & winner) const { setOwner(winner); - FoWChange fw; - fw.player = winner; - fw.mode = 1; - cb->getTilesInRange(fw.tiles, getSightCenter(), getSightRadius(), winner, 1); - cb->sendAndApply(& fw); + cb->changeFogOfWar(getSightCenter(), getSightRadius(), winner, ETileVisibility::REVEALED); } void CGTownInstance::afterAddToMap(CMap * map) diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index ad3ebc5c6..f2e1a6953 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -21,13 +21,6 @@ VCMI_LIB_NAMESPACE_BEGIN -// FIXME: copy-pasted from CObjectHandler -static std::string visitedTxt(const bool visited) -{ - int id = visited ? 352 : 353; - return VLC->generaltexth->allTexts[id]; -} - void CRewardableObject::grantRewardWithMessage(const CGHeroInstance * contextHero, int index, bool markAsVisit) const { auto vi = configuration.info.at(index); @@ -54,17 +47,30 @@ void CRewardableObject::selectRewardWthMessage(const CGHeroInstance * contextHer BlockingDialog sd(configuration.canRefuse, rewardIndices.size() > 1); sd.player = contextHero->tempOwner; sd.text = dialog; - - if (rewardIndices.size() > 1) - for (auto index : rewardIndices) - sd.components.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); - - if (rewardIndices.size() == 1) - configuration.info.at(rewardIndices.front()).reward.loadComponents(sd.components, contextHero); - + sd.components = loadComponents(contextHero, rewardIndices); cb->showBlockingDialog(&sd); } +std::vector CRewardableObject::loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const +{ + std::vector result; + + if (rewardIndices.empty()) + return result; + + if (configuration.selectMode != Rewardable::SELECT_FIRST) + { + for (auto index : rewardIndices) + result.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); + } + else + { + configuration.info.at(rewardIndices.front()).reward.loadComponents(result, contextHero); + } + + return result; +} + void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const { if(!wasVisitedBefore(h)) @@ -124,6 +130,12 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const { logGlobal->debug("Revisiting already visited object"); + if (!wasVisited(h->getOwner())) + { + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); + cb->sendAndApply(&cov); + } + auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED); if (!visitedRewards.empty()) grantRewardWithMessage(h, visitedRewards[0], false); @@ -188,6 +200,8 @@ bool CRewardableObject::wasVisitedBefore(const CGHeroInstance * contextHero) con return contextHero->hasBonusFrom(BonusSource::OBJECT, ID); case Rewardable::VISIT_HERO: return contextHero->visitedObjects.count(ObjectInstanceID(id)); + case Rewardable::VISIT_LIMITER: + return configuration.visitLimiter.heroAllowed(contextHero); default: return false; } @@ -200,6 +214,7 @@ bool CRewardableObject::wasVisited(PlayerColor player) const case Rewardable::VISIT_UNLIMITED: case Rewardable::VISIT_BONUS: case Rewardable::VISIT_HERO: + case Rewardable::VISIT_LIMITER: return false; case Rewardable::VISIT_ONCE: case Rewardable::VISIT_PLAYER: @@ -209,6 +224,11 @@ bool CRewardableObject::wasVisited(PlayerColor player) const } } +bool CRewardableObject::wasScouted(PlayerColor player) const +{ + return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id)); +} + bool CRewardableObject::wasVisited(const CGHeroInstance * h) const { switch (configuration.visitMode) @@ -217,23 +237,104 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const return h->hasBonusFrom(BonusSource::OBJECT, ID); case Rewardable::VISIT_HERO: return h->visitedObjects.count(ObjectInstanceID(id)); + case Rewardable::VISIT_LIMITER: + return wasScouted(h->getOwner()) && configuration.visitLimiter.heroAllowed(h); default: - return wasVisited(h->tempOwner); + return wasVisited(h->getOwner()); } } +std::string CRewardableObject::getDisplayTextImpl(PlayerColor player, const CGHeroInstance * hero, bool includeDescription) const +{ + std::string result = getObjectName(); + + if (includeDescription && !getDescriptionMessage(player, hero).empty()) + result += "\n" + getDescriptionMessage(player, hero); + + if (hero) + { + if(configuration.visitMode != Rewardable::VISIT_UNLIMITED) + { + if (wasVisited(hero)) + result += "\n" + configuration.visitedTooltip.toString(); + else + result += "\n " + configuration.notVisitedTooltip.toString(); + } + } + else + { + if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE) + { + if (wasVisited(player)) + result += "\n" + configuration.visitedTooltip.toString(); + else + result += "\n" + configuration.notVisitedTooltip.toString(); + } + } + return result; +} + std::string CRewardableObject::getHoverText(PlayerColor player) const { - if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE) - return getObjectName() + " " + visitedTxt(wasVisited(player)); - return getObjectName(); + return getDisplayTextImpl(player, nullptr, false); } std::string CRewardableObject::getHoverText(const CGHeroInstance * hero) const { - if(configuration.visitMode != Rewardable::VISIT_UNLIMITED) - return getObjectName() + " " + visitedTxt(wasVisited(hero)); - return getObjectName(); + return getDisplayTextImpl(hero->getOwner(), hero, false); +} + +std::string CRewardableObject::getPopupText(PlayerColor player) const +{ + return getDisplayTextImpl(player, nullptr, true); +} + +std::string CRewardableObject::getPopupText(const CGHeroInstance * hero) const +{ + return getDisplayTextImpl(hero->getOwner(), hero, true); +} + +std::string CRewardableObject::getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const +{ + if (!wasScouted(player) || configuration.info.empty()) + return configuration.description.toString(); + + auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); + if (rewardIndices.empty() && !configuration.info[0].description.empty()) + return configuration.info[0].description.toString(); + + if (!configuration.info[rewardIndices.front()].description.empty()) + return configuration.info[rewardIndices.front()].description.toString(); + + return configuration.description.toString(); +} + +std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const +{ + if (!wasScouted(player)) + return {}; + + if (!configuration.showScoutedPreview) + return {}; + + auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); + if (rewardIndices.empty() && !configuration.info.empty()) + rewardIndices.push_back(0); + + if (rewardIndices.empty()) + return {}; + + return loadComponents(hero, rewardIndices); +} + +std::vector CRewardableObject::getPopupComponents(PlayerColor player) const +{ + return getPopupComponentsImpl(player, nullptr); +} + +std::vector CRewardableObject::getPopupComponents(const CGHeroInstance * hero) const +{ + return getPopupComponentsImpl(hero->getOwner(), hero); } void CRewardableObject::setPropertyDer(ui8 what, ui32 val) diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 86040a89b..3d9484dca 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE CRewardableObject : public CArmedInstance, public Rewardable::Interface { protected: - + bool onceVisitableObjectCleared = false; /// reward selected by player, no serialize @@ -37,10 +37,19 @@ protected: virtual void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const; virtual void selectRewardWthMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, const MetaString & dialog) const; + std::vector loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const; + + std::string getDisplayTextImpl(PlayerColor player, const CGHeroInstance * hero, bool includeDescription) const; + std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const; + std::vector getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const; + public: /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) bool wasVisited(PlayerColor player) const override; bool wasVisited(const CGHeroInstance * h) const override; + + /// Returns true if object was scouted by player and he is aware of its internal state + bool wasScouted(PlayerColor player) const; /// gives reward to player or ask for choice in case of multiple rewards void onHeroVisit(const CGHeroInstance *h) const override; @@ -63,6 +72,12 @@ public: std::string getHoverText(PlayerColor player) const override; std::string getHoverText(const CGHeroInstance * hero) const override; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + + std::vector getPopupComponents(PlayerColor player) const override; + std::vector getPopupComponents(const CGHeroInstance * hero) const override; + template void serialize(Handler &h, const int version) { h & static_cast(*this); @@ -74,10 +89,6 @@ public: //TODO: // MAX -// class DLL_LINKAGE CGPandoraBox : public CArmedInstance -// class DLL_LINKAGE CGEvent : public CGPandoraBox //event objects -// class DLL_LINKAGE CGSeerHut : public CArmedInstance, public IQuestObject //army is used when giving reward -// class DLL_LINKAGE CGQuestGuard : public CGSeerHut // class DLL_LINKAGE CBank : public CArmedInstance // class DLL_LINKAGE CGPyramid : public CBank @@ -90,7 +101,5 @@ public: // POSSIBLE // class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles -// class DLL_LINKAGE CGWitchHut : public CPlayersVisited -// class DLL_LINKAGE CGScholar : public CGObjectInstance VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 771f84186..86b8dd933 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -842,203 +842,6 @@ void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler) } } -void CGWitchHut::initObj(CRandomGenerator & rand) -{ - if (allowedAbilities.empty()) //this can happen for RMG and RoE maps. - { - auto defaultAllowed = VLC->skillh->getDefaultAllowed(); - - // Necromancy and Leadership can't be learned by default - defaultAllowed[SecondarySkill::NECROMANCY] = false; - defaultAllowed[SecondarySkill::LEADERSHIP] = false; - - for(int i = 0; i < defaultAllowed.size(); i++) - if (defaultAllowed[i] && cb->isAllowed(2, i)) - allowedAbilities.insert(SecondarySkill(i)); - } - ability = *RandomGeneratorUtil::nextItem(allowedAbilities, rand); -} - -void CGWitchHut::onHeroVisit( const CGHeroInstance * h ) const -{ - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - if(!wasVisited(h->tempOwner)) - cb->setObjProperty(id, CGWitchHut::OBJPROP_VISITED, h->tempOwner.getNum()); - ui32 txt_id; - if(h->getSecSkillLevel(SecondarySkill(ability))) //you already know this skill - { - txt_id =172; - } - else if(!h->canLearnSkill()) //already all skills slots used - { - txt_id = 173; - } - else //give sec skill - { - iw.components.emplace_back(Component::EComponentType::SEC_SKILL, ability, 1, 0); - txt_id = 171; - cb->changeSecSkill(h, SecondarySkill(ability), 1, true); - } - - iw.text.appendLocalString(EMetaText::ADVOB_TXT,txt_id); - iw.text.replaceLocalString(EMetaText::SEC_SKILL_NAME, ability); - cb->showInfoDialog(&iw); -} - -std::string CGWitchHut::getHoverText(PlayerColor player) const -{ - std::string hoverName = getObjectName(); - if(wasVisited(player)) - { - hoverName += "\n" + VLC->generaltexth->allTexts[356]; // + (learn %s) - boost::algorithm::replace_first(hoverName, "%s", VLC->skillh->getByIndex(ability)->getNameTranslated()); - } - return hoverName; -} - -std::string CGWitchHut::getHoverText(const CGHeroInstance * hero) const -{ - std::string hoverName = getHoverText(hero->tempOwner); - if(wasVisited(hero->tempOwner) && hero->getSecSkillLevel(SecondarySkill(ability))) //hero knows that ability - hoverName += "\n\n" + VLC->generaltexth->allTexts[357]; // (Already learned) - return hoverName; -} - -void CGWitchHut::serializeJsonOptions(JsonSerializeFormat & handler) -{ - //TODO: unify allowed abilities with others - make them std::vector - - std::vector temp; - size_t skillCount = VLC->skillh->size(); - temp.resize(skillCount, false); - - auto standard = VLC->skillh->getDefaultAllowed(); //todo: for WitchHut default is all except Leadership and Necromancy - - if(handler.saving) - { - for(SecondarySkill i(0); i < SecondarySkill(skillCount); ++i) - if(vstd::contains(allowedAbilities, i)) - temp[i] = true; - } - - handler.serializeLIC("allowedSkills", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, standard, temp); - - if(!handler.saving) - { - allowedAbilities.clear(); - for(si32 i = 0; i < skillCount; ++i) - if(temp[i]) - allowedAbilities.insert(SecondarySkill(i)); - } -} - -void CGObservatory::onHeroVisit( const CGHeroInstance * h ) const -{ - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->tempOwner; - switch (ID) - { - case Obj::REDWOOD_OBSERVATORY: - case Obj::PILLAR_OF_FIRE: - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,98 + (ID==Obj::PILLAR_OF_FIRE)); - - FoWChange fw; - fw.player = h->tempOwner; - fw.mode = 1; - cb->getTilesInRange (fw.tiles, pos, 20, h->tempOwner, 1); - cb->sendAndApply (&fw); - break; - } - case Obj::COVER_OF_DARKNESS: - { - iw.text.appendLocalString (EMetaText::ADVOB_TXT, 31); - for (auto & player : cb->gameState()->players) - { - if (cb->getPlayerStatus(player.first) == EPlayerStatus::INGAME && - cb->getPlayerRelations(player.first, h->tempOwner) == PlayerRelations::ENEMIES) - cb->changeFogOfWar(visitablePos(), 20, player.first, true); - } - break; - } - } - cb->showInfoDialog(&iw); -} - -void CGShrine::onHeroVisit( const CGHeroInstance * h ) const -{ - if(spell == SpellID::NONE) - { - logGlobal->error("Not initialized shrine visited!"); - return; - } - - if(!wasVisited(h->tempOwner)) - cb->setObjProperty(id, CGShrine::OBJPROP_VISITED, h->tempOwner.getNum()); - - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - iw.text = visitText; - iw.text.appendLocalString(EMetaText::SPELL_NAME,spell); - iw.text.appendRawString("."); - - if(!h->getArt(ArtifactPosition::SPELLBOOK)) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,131); - } - else if(h->spellbookContainsSpell(spell))//hero already knows the spell - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,174); - } - else if(spell.toSpell()->getLevel() > h->maxSpellLevel()) //it's third level spell and hero doesn't have wisdom - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,130); - } - else //give spell - { - std::set spells; - spells.insert(spell); - cb->changeSpells(h, true, spells); - - iw.components.emplace_back(Component::EComponentType::SPELL, spell, 0, 0); - } - - cb->showInfoDialog(&iw); -} - -void CGShrine::initObj(CRandomGenerator & rand) -{ - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); -} - -std::string CGShrine::getHoverText(PlayerColor player) const -{ - std::string hoverName = getObjectName(); - if(wasVisited(player)) - { - hoverName += "\n" + VLC->generaltexth->allTexts[355]; // + (learn %s) - boost::algorithm::replace_first(hoverName,"%s", spell.toSpell()->getNameTranslated()); - } - return hoverName; -} - -std::string CGShrine::getHoverText(const CGHeroInstance * hero) const -{ - std::string hoverName = getHoverText(hero->tempOwner); - if(wasVisited(hero->tempOwner) && hero->spellbookContainsSpell(spell)) //know what spell there is and hero knows that spell - hoverName += "\n\n" + VLC->generaltexth->allTexts[354]; // (Already learned) - return hoverName; -} - -void CGShrine::serializeJsonOptions(JsonSerializeFormat & handler) -{ - handler.serializeId("spell", spell, SpellID::NONE); -} - void CGSignBottle::initObj(CRandomGenerator & rand) { //if no text is set than we pick random from the predefined ones @@ -1071,132 +874,6 @@ void CGSignBottle::serializeJsonOptions(JsonSerializeFormat& handler) handler.serializeStruct("text", message); } -void CGScholar::onHeroVisit( const CGHeroInstance * h ) const -{ - EBonusType type = bonusType; - int bid = bonusID; - //check if the bonus if applicable, if not - give primary skill (always possible) - int ssl = h->getSecSkillLevel(SecondarySkill(bid)); //current sec skill level, used if bonusType == 1 - if((type == SECONDARY_SKILL && ((ssl == 3) || (!ssl && !h->canLearnSkill()))) ////hero already has expert level in the skill or (don't know skill and doesn't have free slot) - || (type == SPELL && !h->canLearnSpell(SpellID(bid).toSpell()))) - { - type = PRIM_SKILL; - bid = CRandomGenerator::getDefault().nextInt(GameConstants::PRIMARY_SKILLS - 1); - } - - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - iw.text.appendLocalString(EMetaText::ADVOB_TXT,115); - - switch (type) - { - case PRIM_SKILL: - cb->changePrimSkill(h,static_cast(bid),+1); - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, bid, +1, 0); - break; - case SECONDARY_SKILL: - cb->changeSecSkill(h,SecondarySkill(bid),+1); - iw.components.emplace_back(Component::EComponentType::SEC_SKILL, bid, ssl + 1, 0); - break; - case SPELL: - { - std::set hlp; - hlp.insert(SpellID(bid)); - cb->changeSpells(h,true,hlp); - iw.components.emplace_back(Component::EComponentType::SPELL, bid, 0, 0); - } - break; - default: - logGlobal->error("Error: wrong bonus type (%d) for Scholar!\n", static_cast(type)); - return; - } - - cb->showInfoDialog(&iw); - cb->removeObject(this, h->getOwner()); -} - -void CGScholar::initObj(CRandomGenerator & rand) -{ - blockVisit = true; - if(bonusType == RANDOM) - { - bonusType = static_cast(rand.nextInt(2)); - switch(bonusType) - { - case PRIM_SKILL: - bonusID = rand.nextInt(GameConstants::PRIMARY_SKILLS -1); - break; - case SECONDARY_SKILL: - bonusID = rand.nextInt(static_cast(VLC->skillh->size()) - 1); - break; - case SPELL: - std::vector possibilities; - cb->getAllowedSpells (possibilities); - bonusID = *RandomGeneratorUtil::nextItem(possibilities, rand); - break; - } - } -} - -void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler) -{ - if(handler.saving) - { - std::string value; - switch(bonusType) - { - case PRIM_SKILL: - value = NPrimarySkill::names[bonusID]; - handler.serializeString("rewardPrimSkill", value); - break; - case SECONDARY_SKILL: - value = CSkillHandler::encodeSkill(bonusID); - handler.serializeString("rewardSkill", value); - break; - case SPELL: - value = SpellID::encode(bonusID); - handler.serializeString("rewardSpell", value); - break; - case RANDOM: - break; - } - } - else - { - //TODO: unify - const JsonNode & json = handler.getCurrent(); - bonusType = RANDOM; - if(!json["rewardPrimSkill"].String().empty()) - { - auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "primSkill", json["rewardPrimSkill"].String()); - if(raw) - { - bonusType = PRIM_SKILL; - bonusID = raw.value(); - } - } - else if(!json["rewardSkill"].String().empty()) - { - auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "skill", json["rewardSkill"].String()); - if(raw) - { - bonusType = SECONDARY_SKILL; - bonusID = raw.value(); - } - } - else if(!json["rewardSpell"].String().empty()) - { - auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "spell", json["rewardSpell"].String()); - if(raw) - { - bonusType = SPELL; - bonusID = raw.value(); - } - } - } -} - void CGGarrison::onHeroVisit (const CGHeroInstance *h) const { auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); @@ -1267,14 +944,14 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const FoWChange fw; fw.player = h->tempOwner; - fw.mode = 1; + fw.mode = ETileVisibility::REVEALED; fw.waitForDialogs = true; for(const auto & it : eyelist[subID]) { const CGObjectInstance *eye = cb->getObj(it); - cb->getTilesInRange (fw.tiles, eye->pos, 10, h->tempOwner, 1); + cb->getTilesInRange (fw.tiles, eye->pos, 10, ETileVisibility::HIDDEN, h->tempOwner); cb->sendAndApply(&fw); cv.pos = eye->pos; @@ -1413,79 +1090,6 @@ BoatId CGShipyard::getBoatType() const return createdBoat; } -void CCartographer::onHeroVisit( const CGHeroInstance * h ) const -{ - //if player has not bought map of this subtype yet and underground exist for stalagmite cartographer - if (!wasVisited(h->getOwner()) && (subID != 2 || cb->gameState()->map->twoLevel)) - { - if (cb->getResource(h->tempOwner, EGameResID::GOLD) >= 1000) //if he can afford a map - { - //ask if he wants to buy one - int text=0; - switch (subID) - { - case 0: - text = 25; - break; - case 1: - text = 26; - break; - case 2: - text = 27; - break; - default: - logGlobal->warn("Unrecognized subtype of cartographer"); - } - assert(text); - BlockingDialog bd (true, false); - bd.player = h->getOwner(); - bd.text.appendLocalString (EMetaText::ADVOB_TXT, text); - cb->showBlockingDialog (&bd); - } - else //if he cannot afford - { - h->showInfoDialog(28); - } - } - else //if he already visited carographer - { - h->showInfoDialog(24); - } -} - -void CCartographer::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const -{ - if(answer) //if hero wants to buy map - { - cb->giveResource(hero->tempOwner, EGameResID::GOLD, -1000); - FoWChange fw; - fw.mode = 1; - fw.player = hero->tempOwner; - - //subIDs of different types of cartographers: - //water = 0; land = 1; underground = 2; - - IGameCallback::MapTerrainFilterMode tileFilterMode = IGameCallback::MapTerrainFilterMode::NONE; - - switch(subID) - { - case 0: - tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::WATER; - break; - case 1: - tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::LAND_CARTOGRAPHER; - break; - case 2: - tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::UNDERGROUND_CARTOGRAPHER; - break; - } - - cb->getAllTiles(fw.tiles, hero->tempOwner, -1, tileFilterMode); //reveal appropriate tiles - cb->sendAndApply(&fw); - cb->setObjProperty(id, CCartographer::OBJPROP_VISITED, hero->tempOwner.getNum()); - } -} - void CGDenOfthieves::onHeroVisit (const CGHeroInstance * h) const { cb->showObjectWindow(this, EOpenWindowMode::THIEVES_GUILD, h, false); diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 1b4397794..53021a4a9 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -57,46 +57,6 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; }; -class DLL_LINKAGE CGWitchHut : public CTeamVisited -{ -public: - std::set allowedAbilities; - SecondarySkill ability; - - std::string getHoverText(PlayerColor player) const override; - std::string getHoverText(const CGHeroInstance * hero) const override; - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & allowedAbilities; - h & ability; - } -protected: - void serializeJsonOptions(JsonSerializeFormat & handler) override; -}; - -class DLL_LINKAGE CGScholar : public CGObjectInstance -{ -public: - enum EBonusType {PRIM_SKILL, SECONDARY_SKILL, SPELL, RANDOM = 255}; - EBonusType bonusType; - ui16 bonusID; //ID of skill/spell - - CGScholar() : bonusType(EBonusType::RANDOM),bonusID(0){}; - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & bonusType; - h & bonusID; - } -protected: - void serializeJsonOptions(JsonSerializeFormat & handler) override; -}; - class DLL_LINKAGE CGGarrison : public CArmedInstance { public: @@ -169,27 +129,6 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; }; -class DLL_LINKAGE CGShrine : public CTeamVisited -{ -public: - MetaString visitText; - SpellID spell; //id of spell or NONE if random - - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - std::string getHoverText(PlayerColor player) const override; - std::string getHoverText(const CGHeroInstance * hero) const override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this);; - h & spell; - h & visitText; - } -protected: - void serializeJsonOptions(JsonSerializeFormat & handler) override; -}; - class DLL_LINKAGE CGMine : public CArmedInstance { public: @@ -334,17 +273,6 @@ public: } }; -class DLL_LINKAGE CGObservatory : public CGObjectInstance //Redwood observatory -{ -public: - void onHeroVisit(const CGHeroInstance * h) const override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - } -}; - class DLL_LINKAGE CGBoat : public CGObjectInstance, public CBonusSystemNode { public: @@ -416,19 +344,6 @@ public: } }; -class DLL_LINKAGE CCartographer : public CTeamVisited -{ -///behaviour varies depending on surface and floor -public: - void onHeroVisit(const CGHeroInstance * h) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - } -}; - class DLL_LINKAGE CGDenOfthieves : public CGObjectInstance { void onHeroVisit(const CGHeroInstance * h) const override; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 3df10a799..cc3e1b0a5 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -27,6 +27,7 @@ #include "../TerrainHandler.h" #include "../TextOperations.h" #include "../VCMI_Lib.h" +#include "../constants/StringConstants.h" #include "../filesystem/CBinaryReader.h" #include "../filesystem/Filesystem.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" @@ -34,6 +35,7 @@ #include "../mapObjects/CGCreature.h" #include "../mapObjects/MapObjects.h" #include "../mapObjects/ObjectTemplate.h" +#include "../modding/ModScope.h" #include "../spells/CSpellHandler.h" #include @@ -1139,32 +1141,102 @@ CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition) return object; } -CGObjectInstance * CMapLoaderH3M::readWitchHut() +CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::shared_ptr objectTemplate) { - auto * object = new CGWitchHut(); + auto * object = readGeneric(position, objectTemplate); + auto * rewardable = dynamic_cast(object); + + assert(rewardable); // AB and later maps have allowed abilities defined in H3M if(features.levelAB) { - reader->readBitmaskSkills(object->allowedAbilities, false); + std::set allowedAbilities; + reader->readBitmaskSkills(allowedAbilities, false); - if(object->allowedAbilities.size() != 1) + if(allowedAbilities.size() != 1) { auto defaultAllowed = VLC->skillh->getDefaultAllowed(); - for(int skillID = 0; skillID < VLC->skillh->size(); ++skillID) + for(int skillID = features.skillsCount; skillID < defaultAllowed.size(); ++skillID) if(defaultAllowed[skillID]) - object->allowedAbilities.insert(SecondarySkill(skillID)); + allowedAbilities.insert(SecondarySkill(skillID)); } + + JsonVector anyOfList; + + for (auto const & skill : allowedAbilities) + { + JsonNode entry; + entry.String() = VLC->skills()->getById(skill)->getJsonKey(); + anyOfList.push_back(entry); + } + JsonNode variable; + variable["anyOf"].Vector() = anyOfList; + variable.setMeta(ModScope::scopeGame()); // list may include skills from all mods + + rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); } return object; } -CGObjectInstance * CMapLoaderH3M::readScholar() +CGObjectInstance * CMapLoaderH3M::readScholar(const int3 & position, std::shared_ptr objectTemplate) { - auto * object = new CGScholar(); - object->bonusType = static_cast(reader->readUInt8()); - object->bonusID = reader->readUInt8(); + enum class ScholarBonusType : uint8_t { + PRIM_SKILL = 0, + SECONDARY_SKILL = 1, + SPELL = 2, + RANDOM = 255 + }; + + auto * object = readGeneric(position, objectTemplate); + auto * rewardable = dynamic_cast(object); + + uint8_t bonusTypeRaw = reader->readUInt8(); + auto bonusType = static_cast(bonusTypeRaw); + auto bonusID = reader->readUInt8(); + + switch (bonusType) + { + case ScholarBonusType::PRIM_SKILL: + { + JsonNode variable; + JsonNode dice; + variable.String() = NPrimarySkill::names[bonusID]; + variable.setMeta(ModScope::scopeGame()); + dice.Integer() = 80; + rewardable->configuration.presetVariable("primarySkill", "gainedStat", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::SECONDARY_SKILL: + { + JsonNode variable; + JsonNode dice; + variable.String() = VLC->skills()->getByIndex(bonusID)->getJsonKey(); + variable.setMeta(ModScope::scopeGame()); + dice.Integer() = 50; + rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::SPELL: + { + JsonNode variable; + JsonNode dice; + variable.String() = VLC->spells()->getByIndex(bonusID)->getJsonKey(); + variable.setMeta(ModScope::scopeGame()); + dice.Integer() = 20; + rewardable->configuration.presetVariable("spell", "gainedSpell", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::RANDOM: + break;// No-op + default: + logGlobal->warn("Map '%s': Invalid Scholar settings! Ignoring...", mapName); + } + reader->skipZero(6); return object; } @@ -1306,10 +1378,22 @@ CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, s return object; } -CGObjectInstance * CMapLoaderH3M::readShrine() +CGObjectInstance * CMapLoaderH3M::readShrine(const int3 & position, std::shared_ptr objectTemplate) { - auto * object = new CGShrine(); - object->spell = reader->readSpell32(); + auto * object = readGeneric(position, objectTemplate); + auto * rewardable = dynamic_cast(object); + + assert(rewardable); + + SpellID spell = reader->readSpell32(); + + if(spell != SpellID::NONE) + { + JsonNode variable; + variable.String() = VLC->spells()->getById(spell)->getJsonKey(); + variable.setMeta(ModScope::scopeGame()); // list may include spells from all mods + rewardable->configuration.presetVariable("spell", "gainedSpell", variable); + } return object; } @@ -1458,9 +1542,9 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptrsecSkills.empty()) { - if(object->secSkills[0].first != SecondarySkill::DEFAULT) + if(object->secSkills[0].first != SecondarySkill::NONE) logGlobal->debug("Map '%s': Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", mapName, object->getNameTextID(), object->subID); object->secSkills.clear(); } diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index 3eb7a3226..b2099fd6b 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -165,8 +165,8 @@ private: CGObjectInstance * readSeerHut(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); CGObjectInstance * readTown(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readSign(const int3 & position); - CGObjectInstance * readWitchHut(); - CGObjectInstance * readScholar(); + CGObjectInstance * readWitchHut(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readScholar(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readGarrison(const int3 & mapPosition); CGObjectInstance * readArtifact(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readResource(const int3 & position, std::shared_ptr objTempl); @@ -174,7 +174,7 @@ private: CGObjectInstance * readPandora(const int3 & position, const ObjectInstanceID & idToBeGiven); CGObjectInstance * readDwelling(const int3 & position); CGObjectInstance * readDwellingRandom(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readShrine(); + CGObjectInstance * readShrine(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readHeroPlaceholder(const int3 & position); CGObjectInstance * readGrail(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readPyramid(const int3 & position, std::shared_ptr objTempl); diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 8044535fb..c5d8cf9a3 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -368,7 +368,7 @@ void CPathfinderHelper::initializePatrol() if(hero->patrol.patrolRadius) { state = PATROL_RADIUS; - gs->getTilesInRange(patrolTiles, hero->patrol.initialPos, hero->patrol.patrolRadius, std::optional(), 0, int3::DIST_MANHATTAN); + gs->getTilesInRange(patrolTiles, hero->patrol.initialPos, hero->patrol.patrolRadius, ETileVisibility::REVEALED, std::optional(), int3::DIST_MANHATTAN); } else state = PATROL_LOCKED; diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index cde5b7c44..cc97eea6b 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -23,7 +23,6 @@ #include "../mapObjectConstructors/DwellingInstanceConstructor.h" #include "../mapObjectConstructors/HillFortInstanceConstructor.h" #include "../mapObjectConstructors/ShipyardInstanceConstructor.h" -#include "../mapObjectConstructors/ShrineInstanceConstructor.h" #include "../mapObjects/MapObjects.h" #include "../mapObjects/CGCreature.h" #include "../mapObjects/CGTownBuilding.h" @@ -55,8 +54,6 @@ void registerTypesMapObjects1(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); @@ -103,7 +100,6 @@ void registerTypesMapObjectTypes(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); @@ -112,7 +108,6 @@ void registerTypesMapObjectTypes(Serializer &s) #define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType >() REGISTER_GENERIC_HANDLER(CGObjectInstance); - REGISTER_GENERIC_HANDLER(CCartographer); REGISTER_GENERIC_HANDLER(CGArtifact); REGISTER_GENERIC_HANDLER(CGBlackMarket); REGISTER_GENERIC_HANDLER(CGBoat); @@ -132,14 +127,11 @@ void registerTypesMapObjectTypes(Serializer &s) REGISTER_GENERIC_HANDLER(CGMarket); REGISTER_GENERIC_HANDLER(CGMine); REGISTER_GENERIC_HANDLER(CGObelisk); - REGISTER_GENERIC_HANDLER(CGObservatory); REGISTER_GENERIC_HANDLER(CGPandoraBox); REGISTER_GENERIC_HANDLER(CGQuestGuard); REGISTER_GENERIC_HANDLER(CGResource); - REGISTER_GENERIC_HANDLER(CGScholar); REGISTER_GENERIC_HANDLER(CGSeerHut); REGISTER_GENERIC_HANDLER(CGShipyard); - REGISTER_GENERIC_HANDLER(CGShrine); REGISTER_GENERIC_HANDLER(CGSignBottle); REGISTER_GENERIC_HANDLER(CGSirens); REGISTER_GENERIC_HANDLER(CGMonolith); @@ -147,7 +139,6 @@ void registerTypesMapObjectTypes(Serializer &s) REGISTER_GENERIC_HANDLER(CGWhirlpool); REGISTER_GENERIC_HANDLER(CGTownInstance); REGISTER_GENERIC_HANDLER(CGUniversity); - REGISTER_GENERIC_HANDLER(CGWitchHut); REGISTER_GENERIC_HANDLER(HillFort); #undef REGISTER_GENERIC_HANDLER @@ -177,9 +168,6 @@ void registerTypesMapObjects2(Serializer &s) s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); //s.template registerType(); diff --git a/lib/rewardable/Configuration.cpp b/lib/rewardable/Configuration.cpp index 5e2430e62..4cab55235 100644 --- a/lib/rewardable/Configuration.cpp +++ b/lib/rewardable/Configuration.cpp @@ -24,6 +24,38 @@ ui16 Rewardable::Configuration::getResetDuration() const return resetParameters.period; } +std::optional Rewardable::Configuration::getVariable(const std::string & category, const std::string & name) const +{ + std::string variableID = category + '@' + name; + + if (variables.values.count(variableID)) + return variables.values.at(variableID); + + return std::nullopt; +} + +JsonNode Rewardable::Configuration::getPresetVariable(const std::string & category, const std::string & name) const +{ + std::string variableID = category + '@' + name; + + if (variables.preset.count(variableID)) + return variables.preset.at(variableID); + else + return JsonNode(); +} + +void Rewardable::Configuration::presetVariable(const std::string & category, const std::string & name, const JsonNode & value) +{ + std::string variableID = category + '@' + name; + variables.preset[variableID] = value; +} + +void Rewardable::Configuration::initVariable(const std::string & category, const std::string & name, int value) +{ + std::string variableID = category + '@' + name; + variables.values[variableID] = value; +} + void Rewardable::ResetInfo::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("period", period); @@ -39,6 +71,27 @@ void Rewardable::VisitInfo::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("visitType", visitType); } +void Rewardable::Variables::serializeJson(JsonSerializeFormat & handler) +{ + if (handler.saving) + { + JsonNode presetNode; + for (auto const & entry : preset) + presetNode[entry.first] = entry.second; + + handler.serializeRaw("preset", presetNode, {}); + } + else + { + preset.clear(); + JsonNode presetNode; + handler.serializeRaw("preset", presetNode, {}); + + for (auto const & entry : presetNode.Struct()) + preset[entry.first] = entry.second; + } +} + void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler) { handler.serializeStruct("onSelect", onSelect); @@ -47,6 +100,7 @@ void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler) handler.serializeEnum("visitMode", visitMode, std::vector{VisitModeString.begin(), VisitModeString.end()}); handler.serializeStruct("resetParameters", resetParameters); handler.serializeBool("canRefuse", canRefuse); + handler.serializeBool("showScoutedPreview", showScoutedPreview); handler.serializeInt("infoWindowType", infoWindowType); } diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index 72377da04..c31290506 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -26,6 +26,7 @@ enum EVisitMode VISIT_ONCE, // only once, first to visit get all the rewards VISIT_HERO, // every hero can visit object once VISIT_BONUS, // can be visited by any hero that don't have bonus from this object + VISIT_LIMITER, // can be visited by heroes that don't fulfill provided limiter VISIT_PLAYER // every player can visit object once }; @@ -46,7 +47,7 @@ enum class EEventType }; const std::array SelectModeString{"selectFirst", "selectPlayer", "selectRandom"}; -const std::array VisitModeString{"unlimited", "once", "hero", "bonus", "player"}; +const std::array VisitModeString{"unlimited", "once", "hero", "bonus", "limiter", "player"}; struct DLL_LINKAGE ResetInfo { @@ -62,7 +63,6 @@ struct DLL_LINKAGE ResetInfo /// if true - reset list of visitors (heroes & players) on reset bool visitors; - /// if true - re-randomize rewards on a new week bool rewards; @@ -84,9 +84,13 @@ struct DLL_LINKAGE VisitInfo /// Message that will be displayed on granting of this reward, if not empty MetaString message; + /// Object description that will be shown on right-click, after object name + /// Used only after player have "scouted" object and knows internal state of an object + MetaString description; + /// Event to which this reward is assigned EEventType visitType; - + void serializeJson(JsonSerializeFormat & handler); template void serialize(Handler &h, const int version) @@ -94,16 +98,44 @@ struct DLL_LINKAGE VisitInfo h & limiter; h & reward; h & message; + h & description; h & visitType; } }; +struct DLL_LINKAGE Variables +{ + /// List of variables used by this object in their current values + std::map values; + + /// List of per-instance preconfigured variables, e.g. from map + std::map preset; + + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & values; + h & preset; + } +}; + /// Base class that can handle granting rewards to visiting heroes. struct DLL_LINKAGE Configuration { /// Message that will be shown if player needs to select one of multiple rewards MetaString onSelect; + /// Object description that will be shown on right-click, after object name + /// Used only if player is not aware of object internal state, e.g. have never visited it + MetaString description; + + /// Text that will be shown if hero has not visited this object + MetaString notVisitedTooltip; + + /// Text that will be shown after hero has visited this object + MetaString visitedTooltip; + /// Rewards that can be applied by an object std::vector info; @@ -116,25 +148,45 @@ struct DLL_LINKAGE Configuration /// how and when should the object be reset Rewardable::ResetInfo resetParameters; + /// List of variables shoread between all limiters and rewards + Rewardable::Variables variables; + + /// Limiter that will be used to determine that object is visited. Only if visit mode is set to "limiter" + Rewardable::Limiter visitLimiter; + /// if true - player can refuse visiting an object (e.g. Tomb) bool canRefuse = false; + /// if true - right-clicking object will show preview of object rewards + bool showScoutedPreview = false; + /// if true - object info will shown in infobox (like resource pickup) EInfoWindowMode infoWindowType = EInfoWindowMode::AUTO; EVisitMode getVisitMode() const; ui16 getResetDuration() const; + + std::optional getVariable(const std::string & category, const std::string & name) const; + JsonNode getPresetVariable(const std::string & category, const std::string & name) const; + void presetVariable(const std::string & category, const std::string & name, const JsonNode & value); + void initVariable(const std::string & category, const std::string & name, int value); void serializeJson(JsonSerializeFormat & handler); template void serialize(Handler &h, const int version) { - h & info; - h & canRefuse; - h & resetParameters; h & onSelect; - h & visitMode; + h & description; + h & notVisitedTooltip; + h & visitedTooltip; + h & info; h & selectMode; + h & visitMode; + h & resetParameters; + h & variables; + h & visitLimiter; + h & canRefuse; + h & showScoutedPreview; h & infoWindowType; } }; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index b9a8f1b2b..aadf30285 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -24,19 +24,36 @@ VCMI_LIB_NAMESPACE_BEGIN namespace { - MetaString loadMessage(const JsonNode & value, const TextIdentifier & textIdentifier ) + MetaString loadMessage(const JsonNode & value, const TextIdentifier & textIdentifier, EMetaText textSource = EMetaText::ADVOB_TXT ) { MetaString ret; + + if (value.isVector()) + { + for(const auto & entry : value.Vector()) + { + if (entry.isNumber()) + ret.appendLocalString(textSource, static_cast(entry.Float())); + if (entry.isString()) + ret.appendRawString(entry.String()); + } + return ret; + } + if (value.isNumber()) { - ret.appendLocalString(EMetaText::ADVOB_TXT, static_cast(value.Float())); + ret.appendLocalString(textSource, static_cast(value.Float())); return ret; } if (value.String().empty()) return ret; - ret.appendTextID(textIdentifier.get()); + if (value.String()[0] == '@') + ret.appendTextID(value.String().substr(1)); + else + ret.appendTextID(textIdentifier.get()); + return ret; } @@ -56,7 +73,7 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o objectTextID = objectName; auto loadString = [&](const JsonNode & entry, const TextIdentifier & textID){ - if (entry.isString() && !entry.String().empty()) + if (entry.isString() && !entry.String().empty() && entry.String()[0] != '@') VLC->generaltexth->registerString(entry.meta, textID, entry.String()); }; @@ -81,6 +98,9 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o } loadString(parameters["onSelectMessage"], TextIdentifier(objectName, "onSelect")); + loadString(parameters["description"], TextIdentifier(objectName, "description")); + loadString(parameters["notVisitedTooltip"], TextIdentifier(objectName, "notVisitedText")); + loadString(parameters["visitedTooltip"], TextIdentifier(objectName, "visitedTooltip")); loadString(parameters["onVisitedMessage"], TextIdentifier(objectName, "onVisited")); loadString(parameters["onEmptyMessage"], TextIdentifier(objectName, "onEmpty")); } @@ -102,28 +122,27 @@ Rewardable::LimitersList Rewardable::Info::configureSublimiters(Rewardable::Conf void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Limiter & limiter, const JsonNode & source) const { - std::vector spells; - IObjectInterface::cb->getAllowedSpells(spells); + auto const & variables = object.variables.values; + limiter.dayOfWeek = JsonRandom::loadValue(source["dayOfWeek"], rng, variables); + limiter.daysPassed = JsonRandom::loadValue(source["daysPassed"], rng, variables); + limiter.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables); + limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables); + limiter.canLearnSkills = source["canLearnSkills"].Bool(); - limiter.dayOfWeek = JsonRandom::loadValue(source["dayOfWeek"], rng); - limiter.daysPassed = JsonRandom::loadValue(source["daysPassed"], rng); - limiter.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng); - limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng) - + JsonRandom::loadValue(source["minLevel"], rng); // VCMI 1.1 compatibilty + limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables); + limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng, variables); - limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng); - limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng); + limiter.resources = JsonRandom::loadResources(source["resources"], rng, variables); - limiter.resources = JsonRandom::loadResources(source["resources"], rng); - - limiter.primary = JsonRandom::loadPrimary(source["primary"], rng); - limiter.secondary = JsonRandom::loadSecondary(source["secondary"], rng); - limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng); - limiter.spells = JsonRandom::loadSpells(source["spells"], rng, spells); - limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng); + limiter.primary = JsonRandom::loadPrimaries(source["primary"], rng, variables); + limiter.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); + limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); + limiter.spells = JsonRandom::loadSpells(source["spells"], rng, variables); + limiter.canLearnSpells = JsonRandom::loadSpells(source["canLearnSpells"], rng, variables); + limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables); - limiter.players = JsonRandom::loadColors(source["colors"], rng); + limiter.players = JsonRandom::loadColors(source["colors"], rng, variables); limiter.heroes = JsonRandom::loadHeroes(source["heroes"], rng); limiter.heroClasses = JsonRandom::loadHeroClasses(source["heroClasses"], rng); @@ -134,39 +153,49 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Reward & reward, const JsonNode & source) const { - reward.resources = JsonRandom::loadResources(source["resources"], rng); + auto const & variables = object.variables.values; - reward.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng) - + JsonRandom::loadValue(source["gainedExp"], rng); // VCMI 1.1 compatibilty + reward.resources = JsonRandom::loadResources(source["resources"], rng, variables); - reward.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng) - + JsonRandom::loadValue(source["gainedLevels"], rng); // VCMI 1.1 compatibilty + reward.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables); + reward.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables); - reward.manaDiff = JsonRandom::loadValue(source["manaPoints"], rng); - reward.manaOverflowFactor = JsonRandom::loadValue(source["manaOverflowFactor"], rng); - reward.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, -1); + reward.manaDiff = JsonRandom::loadValue(source["manaPoints"], rng, variables); + reward.manaOverflowFactor = JsonRandom::loadValue(source["manaOverflowFactor"], rng, variables); + reward.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables, -1); - reward.movePoints = JsonRandom::loadValue(source["movePoints"], rng); - reward.movePercentage = JsonRandom::loadValue(source["movePercentage"], rng, -1); + reward.movePoints = JsonRandom::loadValue(source["movePoints"], rng, variables); + reward.movePercentage = JsonRandom::loadValue(source["movePercentage"], rng, variables, -1); reward.removeObject = source["removeObject"].Bool(); reward.bonuses = JsonRandom::loadBonuses(source["bonuses"]); - reward.primary = JsonRandom::loadPrimary(source["primary"], rng); - reward.secondary = JsonRandom::loadSecondary(source["secondary"], rng); + reward.primary = JsonRandom::loadPrimaries(source["primary"], rng, variables); + reward.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); - std::vector spells; - IObjectInterface::cb->getAllowedSpells(spells); - - reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng); - reward.spells = JsonRandom::loadSpells(source["spells"], rng, spells); - reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng); + reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); + reward.spells = JsonRandom::loadSpells(source["spells"], rng, variables); + reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables); if(!source["spellCast"].isNull() && source["spellCast"].isStruct()) { - reward.spellCast.first = JsonRandom::loadSpell(source["spellCast"]["spell"], rng); + reward.spellCast.first = JsonRandom::loadSpell(source["spellCast"]["spell"], rng, variables); reward.spellCast.second = source["spellCast"]["schoolLevel"].Integer(); } + if (!source["revealTiles"].isNull()) + { + auto const & entry = source["revealTiles"]; + + reward.revealTiles = RewardRevealTiles(); + reward.revealTiles->radius = JsonRandom::loadValue(entry["radius"], rng, variables); + reward.revealTiles->hide = entry["hide"].Bool(); + + reward.revealTiles->scoreSurface = JsonRandom::loadValue(entry["surface"], rng, variables); + reward.revealTiles->scoreSubterra = JsonRandom::loadValue(entry["subterra"], rng, variables); + reward.revealTiles->scoreWater = JsonRandom::loadValue(entry["water"], rng, variables); + reward.revealTiles->scoreRock = JsonRandom::loadValue(entry["rock"], rng, variables); + } + for ( auto node : source["changeCreatures"].Struct() ) { CreatureID from(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.first).value()); @@ -185,11 +214,66 @@ void Rewardable::Info::configureResetInfo(Rewardable::Configuration & object, CR resetParameters.rewards = source["rewards"].Bool(); } +void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const +{ + for(const auto & category : source.Struct()) + { + for(const auto & entry : category.second.Struct()) + { + JsonNode preset = object.getPresetVariable(category.first, entry.first); + const JsonNode & input = preset.isNull() ? entry.second : preset; + int32_t value = -1; + + if (category.first == "number") + value = JsonRandom::loadValue(input, rng, object.variables.values); + + if (category.first == "artifact") + value = JsonRandom::loadArtifact(input, rng, object.variables.values).getNum(); + + if (category.first == "spell") + value = JsonRandom::loadSpell(input, rng, object.variables.values).getNum(); + + if (category.first == "primarySkill") + value = static_cast(JsonRandom::loadPrimary(input, rng, object.variables.values)); + + if (category.first == "secondarySkill") + value = JsonRandom::loadSecondary(input, rng, object.variables.values).getNum(); + + object.initVariable(category.first, entry.first, value); + } + } +} + +void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables) const +{ + for (const auto & variable : variables.values ) + { + if( boost::algorithm::starts_with(variable.first, "spell")) + target.replaceLocalString(EMetaText::SPELL_NAME, variable.second); + + if( boost::algorithm::starts_with(variable.first, "secondarySkill")) + target.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); + } +} + +void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const +{ + for (const auto & artifact : info.reward.artifacts ) + target.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); + + for (const auto & artifact : info.reward.spells ) + target.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); + + for (const auto & secondary : info.reward.secondary ) + target.replaceLocalString(EMetaText::SEC_SKILL_NAME, secondary.first.getNum()); + + replaceTextPlaceholders(target, variables); +} + void Rewardable::Info::configureRewards( Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, - std::map & thrownDice, Rewardable::EEventType event, const std::string & modeName) const { @@ -200,21 +284,32 @@ void Rewardable::Info::configureRewards( if (!reward["appearChance"].isNull()) { JsonNode chance = reward["appearChance"]; - si32 diceID = static_cast(chance["dice"].Float()); + std::string diceID = std::to_string(chance["dice"].Integer()); - if (thrownDice.count(diceID) == 0) - thrownDice[diceID] = rng.getIntRange(0, 99)(); + auto diceValue = object.getVariable("dice", diceID); + + if (!diceValue.has_value()) + { + const JsonNode & preset = object.getPresetVariable("dice", diceID); + if (preset.isNull()) + object.initVariable("dice", diceID, rng.getIntRange(0, 99)()); + else + object.initVariable("dice", diceID, preset.Integer()); + + diceValue = object.getVariable("dice", diceID); + } + assert(diceValue.has_value()); if (!chance["min"].isNull()) { int min = static_cast(chance["min"].Float()); - if (min > thrownDice[diceID]) + if (min > *diceValue) continue; } if (!chance["max"].isNull()) { int max = static_cast(chance["max"].Float()); - if (max <= thrownDice[diceID]) + if (max <= *diceValue) continue; } } @@ -225,12 +320,10 @@ void Rewardable::Info::configureRewards( info.visitType = event; info.message = loadMessage(reward["message"], TextIdentifier(objectTextID, modeName, i)); + info.description = loadMessage(reward["description"], TextIdentifier(objectTextID, "description", modeName, i), EMetaText::GENERAL_TXT); - for (const auto & artifact : info.reward.artifacts ) - info.message.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); - - for (const auto & artifact : info.reward.spells ) - info.message.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); + replaceTextPlaceholders(info.message, object.variables, info); + replaceTextPlaceholders(info.description, object.variables, info); object.info.push_back(info); } @@ -240,19 +333,30 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand { object.info.clear(); - std::map thrownDice; + configureVariables(object, rng, parameters["variables"]); - configureRewards(object, rng, parameters["rewards"], thrownDice, Rewardable::EEventType::EVENT_FIRST_VISIT, "rewards"); - configureRewards(object, rng, parameters["onVisited"], thrownDice, Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited"); - configureRewards(object, rng, parameters["onEmpty"], thrownDice, Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty"); + configureRewards(object, rng, parameters["rewards"], Rewardable::EEventType::EVENT_FIRST_VISIT, "rewards"); + configureRewards(object, rng, parameters["onVisited"], Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited"); + configureRewards(object, rng, parameters["onEmpty"], Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty"); - object.onSelect = loadMessage(parameters["onSelectMessage"], TextIdentifier(objectTextID, "onSelect")); + object.onSelect = loadMessage(parameters["onSelectMessage"], TextIdentifier(objectTextID, "onSelect")); + object.description = loadMessage(parameters["description"], TextIdentifier(objectTextID, "description")); + object.notVisitedTooltip = loadMessage(parameters["notVisitedTooltip"], TextIdentifier(objectTextID, "notVisitedTooltip"), EMetaText::GENERAL_TXT); + object.visitedTooltip = loadMessage(parameters["visitedTooltip"], TextIdentifier(objectTextID, "visitedTooltip"), EMetaText::GENERAL_TXT); + + if (object.notVisitedTooltip.empty()) + object.notVisitedTooltip.appendTextID("core.genrltxt.353"); + + if (object.visitedTooltip.empty()) + object.visitedTooltip.appendTextID("core.genrltxt.352"); if (!parameters["onVisitedMessage"].isNull()) { Rewardable::VisitInfo onVisited; onVisited.visitType = Rewardable::EEventType::EVENT_ALREADY_VISITED; onVisited.message = loadMessage(parameters["onVisitedMessage"], TextIdentifier(objectTextID, "onVisited")); + replaceTextPlaceholders(onVisited.message, object.variables); + object.info.push_back(onVisited); } @@ -261,12 +365,15 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand Rewardable::VisitInfo onEmpty; onEmpty.visitType = Rewardable::EEventType::EVENT_NOT_AVAILABLE; onEmpty.message = loadMessage(parameters["onEmptyMessage"], TextIdentifier(objectTextID, "onEmpty")); + replaceTextPlaceholders(onEmpty.message, object.variables); + object.info.push_back(onEmpty); } configureResetInfo(object, rng, object.resetParameters, parameters["resetParameters"]); object.canRefuse = parameters["canRefuse"].Bool(); + object.showScoutedPreview = parameters["showScoutedPreview"].Bool(); if(parameters["showInInfobox"].isNull()) object.infoWindowType = EInfoWindowMode::AUTO; @@ -292,6 +399,10 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand break; } } + + if (object.visitMode == Rewardable::VISIT_LIMITER) + configureLimiter(object, rng, object.visitLimiter, parameters["visitLimiter"]); + } bool Rewardable::Info::givesResources() const diff --git a/lib/rewardable/Info.h b/lib/rewardable/Info.h index c9c826fae..5ad96ca40 100644 --- a/lib/rewardable/Info.h +++ b/lib/rewardable/Info.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CRandomGenerator; +class MetaString; namespace Rewardable { @@ -24,6 +25,8 @@ struct Limiter; using LimitersList = std::vector>; struct Reward; struct Configuration; +struct Variables; +struct VisitInfo; struct ResetInfo; enum class EEventType; @@ -32,7 +35,11 @@ class DLL_LINKAGE Info : public IObjectInfo JsonNode parameters; std::string objectTextID; - void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, std::map & thrownDice, Rewardable::EEventType mode, const std::string & textPrefix) const; + void replaceTextPlaceholders(MetaString & target, const Variables & variables) const; + void replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const; + + void configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const; + void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, Rewardable::EEventType mode, const std::string & textPrefix) const; void configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Limiter & limiter, const JsonNode & source) const; Rewardable::LimitersList configureSublimiters(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const; diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index 84cfdc8e3..18fd6cc9b 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -12,8 +12,11 @@ #include "Interface.h" #include "../CHeroHandler.h" +#include "../TerrainHandler.h" +#include "../CPlayerState.h" #include "../CSoundBase.h" #include "../NetPacks.h" +#include "../gameState/CGameState.h" #include "../spells/CSpellHandler.h" #include "../spells/ISpellMechanics.h" #include "../mapObjects/MiscObjects.h" @@ -46,6 +49,58 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R cb->giveResources(hero->tempOwner, info.reward.resources); + if (info.reward.revealTiles) + { + const auto & props = *info.reward.revealTiles; + + const auto functor = [&props](const TerrainTile * tile) + { + int score = 0; + if (tile->terType->isSurface()) + score += props.scoreSurface; + + if (tile->terType->isUnderground()) + score += props.scoreSubterra; + + if (tile->terType->isWater()) + score += props.scoreWater; + + if (tile->terType->isRock()) + score += props.scoreRock; + + return score > 0; + }; + + std::unordered_set tiles; + if (props.radius > 0) + { + cb->getTilesInRange(tiles, hero->getSightCenter(), props.radius, ETileVisibility::HIDDEN, hero->getOwner()); + if (props.hide) + cb->getTilesInRange(tiles, hero->getSightCenter(), props.radius, ETileVisibility::REVEALED, hero->getOwner()); + + vstd::erase_if(tiles, [&](const int3 & coord){ + return !functor(cb->getTile(coord)); + }); + } + else + { + cb->getAllTiles(tiles, hero->tempOwner, -1, functor); + } + + if (props.hide) + { + for (auto & player : cb->gameState()->players) + { + if (cb->getPlayerStatus(player.first) == EPlayerStatus::INGAME && cb->getPlayerRelations(player.first, hero->getOwner()) == PlayerRelations::ENEMIES) + cb->changeFogOfWar(tiles, player.first, ETileVisibility::HIDDEN); + } + } + else + { + cb->changeFogOfWar(tiles, hero->getOwner(), ETileVisibility::REVEALED); + } + } + for(const auto & entry : info.reward.secondary) { int current = hero->getSecSkillLevel(entry.first); diff --git a/lib/rewardable/Interface.h b/lib/rewardable/Interface.h index c675f82bc..ac85e6b72 100644 --- a/lib/rewardable/Interface.h +++ b/lib/rewardable/Interface.h @@ -10,8 +10,6 @@ #pragma once -#include "../CCreatureSet.h" -#include "../ResourceSet.h" #include "../spells/ExternalCaster.h" #include "Configuration.h" diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index b23c50610..d0db694fb 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -101,6 +101,9 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const if(manaPoints > hero->mana) return false; + if (canLearnSkills && !hero->canLearnSkill()) + return false; + if(manaPercentage > 100 * hero->mana / hero->manaLimit()) return false; @@ -122,6 +125,12 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } + for(const auto & spell : canLearnSpells) + { + if (!hero->canLearnSpell(spell.toSpell(VLC->spells()), true)) + return false; + } + { std::unordered_map artifactsRequirements; // artifact ID -> required count for(const auto & art : artifacts) diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 6fa52925e..407db0f24 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -44,6 +44,9 @@ struct DLL_LINKAGE Limiter final /// percentage of mana points that hero needs to have si32 manaPercentage; + /// Number of free secondary slots that hero needs to have + bool canLearnSkills; + /// resources player needs to have in order to trigger reward TResources resources; @@ -58,6 +61,9 @@ struct DLL_LINKAGE Limiter final /// Spells that hero must have in the spellbook std::vector spells; + /// Spells that hero must be able to learn + std::vector canLearnSpells; + /// creatures that hero needs to have std::vector creatures; @@ -94,10 +100,13 @@ struct DLL_LINKAGE Limiter final h & heroLevel; h & manaPoints; h & manaPercentage; + h & canLearnSkills; h & resources; h & primary; h & secondary; h & artifacts; + h & spells; + h & canLearnSpells; h & creatures; h & heroes; h & heroClasses; diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index bbdbf3cdc..5b965011b 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -18,6 +18,16 @@ VCMI_LIB_NAMESPACE_BEGIN +void Rewardable::RewardRevealTiles::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeBool("hide", hide); + handler.serializeInt("scoreSurface", scoreSurface); + handler.serializeInt("scoreSubterra", scoreSubterra); + handler.serializeInt("scoreWater", scoreWater); + handler.serializeInt("scoreRock", scoreRock); + handler.serializeInt("radius", radius); +} + Rewardable::Reward::Reward() : heroExperience(0) , heroLevel(0) @@ -56,8 +66,7 @@ Component Rewardable::Reward::getDisplayedComponent(const CGHeroInstance * h) co return comps.front(); } -void Rewardable::Reward::loadComponents(std::vector & comps, - const CGHeroInstance * h) const +void Rewardable::Reward::loadComponents(std::vector & comps, const CGHeroInstance * h) const { for (auto comp : extraComponents) comps.push_back(comp); @@ -71,14 +80,13 @@ void Rewardable::Reward::loadComponents(std::vector & comps, } if (heroExperience) - { - comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); - } + comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h ? h->calculateXp(heroExperience) : heroExperience), 0); + if (heroLevel) comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); if (manaDiff || manaPercentage >= 0) - comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, calculateManaPoints(h) - h->mana, 0); + comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, h ? (calculateManaPoints(h) - h->mana) : manaDiff, 0); for (size_t i=0; i>; +struct RewardRevealTiles +{ + /// Reveal distance, if not positive - reveal entire map + int radius; + /// Reveal score of terrains with "surface" flag set + int scoreSurface; + /// Reveal score of terrains with "subterra" flag set + int scoreSubterra; + /// Reveal score of terrains with "water" flag set + int scoreWater; + /// Reveal score of terrains with "rock" flag set + int scoreRock; + /// If set, then terrain will be instead hidden for all enemies (Cover of Darkness) + bool hide; + + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & radius; + h & scoreSurface; + h & scoreSubterra; + h & scoreWater; + h & scoreRock; + h & hide; + } +}; + /// Reward that can be granted to a hero /// NOTE: eventually should replace seer hut rewards and events/pandoras struct DLL_LINKAGE Reward final @@ -74,12 +102,14 @@ struct DLL_LINKAGE Reward final /// list of components that will be added to reward description. First entry in list will override displayed component std::vector extraComponents; + std::optional revealTiles; + /// if set to true, object will be removed after granting reward bool removeObject; /// Generates list of components that describes reward for a specific hero - void loadComponents(std::vector & comps, - const CGHeroInstance * h) const; + /// If hero is nullptr, then rewards will be generated without accounting for hero + void loadComponents(std::vector & comps, const CGHeroInstance * h) const; Component getDisplayedComponent(const CGHeroInstance * h) const; @@ -107,8 +137,8 @@ struct DLL_LINKAGE Reward final h & spells; h & creatures; h & creaturesChange; - if(version >= 821) - h & spellCast; + h & revealTiles; + h & spellCast; } void serializeJson(JsonSerializeFormat & handler); diff --git a/scripting/lua/api/netpacks/SetResources.cpp b/scripting/lua/api/netpacks/SetResources.cpp index 2d8b5c569..d71e2aa34 100644 --- a/scripting/lua/api/netpacks/SetResources.cpp +++ b/scripting/lua/api/netpacks/SetResources.cpp @@ -102,7 +102,7 @@ int SetResourcesProxy::getAmount(lua_State * L) if(!S.tryGet(1, object)) return S.retVoid(); - auto type = EGameResID::INVALID; + auto type = EGameResID::NONE; if(!S.tryGet(2, type)) return S.retVoid(); @@ -122,7 +122,7 @@ int SetResourcesProxy::setAmount(lua_State * L) if(!S.tryGet(1, object)) return S.retVoid(); - auto type = EGameResID::INVALID; + auto type = EGameResID::NONE; if(!S.tryGet(2, type)) return S.retVoid(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 88eeda6d0..fec358bf8 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -858,7 +858,7 @@ void CGameHandler::onNewTurn() if (player != PlayerColor::NEUTRAL) //do not reveal fow for neutral player { FoWChange fw; - fw.mode = 1; + fw.mode = ETileVisibility::REVEALED; fw.player = player; // find all hidden tiles const auto fow = getPlayerTeam(player)->fogOfWarMap; @@ -879,7 +879,7 @@ void CGameHandler::onNewTurn() { if (getPlayerStatus(player.first) == EPlayerStatus::INGAME && getPlayerRelations(player.first, t->tempOwner) == PlayerRelations::ENEMIES) - changeFogOfWar(t->visitablePos(), t->getBonusLocalFirst(Selector::type()(BonusType::DARKNESS))->val, player.first, true); + changeFogOfWar(t->visitablePos(), t->getBonusLocalFirst(Selector::type()(BonusType::DARKNESS))->val, player.first, ETileVisibility::HIDDEN); } } } @@ -1174,7 +1174,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo { obj->onHeroLeave(h); } - this->getTilesInRange(tmh.fowRevealed, h->getSightCenter()+(tmh.end-tmh.start), h->getSightRadius(), h->tempOwner, 1); + this->getTilesInRange(tmh.fowRevealed, h->getSightCenter()+(tmh.end-tmh.start), h->getSightRadius(), ETileVisibility::HIDDEN, h->tempOwner); }; auto doMove = [&](TryMoveHero::EResult result, EGuardLook lookForGuards, @@ -1523,7 +1523,7 @@ void CGameHandler::giveHero(ObjectInstanceID id, PlayerColor player, ObjectInsta //Reveal fow around new hero, especially released from Prison auto h = getHero(id); - changeFogOfWar(h->pos, h->getSightRadius(), player, false); + changeFogOfWar(h->pos, h->getSightRadius(), player, ETileVisibility::REVEALED); } void CGameHandler::changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) @@ -2387,11 +2387,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, processAfterBuiltStructure(builtID); // now when everything is built - reveal tiles for lookout tower - FoWChange fw; - fw.player = t->tempOwner; - fw.mode = 1; - getTilesInRange(fw.tiles, t->getSightCenter(), t->getSightRadius(), t->tempOwner, 1); - sendAndApply(&fw); + changeFogOfWar(t->getSightCenter(), t->getSightRadius(), t->getOwner(), ETileVisibility::REVEALED); if(t->visitingHero) visitCastleObjects(t, t->visitingHero); @@ -4061,10 +4057,7 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) void CGameHandler::synchronizeArtifactHandlerLists() { UpdateArtHandlerLists uahl; - uahl.treasures = VLC->arth->treasures; - uahl.minors = VLC->arth->minors; - uahl.majors = VLC->arth->majors; - uahl.relics = VLC->arth->relics; + uahl.allocatedArtifacts = VLC->arth->allocatedArtifacts; sendAndApply(&uahl); } @@ -4111,34 +4104,40 @@ void CGameHandler::removeAfterVisit(const CGObjectInstance *object) assert("This function needs to be called during the object visit!"); } -void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) +void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) { std::unordered_set tiles; - getTilesInRange(tiles, center, radius, player, hide? -1 : 1); - if (hide) + + if (mode == ETileVisibility::HIDDEN) { + getTilesInRange(tiles, center, radius, ETileVisibility::REVEALED, player); + std::unordered_set observedTiles; //do not hide tiles observed by heroes. May lead to disastrous AI problems auto p = getPlayerState(player); for (auto h : p->heroes) { - getTilesInRange(observedTiles, h->getSightCenter(), h->getSightRadius(), h->tempOwner, -1); + getTilesInRange(observedTiles, h->getSightCenter(), h->getSightRadius(), ETileVisibility::REVEALED, h->tempOwner); } for (auto t : p->towns) { - getTilesInRange(observedTiles, t->getSightCenter(), t->getSightRadius(), t->tempOwner, -1); + getTilesInRange(observedTiles, t->getSightCenter(), t->getSightRadius(), ETileVisibility::REVEALED, t->tempOwner); } for (auto tile : observedTiles) vstd::erase_if_present (tiles, tile); } - changeFogOfWar(tiles, player, hide); + else + { + getTilesInRange(tiles, center, radius, ETileVisibility::HIDDEN, player); + } + changeFogOfWar(tiles, player, mode); } -void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) +void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) { FoWChange fow; fow.tiles = tiles; fow.player = player; - fow.mode = hide? 0 : 1; + fow.mode = mode; sendAndApply(&fow); } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 2784aa6a7..582eb0c2b 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -148,8 +148,8 @@ public: void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override; void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override; - void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override; - void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) override; + void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override; + void changeFogOfWar(std::unordered_set &tiles, PlayerColor player,ETileVisibility mode) override; void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override; diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 7cadfda29..62b30a72e 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -346,7 +346,7 @@ void PlayerMessageProcessor::cheatDefeat(PlayerColor player) void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) { FoWChange fc; - fc.mode = reveal; + fc.mode = reveal ? ETileVisibility::REVEALED : ETileVisibility::HIDDEN; fc.player = player; const auto & fowMap = gameHandler->gameState()->getPlayerTeam(player)->fogOfWarMap; const auto & mapSize = gameHandler->gameState()->getMapSize(); @@ -356,7 +356,7 @@ void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) for(int z = 0; z < mapSize.z; z++) for(int x = 0; x < mapSize.x; x++) for(int y = 0; y < mapSize.y; y++) - if(!(*fowMap)[z][x][y] || !fc.mode) + if(!(*fowMap)[z][x][y] || fc.mode == ETileVisibility::HIDDEN) hlp_tab[lastUnc++] = int3(x, y, z); fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index 42dc8d3cc..f92c1690c 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -85,8 +85,8 @@ public: void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {} void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {} void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {} //when two heroes meet on adventure map - void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} - void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) override {} + void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {} + void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) override {} void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {} ///useful callback methods