diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 16ee57405..4e49b9c7a 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -267,6 +267,7 @@ jobs: env: HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }} if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }} + continue-on-error: true run: | ctest --preset ${{matrix.preset}} diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index c4675afc0..60b6b4b02 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -857,15 +857,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange( exchangeBattle->nextRound(); } - // avoid blocking path for stronger stack by weaker stack - // the method checks if all stacks can be placed around enemy - std::map reachabilityMap; - - auto hexes = ap.attack.defender->getSurroundingHexes(); - - for(auto hex : hexes) - reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex); - auto score = v.getScore(); if(simulationTurnsCount < totalTurnsCount) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index c8a7efb30..a10010cbb 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -285,8 +285,8 @@ void AIGateway::tileRevealed(const std::unordered_set & pos) addVisitableObj(obj); } - if (nullkiller->settings->isUpdateHitmapOnTileReveal()) - nullkiller->dangerHitMap->reset(); + if (nullkiller->settings->isUpdateHitmapOnTileReveal() && !pos.empty()) + nullkiller->dangerHitMap->resetTileOwners(); } void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) @@ -389,9 +389,10 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor & } if(obj->ID == Obj::HERO && cb->getPlayerRelations(obj->tempOwner, playerID) == PlayerRelations::ENEMIES) - { - nullkiller->dangerHitMap->reset(); - } + nullkiller->dangerHitMap->resetHitmap(); + + if(obj->ID == Obj::TOWN) + nullkiller->dangerHitMap->resetTileOwners(); } void AIGateway::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) @@ -507,7 +508,7 @@ void AIGateway::objectPropertyChanged(const SetObjectProperty * sop) else if(relations == PlayerRelations::SAME_PLAYER && obj->ID == Obj::TOWN) { // reevaluate defence for a new town - nullkiller->dangerHitMap->reset(); + nullkiller->dangerHitMap->resetHitmap(); } } } @@ -1246,7 +1247,7 @@ void AIGateway::addVisitableObj(const CGObjectInstance * obj) if(obj->ID == Obj::HERO && cb->getPlayerRelations(obj->tempOwner, playerID) == PlayerRelations::ENEMIES) { - nullkiller->dangerHitMap->reset(); + nullkiller->dangerHitMap->resetHitmap(); } } diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 6a1c0c070..f22c46c0d 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -11,49 +11,42 @@ #include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h" #include "../../../lib/entities/building/CBuilding.h" +#include "../../../lib/IGameSettings.h" namespace NKAI { void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) { - std::map parentMap; - - for(auto &pair : developmentInfo.town->getTown()->buildings) - { - if(pair.second->upgrade != BuildingID::NONE) - { - parentMap[pair.second->upgrade] = pair.first; - } - } - for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++) { logAi->trace("Checking dwelling level %d", level); - BuildingInfo nextToBuild = BuildingInfo(); + std::vector dwellingsInTown; - BuildingID buildID = BuildingID(BuildingID::getDwellingFromLevel(level, 0)); + for(BuildingID buildID = BuildingID::getDwellingFromLevel(level, 0); buildID.hasValue(); BuildingID::advanceDwelling(buildID)) + if(developmentInfo.town->getTown()->buildings.count(buildID) != 0) + dwellingsInTown.push_back(buildID); - for(; developmentInfo.town->getBuildings().count(buildID); BuildingID::advanceDwelling(buildID)) + // find best, already built dwelling + for (const auto & buildID : boost::adaptors::reverse(dwellingsInTown)) { - if(!developmentInfo.town->hasBuilt(buildID)) - continue; // no such building in town + if (!developmentInfo.town->hasBuilt(buildID)) + continue; - auto info = getBuildingOrPrerequisite(developmentInfo.town, buildID); - - if(info.exists) - { - developmentInfo.addExistingDwelling(info); - - break; - } - - nextToBuild = info; + const auto & info = getBuildingOrPrerequisite(developmentInfo.town, buildID); + developmentInfo.addExistingDwelling(info); + break; } - if(nextToBuild.id != BuildingID::NONE) + // find all non-built dwellings that can be built and add them for consideration + for (const auto & buildID : dwellingsInTown) { - developmentInfo.addBuildingToBuild(nextToBuild); + if (developmentInfo.town->hasBuilt(buildID)) + continue; + + const auto & info = getBuildingOrPrerequisite(developmentInfo.town, buildID); + if (info.canBuild || info.notEnoughRes) + developmentInfo.addBuildingToBuild(info); } } } @@ -148,6 +141,9 @@ void BuildAnalyzer::update() for(const CGTownInstance* town : towns) { + if(town->built >= cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)) + continue; // Not much point in trying anything - can't built in this town anymore today + logAi->trace("Checking town %s", town->getNameTranslated()); developmentInfos.push_back(TownDevelopmentInfo(town)); @@ -272,11 +268,11 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( if(vstd::contains_if(missingBuildings, otherDwelling)) { - logAi->trace("cant build. Need other dwelling"); + logAi->trace("cant build %d. Need other dwelling %d", toBuild.getNum(), missingBuildings.front().getNum()); } else if(missingBuildings[0] != toBuild) { - logAi->trace("cant build. Need %d", missingBuildings[0].num); + logAi->trace("cant build %d. Need %d", toBuild.getNum(), missingBuildings[0].num); BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], excludeDwellingDependencies); @@ -307,10 +303,14 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( return info; } } + else + { + logAi->trace("Cant build. Reason: %d", static_cast(canBuild)); + } } else { - logAi->trace("exists"); + logAi->trace("Dwelling %d exists", toBuild.getNum()); info.exists = true; } diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 5a8d0a24f..750bfb772 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -119,7 +119,7 @@ void DangerHitMapAnalyzer::updateHitMap() PathfinderSettings ps; - ps.scoutTurnDistanceLimit = ps.mainTurnDistanceLimit = ai->settings->getMainHeroTurnDistanceLimit(); + ps.scoutTurnDistanceLimit = ps.mainTurnDistanceLimit = ai->settings->getThreatTurnDistanceLimit(); ps.useHeroChain = false; ai->pathfinder->updatePaths(pair.second, ps); @@ -345,10 +345,4 @@ std::set DangerHitMapAnalyzer::getOneTurnAccessibleObj return result; } -void DangerHitMapAnalyzer::reset() -{ - hitMapUpToDate = false; - tileOwnersUpToDate = false; -} - } diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 2bd39a2d8..7e9a66c8a 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -87,7 +87,7 @@ public: const HitMapNode & getObjectThreat(const CGObjectInstance * obj) const; const HitMapNode & getTileThreat(const int3 & tile) const; std::set getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; - void reset(); + void resetHitmap() {hitMapUpToDate = false;} void resetTileOwners() { tileOwnersUpToDate = false; } PlayerColor getTileOwner(const int3 & tile) const; const CGTownInstance * getClosestTown(const int3 & tile) const; diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index 50f800913..8642765fa 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -59,17 +59,19 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const { closestThreat = std::min(closestThreat, threat.turn); } - for (auto& buildingInfo : developmentInfo.toBuild) + + if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE) { - if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes) + for (auto& buildingInfo : developmentInfo.toBuild) { - if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE) + if ( !buildingInfo.notEnoughRes && (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE)) { tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); emergencyDefense = true; } } } + if (!emergencyDefense) { for (auto& buildingInfo : developmentInfo.toBuild) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 88ee4f1dc..99decafd4 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -229,7 +229,7 @@ void Nullkiller::resetAiState() lockedResources = TResources(); scanDepth = ScanDepth::MAIN_FULL; lockedHeroes.clear(); - dangerHitMap->reset(); + dangerHitMap->resetHitmap(); useHeroChain = true; objectClusterizer->reset(); diff --git a/AI/Nullkiller/Engine/Settings.cpp b/AI/Nullkiller/Engine/Settings.cpp index 44ef27176..11357f9c1 100644 --- a/AI/Nullkiller/Engine/Settings.cpp +++ b/AI/Nullkiller/Engine/Settings.cpp @@ -28,6 +28,7 @@ namespace NKAI : maxRoamingHeroes(8), mainHeroTurnDistanceLimit(10), scoutHeroTurnDistanceLimit(5), + threatTurnDistanceLimit(5), maxGoldPressure(0.3f), retreatThresholdRelative(0.3), retreatThresholdAbsolute(10000), diff --git a/AI/Nullkiller/Engine/Settings.h b/AI/Nullkiller/Engine/Settings.h index 01f4f2a9c..ff2d1b859 100644 --- a/AI/Nullkiller/Engine/Settings.h +++ b/AI/Nullkiller/Engine/Settings.h @@ -24,6 +24,7 @@ namespace NKAI int maxRoamingHeroes; int mainHeroTurnDistanceLimit; int scoutHeroTurnDistanceLimit; + int threatTurnDistanceLimit; int maxPass; int maxPriorityPass; int pathfinderBucketsCount; @@ -52,6 +53,7 @@ namespace NKAI int getMaxRoamingHeroes() const { return maxRoamingHeroes; } int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; } int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; } + int getThreatTurnDistanceLimit() const { return threatTurnDistanceLimit; } int getPathfinderBucketsCount() const { return pathfinderBucketsCount; } int getPathfinderBucketSize() const { return pathfinderBucketSize; } bool isObjectGraphAllowed() const { return allowObjectGraph; } diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index eb2a53827..7d497b681 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -11,7 +11,7 @@ #pragma once #define NKAI_PATHFINDER_TRACE_LEVEL 0 -constexpr int NKAI_GRAPH_TRACE_LEVEL = 0; +constexpr int NKAI_GRAPH_TRACE_LEVEL = 0; // To actually enable graph visualization, enter `/vslog graph` in game chat #define NKAI_TRACE_LEVEL 0 #include "../../../lib/pathfinder/CGPathNode.h" diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index 8db9230cc..489f830ac 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/AI/Nullkiller/Pathfinding/Actors.cpp @@ -47,11 +47,10 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t initialTurn = 0; armyValue = getHeroArmyStrengthWithCommander(hero, hero); heroFightingStrength = hero->getHeroStrength(); - tiCache.reset(new TurnInfo(hero)); } ChainActor::ChainActor(const ChainActor * carrier, const ChainActor * other, const CCreatureSet * heroArmy) - :hero(carrier->hero), tiCache(carrier->tiCache), heroRole(carrier->heroRole), isMovable(true), creatureSet(heroArmy), chainMask(carrier->chainMask | other->chainMask), + :hero(carrier->hero), heroRole(carrier->heroRole), isMovable(true), creatureSet(heroArmy), chainMask(carrier->chainMask | other->chainMask), baseActor(this), carrierParent(carrier), otherParent(other), heroFightingStrength(carrier->heroFightingStrength), actorExchangeCount(carrier->actorExchangeCount + other->actorExchangeCount), armyCost(carrier->armyCost + other->armyCost), actorAction() { @@ -75,7 +74,7 @@ int ChainActor::maxMovePoints(CGPathNode::ELayer layer) throw std::logic_error("Asking movement points for static actor"); #endif - return hero->movementPointsLimitCached(layer, tiCache.get()); + return hero->movementPointsLimit(layer); } std::string ChainActor::toString() const @@ -133,7 +132,6 @@ void ChainActor::setBaseActor(HeroActor * base) heroFightingStrength = base->heroFightingStrength; armyCost = base->armyCost; actorAction = base->actorAction; - tiCache = base->tiCache; actorExchangeCount = base->actorExchangeCount; } diff --git a/AI/Nullkiller/Pathfinding/Actors.h b/AI/Nullkiller/Pathfinding/Actors.h index 1f653fbd3..8caf02e03 100644 --- a/AI/Nullkiller/Pathfinding/Actors.h +++ b/AI/Nullkiller/Pathfinding/Actors.h @@ -73,7 +73,6 @@ public: float heroFightingStrength; uint8_t actorExchangeCount; TResources armyCost; - std::shared_ptr tiCache; ChainActor() = default; virtual ~ChainActor() = default; diff --git a/AI/Nullkiller/Pathfinding/GraphPaths.cpp b/AI/Nullkiller/Pathfinding/GraphPaths.cpp index 136e8491c..e8d68c79b 100644 --- a/AI/Nullkiller/Pathfinding/GraphPaths.cpp +++ b/AI/Nullkiller/Pathfinding/GraphPaths.cpp @@ -367,6 +367,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector & paths, int3 // final node n.coord = tile; n.cost = targetNode.cost; + n.turns = static_cast(targetNode.cost); n.danger = danger; n.parentIndex = path.nodes.size(); path.nodes.push_back(n); diff --git a/ChangeLog.md b/ChangeLog.md index d1ee43cc2..cb8852c30 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,36 @@ # VCMI Project Changelog +## 1.6.2 -> 1.6.3 + +### Stability + +* Fixed possible crash on attempt to play corrupted video file +* Fixed possible crash on invalid or corrupted game data +* Fixed possible crash on invalid upscaling filter + +### Interface + +* Added right-click popup to Keymasters, Border Guards, and Border Gates that show all discovered objects of the same color +* Added right-click popup to Obelisks that shows all discovered objects and their visitation status +* Added support for randomly selected main menu backgrounds +* Fixed display of long text in text-only right-click popups +* Hero overview screen on map setup will now have scrollbars for hero desription when necessary +* Fixed teleporter right-click popup appearing out of screen when interacting with teleporter near screen edge +* Scenario Information window will now correctly replace random hero with actual starting hero + +### AI + +* Improved performance of Battle AI +* Improved performance of pathfinding calculations +* Reduced calculation of threat range, especially on low difficulties to improve performance +* Fixed Nullkiller AI not considering nearby objects for visiting in some cases, breaking its exploration logic +* Fixed Nullkiller AI not building town dwellings + +### Map Editor + +* Added option to convert .h3c files into .vcmp +* It is now possible to configure town to have same faction as player + ## 1.6.1 -> 1.6.2 ### General diff --git a/Global.h b/Global.h index f43dfa851..2856abdcc 100644 --- a/Global.h +++ b/Global.h @@ -369,6 +369,15 @@ namespace vstd return it->second; } + // given a map from keys to values, creates a new map from values to keys + template + static std::map reverseMap(const std::map& m) { + std::map r; + for (const auto& kv : m) + r[kv.second] = kv.first; + return r; + } + //returns first key that maps to given value if present, returns success via found if provided template Key findKey(const std::map & map, const T & value, bool * found = nullptr) diff --git a/Mods/vcmi/Content/Sprites/minimapIcons/bordergate.png b/Mods/vcmi/Content/Sprites/minimapIcons/bordergate.png new file mode 100644 index 000000000..511fab632 Binary files /dev/null and b/Mods/vcmi/Content/Sprites/minimapIcons/bordergate.png differ diff --git a/Mods/vcmi/Content/Sprites/minimapIcons/borderguard.png b/Mods/vcmi/Content/Sprites/minimapIcons/borderguard.png new file mode 100644 index 000000000..6d9d21c4c Binary files /dev/null and b/Mods/vcmi/Content/Sprites/minimapIcons/borderguard.png differ diff --git a/Mods/vcmi/Content/Sprites/minimapIcons/keymaster.png b/Mods/vcmi/Content/Sprites/minimapIcons/keymaster.png new file mode 100644 index 000000000..3b63f038a Binary files /dev/null and b/Mods/vcmi/Content/Sprites/minimapIcons/keymaster.png differ diff --git a/Mods/vcmi/Content/Sprites/minimapIcons/obelisk.png b/Mods/vcmi/Content/Sprites/minimapIcons/obelisk.png new file mode 100644 index 000000000..ebc6f7cfd Binary files /dev/null and b/Mods/vcmi/Content/Sprites/minimapIcons/obelisk.png differ diff --git a/Mods/vcmi/Content/Sprites/minimapIcons/obeliskVisited.png b/Mods/vcmi/Content/Sprites/minimapIcons/obeliskVisited.png new file mode 100644 index 000000000..c887afe00 Binary files /dev/null and b/Mods/vcmi/Content/Sprites/minimapIcons/obeliskVisited.png differ diff --git a/Mods/vcmi/Content/Sprites/portalBidirectional.png b/Mods/vcmi/Content/Sprites/minimapIcons/portalBidirectional.png similarity index 100% rename from Mods/vcmi/Content/Sprites/portalBidirectional.png rename to Mods/vcmi/Content/Sprites/minimapIcons/portalBidirectional.png diff --git a/Mods/vcmi/Content/Sprites/portalEntrance.png b/Mods/vcmi/Content/Sprites/minimapIcons/portalEntrance.png similarity index 100% rename from Mods/vcmi/Content/Sprites/portalEntrance.png rename to Mods/vcmi/Content/Sprites/minimapIcons/portalEntrance.png diff --git a/Mods/vcmi/Content/Sprites/portalExit.png b/Mods/vcmi/Content/Sprites/minimapIcons/portalExit.png similarity index 100% rename from Mods/vcmi/Content/Sprites/portalExit.png rename to Mods/vcmi/Content/Sprites/minimapIcons/portalExit.png diff --git a/Mods/vcmi/Content/Sprites2x/minimapIcons/bordergate.png b/Mods/vcmi/Content/Sprites2x/minimapIcons/bordergate.png new file mode 100644 index 000000000..ef848cad6 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/minimapIcons/bordergate.png differ diff --git a/Mods/vcmi/Content/Sprites2x/minimapIcons/borderguard.png b/Mods/vcmi/Content/Sprites2x/minimapIcons/borderguard.png new file mode 100644 index 000000000..8e51c62ae Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/minimapIcons/borderguard.png differ diff --git a/Mods/vcmi/Content/Sprites2x/minimapIcons/keymaster.png b/Mods/vcmi/Content/Sprites2x/minimapIcons/keymaster.png new file mode 100644 index 000000000..80541a5cb Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/minimapIcons/keymaster.png differ diff --git a/Mods/vcmi/Content/Sprites2x/minimapIcons/obelisk.png b/Mods/vcmi/Content/Sprites2x/minimapIcons/obelisk.png new file mode 100644 index 000000000..db8649f96 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/minimapIcons/obelisk.png differ diff --git a/Mods/vcmi/Content/Sprites2x/minimapIcons/obeliskVisited.png b/Mods/vcmi/Content/Sprites2x/minimapIcons/obeliskVisited.png new file mode 100644 index 000000000..7811ae038 Binary files /dev/null and b/Mods/vcmi/Content/Sprites2x/minimapIcons/obeliskVisited.png differ diff --git a/Mods/vcmi/Content/Sprites2x/portalBidirectional.png b/Mods/vcmi/Content/Sprites2x/minimapIcons/portalBidirectional.png similarity index 100% rename from Mods/vcmi/Content/Sprites2x/portalBidirectional.png rename to Mods/vcmi/Content/Sprites2x/minimapIcons/portalBidirectional.png diff --git a/Mods/vcmi/Content/Sprites2x/portalEntrance.png b/Mods/vcmi/Content/Sprites2x/minimapIcons/portalEntrance.png similarity index 100% rename from Mods/vcmi/Content/Sprites2x/portalEntrance.png rename to Mods/vcmi/Content/Sprites2x/minimapIcons/portalEntrance.png diff --git a/Mods/vcmi/Content/Sprites2x/portalExit.png b/Mods/vcmi/Content/Sprites2x/minimapIcons/portalExit.png similarity index 100% rename from Mods/vcmi/Content/Sprites2x/portalExit.png rename to Mods/vcmi/Content/Sprites2x/minimapIcons/portalExit.png diff --git a/Mods/vcmi/Content/config/vietnamese.json b/Mods/vcmi/Content/config/vietnamese.json index 9b08b5e2b..f9f785ef0 100644 --- a/Mods/vcmi/Content/config/vietnamese.json +++ b/Mods/vcmi/Content/config/vietnamese.json @@ -1,362 +1,787 @@ { - "vcmi.adventureMap.monsterThreat.title": "\n\nMức độ: ", - "vcmi.adventureMap.monsterThreat.levels.0": "Nhẹ nhàng", - "vcmi.adventureMap.monsterThreat.levels.1": "Rất yếu", - "vcmi.adventureMap.monsterThreat.levels.2": "Yếu", - "vcmi.adventureMap.monsterThreat.levels.3": "Yếu hơn", - "vcmi.adventureMap.monsterThreat.levels.4": "Ngang bằng", - "vcmi.adventureMap.monsterThreat.levels.5": "Nhỉnh hơn", - "vcmi.adventureMap.monsterThreat.levels.6": "Mạnh", - "vcmi.adventureMap.monsterThreat.levels.7": "Rất mạnh", - "vcmi.adventureMap.monsterThreat.levels.8": "Thách thức", - "vcmi.adventureMap.monsterThreat.levels.9": "Áp đảo", - "vcmi.adventureMap.monsterThreat.levels.10": "Chết chóc", - "vcmi.adventureMap.monsterThreat.levels.11": "Bất khả diệt", + "vcmi.adventureMap.monsterThreat.title": "\n\nMức độ: ", + "vcmi.adventureMap.monsterThreat.levels.0": "Nhẹ nhàng", + "vcmi.adventureMap.monsterThreat.levels.1": "Rất yếu", + "vcmi.adventureMap.monsterThreat.levels.2": "Yếu", + "vcmi.adventureMap.monsterThreat.levels.3": "Yếu hơn", + "vcmi.adventureMap.monsterThreat.levels.4": "Ngang bằng", + "vcmi.adventureMap.monsterThreat.levels.5": "Nhỉnh hơn", + "vcmi.adventureMap.monsterThreat.levels.6": "Mạnh", + "vcmi.adventureMap.monsterThreat.levels.7": "Rất mạnh", + "vcmi.adventureMap.monsterThreat.levels.8": "Thách thức", + "vcmi.adventureMap.monsterThreat.levels.9": "Áp đảo", + "vcmi.adventureMap.monsterThreat.levels.10": "Chết chóc", + "vcmi.adventureMap.monsterThreat.levels.11": "Bất khả diệt", + "vcmi.adventureMap.monsterLevel": "\n\nCấp %LEVEL %TOWN %ATTACK_TYPE đơn vị", + "vcmi.adventureMap.monsterMeleeType": "Cận chiến", + "vcmi.adventureMap.monsterRangedType": "Bắn xa", + "vcmi.adventureMap.search.hover": "Tìm đối tượng trên bản đồ.", + "vcmi.adventureMap.search.help": "Chọn đối tượng để tìm kiếm.", - "vcmi.adventureMap.confirmRestartGame": "Bạn muốn chơi lại?", - "vcmi.adventureMap.noTownWithMarket": "Chợ không có sẵn!", - "vcmi.adventureMap.noTownWithTavern": "Thành không có sẵn quán rượu!", - "vcmi.adventureMap.spellUnknownProblem": "Phép này có lỗi! Không có thông tin nào khác.", - "vcmi.adventureMap.playerAttacked": "Người chơi bị tấn công: %s", - "vcmi.adventureMap.moveCostDetails": "Điểm di chuyển - Cần: %TURNS lượt + %POINTS điểm, Còn lại: %REMAINING", - "vcmi.adventureMap.moveCostDetailsNoTurns": "Điểm di chuyển - Cần: %POINTS điểm, Còn lại: %REMAINING", + "vcmi.adventureMap.confirmRestartGame": "Bạn muốn chơi lại?", + "vcmi.adventureMap.noTownWithMarket": "Chợ không có sẵn!", + "vcmi.adventureMap.noTownWithTavern": "Thành không có sẵn quán rượu!", + "vcmi.adventureMap.spellUnknownProblem": "Phép này có lỗi! Không có thông tin nào khác.", + "vcmi.adventureMap.playerAttacked": "Người chơi bị tấn công: %s", + "vcmi.adventureMap.moveCostDetails": "Điểm di chuyển - Cần: %TURNS lượt + %POINTS điểm, Còn lại: %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns": "Điểm di chuyển - Cần: %POINTS điểm, Còn lại: %REMAINING", + "vcmi.adventureMap.movementPointsHeroInfo": "(Điểm di chuyển: %REMAINING / %POINTS)", + "vcmi.adventureMap.replayOpponentTurnNotImplemented": "Xin lỗi, lượt chơi lại của đối thủ vẫn chưa được triển khai!", - "vcmi.capitalColors.0": "Đỏ", - "vcmi.capitalColors.1": "Xanh dương", - "vcmi.capitalColors.2": "Nâu", - "vcmi.capitalColors.3": "Xanh lá", - "vcmi.capitalColors.4": "Cam", - "vcmi.capitalColors.5": "Tím", - "vcmi.capitalColors.6": "Xanh đậm", - "vcmi.capitalColors.7": "Hồng", + "vcmi.bonusSource.artifact": "Báu vật", + "vcmi.bonusSource.creature": "Kỹ năng", + "vcmi.bonusSource.spell": "Phép thuật", + "vcmi.bonusSource.hero": "Tướng", + "vcmi.bonusSource.commander": "Chỉ huy", + "vcmi.bonusSource.other": "Khác", - "vcmi.heroOverview.startingArmy": "Lính ban đầu", - "vcmi.heroOverview.warMachine": "Chiến cơ", - "vcmi.heroOverview.secondarySkills": "Kĩ năng", - "vcmi.heroOverview.spells": "Phép", + "vcmi.capitalColors.0": "Đỏ", + "vcmi.capitalColors.1": "Xanh dương", + "vcmi.capitalColors.2": "Nâu", + "vcmi.capitalColors.3": "Xanh lá", + "vcmi.capitalColors.4": "Cam", + "vcmi.capitalColors.5": "Tím", + "vcmi.capitalColors.6": "Xanh đậm", + "vcmi.capitalColors.7": "Hồng", + + "vcmi.heroOverview.startingArmy": "Quân ban đầu", + "vcmi.heroOverview.warMachine": "Cỗ máy chiến đấu", + "vcmi.heroOverview.secondarySkills": "Kỹ năng phụ", + "vcmi.heroOverview.spells": "Phép thuật", + + "vcmi.quickExchange.moveUnit" : "Di chuyển đơn vị", + "vcmi.quickExchange.moveAllUnits" : "Di chuyển tất cả đơn vị", + "vcmi.quickExchange.swapAllUnits" : "Trao đổi quân", + "vcmi.quickExchange.moveAllArtifacts" : "Di chuyển tất cả báu vật", + "vcmi.quickExchange.swapAllArtifacts" : "Trao đổi báu vật", + + "vcmi.radialWheel.mergeSameUnit" : "Gộp quân cùng loại", + "vcmi.radialWheel.fillSingleUnit": "Làm đầy với từng loài", + "vcmi.radialWheel.splitSingleUnit": "Tách một quân cùng loài", + "vcmi.radialWheel.splitUnitEqually": "Chia quân bằng nhau", + "vcmi.radialWheel.moveUnit": "Di chuyển quân đến đội khác", + "vcmi.radialWheel.splitUnit": "Chia quân đến ô khác", + + "vcmi.radialWheel.heroGetArmy" : "Lấy quân từ tướng khác", + "vcmi.radialWheel.heroSwapArmy" : "Trao đổi quân với tướng khác", + "vcmi.radialWheel.heroExchange" : "Mở màn hình trao đổi tướng", + "vcmi.radialWheel.heroGetArtifacts" : "Lấy báu vật từ tướng khác", + "vcmi.radialWheel.heroSwapArtifacts" : "Trao đổi báu vật với tướng khác", + "vcmi.radialWheel.heroDismiss" : "Loại bỏ tướng", - "vcmi.radialWheel.mergeSameUnit": "Sáp nhập cùng loài", - "vcmi.radialWheel.fillSingleUnit": "Làm đầy với từng loài", - "vcmi.radialWheel.splitSingleUnit": "Tách 1 loài", - "vcmi.radialWheel.splitUnitEqually": "Chia quái bằng nhau", - "vcmi.radialWheel.moveUnit": "Di chuyển quái đến đội khác", - "vcmi.radialWheel.splitUnit": "Chia quái đến ô khác", + "vcmi.radialWheel.moveTop" : "Di chuyển lên trên cùng", + "vcmi.radialWheel.moveUp" : "Di chuyển lên trên", + "vcmi.radialWheel.moveDown" : "Di chuyển xuống dưới", + "vcmi.radialWheel.moveBottom" : "Di chuyển xuống dưới cùng", + + "vcmi.randomMap.description" : "Bản đồ được tạo ngẫu nhiên.\nMẫu là %s, kích cỡ %dx%d, cấp %d, người chơi %d, máy %d, nước %s, quái vật %s, bản đồ VCMI", + "vcmi.randomMap.description.isHuman" : ", người chơi %s", + "vcmi.randomMap.description.townChoice" : ", %s town choice is %s", + "vcmi.randomMap.description.water.none" : "không", + "vcmi.randomMap.description.water.normal" : "bình thường", + "vcmi.randomMap.description.water.islands" : "đảo", + "vcmi.randomMap.description.monster.weak" : "yếu", + "vcmi.randomMap.description.monster.normal" : "bình thường", + "vcmi.randomMap.description.monster.strong" : "khỏe", - "vcmi.mainMenu.highscoresNotImplemented": "Xin lỗi, bảng xếp hạng chưa được làm đầy đủ\n", - "vcmi.mainMenu.serverConnecting": "Đang kết nối...", - "vcmi.mainMenu.serverAddressEnter": "Nhập địa chỉ:", - "vcmi.mainMenu.serverClosing": "Đang hủy kết nối...", - "vcmi.mainMenu.hostTCP": "Chủ phòng TCP/IP", - "vcmi.mainMenu.joinTCP": "Tham gia TCP/IP", - "vcmi.mainMenu.playerName": "Người chơi", + "vcmi.spellBook.search" : "tìm kiếm...", - "vcmi.lobby.filepath": "Tên tập tin", - "vcmi.lobby.creationDate": "Ngày tạo", + "vcmi.spellResearch.canNotAfford" : "Bạn không đủ khả năng để thay thế {%SPELL1} với {%SPELL2}. Nhưng bạn có thể loại bỏ phép thuật này và nghiên cứu phép thuật khác.", + "vcmi.spellResearch.comeAgain" : "Hôm nay bạn đã nghiên cứu phép thuật. Hãy quay lại đây vào ngày mai.", + "vcmi.spellResearch.pay" : "Bạn có muốn thay thế phép {%SPELL1} bằng {%SPELL2} không? Hoặc loại bỏ phép thuật này và nghiên cứu phép thuật khác?", + "vcmi.spellResearch.research" : "Nghiên cứu phép thuật này", + "vcmi.spellResearch.skip" : "Loại bỏ phép thuật này", + "vcmi.spellResearch.abort" : "Hủy bỏ", + "vcmi.spellResearch.noMoreSpells" : "Không còn phép thuật để nghiên cứu.", - "vcmi.server.errors.existingProcess": "1 chương trình VCMI khác đang chạy. Tắt nó trước khi mở cái mới", - "vcmi.server.errors.modsIncompatibility": "Các bản sửa đổi cần để tải trò chơi:", - "vcmi.server.confirmReconnect": "Bạn có muốn kết nối lại phiên trước?", + "vcmi.mainMenu.serverConnecting" : "Đang kết nối...", + "vcmi.mainMenu.serverAddressEnter" : "Nhập địa chỉ:", + "vcmi.mainMenu.serverConnectionFailed" : "Không thể kết nối", + "vcmi.mainMenu.serverClosing" : "Đang hủy kết nối...", + "vcmi.mainMenu.hostTCP" : "Chủ phòng TCP/IP", + "vcmi.mainMenu.joinTCP" : "Tham gia TCP/IP", - "vcmi.settingsMainWindow.generalTab.hover": "Chung", - "vcmi.settingsMainWindow.generalTab.help": "Chuyển sang bảng Chung, chứa các cài đặt liên quan đến phần chung trò chơi", - "vcmi.settingsMainWindow.battleTab.hover": "Chiến đấu", - "vcmi.settingsMainWindow.battleTab.help": "Chuyển sang bảng Chiến đấu, cho phép thiết lập hành vi trong trận đánh", - "vcmi.settingsMainWindow.adventureTab.hover": "Phiêu lưu", - "vcmi.settingsMainWindow.adventureTab.help": "Chuyển sang bảng Phiêu lưu (bản đồ phiêu lưu là nơi mà người chơi di chuyển tướng của họ)", + "vcmi.lobby.filepath" : "File path", + "vcmi.lobby.creationDate" : "Creation date", + "vcmi.lobby.scenarioName" : "Scenario name", + "vcmi.lobby.mapPreview" : "Map preview", + "vcmi.lobby.noPreview" : "no preview", + "vcmi.lobby.noUnderground" : "no underground", + "vcmi.lobby.sortDate" : "Sorts maps by change date", + "vcmi.lobby.backToLobby" : "Return to lobby", + "vcmi.lobby.author" : "Author", + "vcmi.lobby.handicap" : "Handicap", + "vcmi.lobby.handicap.resource" : "Gives players appropriate resources to start with in addition to the normal starting resources. Negative values are allowed, but are limited to 0 in total (the player never starts with negative resources).", + "vcmi.lobby.handicap.income" : "Changes the player's various incomes by the percentage. Is rounded up.", + "vcmi.lobby.handicap.growth" : "Changes the growth rate of creatures in the towns owned by the player. Is rounded up.", + "vcmi.lobby.deleteUnsupportedSave" : "{Unsupported saves found}\n\nVCMI has found %d saved games that are no longer supported, possibly due to differences in VCMI versions.\n\nDo you want to delete them?", + "vcmi.lobby.deleteSaveGameTitle" : "Select a Saved Game to delete", + "vcmi.lobby.deleteMapTitle" : "Select a Scenario to delete", + "vcmi.lobby.deleteFile" : "Do you want to delete following file?", + "vcmi.lobby.deleteFolder" : "Do you want to delete following folder?", + "vcmi.lobby.deleteMode" : "Switch to delete mode and back", - "vcmi.systemOptions.videoGroup": "Thiết lập phim ảnh", - "vcmi.systemOptions.audioGroup": "Thiết lập âm thanh", - "vcmi.systemOptions.otherGroup": "Thiết lập khác", - "vcmi.systemOptions.townsGroup": "Thành phố", + "vcmi.broadcast.failedLoadGame" : "Failed to load game", + "vcmi.broadcast.command" : "Use '!help' to list available commands", + "vcmi.broadcast.simturn.end" : "Simultaneous turns have ended", + "vcmi.broadcast.simturn.endBetween" : "Simultaneous turns between players %s and %s have ended", + "vcmi.broadcast.serverProblem" : "Server encountered a problem", + "vcmi.broadcast.gameTerminated" : "game was terminated", + "vcmi.broadcast.gameSavedAs" : "game saved as", + "vcmi.broadcast.noCheater" : "No cheaters registered!", + "vcmi.broadcast.playerCheater" : "Player %s is cheater!", + "vcmi.broadcast.statisticFile" : "Statistic files can be found in %s directory", + "vcmi.broadcast.help.commands" : "Available commands to host:", + "vcmi.broadcast.help.exit" : "'!exit' - immediately ends current game", + "vcmi.broadcast.help.kick" : "'!kick ' - kick specified player from the game", + "vcmi.broadcast.help.save" : "'!save ' - save game under specified filename", + "vcmi.broadcast.help.statistic" : "'!statistic' - save game statistics as csv file", + "vcmi.broadcast.help.commandsAll" : "Available commands to all players:", + "vcmi.broadcast.help.help" : "'!help' - display this help", + "vcmi.broadcast.help.cheaters" : "'!cheaters' - list players that entered cheat command during game", + "vcmi.broadcast.help.vote" : "'!vote' - allows to change some game settings if all players vote for it", + "vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - allow simultaneous turns for specified number of days, or until contact", + "vcmi.broadcast.vote.force" : "'!vote simturns force X' - force simultaneous turns for specified number of days, blocking player contacts", + "vcmi.broadcast.vote.abort" : "'!vote simturns abort' - abort simultaneous turns once this turn ends", + "vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolong base timer for all players by specified number of seconds", + "vcmi.broadcast.vote.noActive" : "No active voting!", + "vcmi.broadcast.vote.yes" : "yes", + "vcmi.broadcast.vote.no" : "no", + "vcmi.broadcast.vote.notRecognized" : "Voting command not recognized!", + "vcmi.broadcast.vote.success.untilContacts" : "Voting successful. Simultaneous turns will run for %s more days, or until contact", + "vcmi.broadcast.vote.success.contactsBlocked" : "Voting successful. Simultaneous turns will run for %s more days. Contacts are blocked", + "vcmi.broadcast.vote.success.nextDay" : "Voting successful. Simultaneous turns will end on next day", + "vcmi.broadcast.vote.success.timer" : "Voting successful. Timer for all players has been prolonger for %s seconds", + "vcmi.broadcast.vote.aborted" : "Player voted against change. Voting aborted", + "vcmi.broadcast.vote.start.untilContacts" : "Started voting to allow simultaneous turns for %s more days", + "vcmi.broadcast.vote.start.contactsBlocked" : "Started voting to force simultaneous turns for %s more days", + "vcmi.broadcast.vote.start.nextDay" : "Started voting to end simultaneous turns starting from next day", + "vcmi.broadcast.vote.start.timer" : "Started voting to prolong timer for all players by %s seconds", + "vcmi.broadcast.vote.hint" : "Type '!vote yes' to agree to this change or '!vote no' to vote against it", + + "vcmi.lobby.login.title" : "VCMI Online Lobby", + "vcmi.lobby.login.username" : "Username:", + "vcmi.lobby.login.connecting" : "Connecting...", + "vcmi.lobby.login.error" : "Connection error: %s", + "vcmi.lobby.login.create" : "New Account", + "vcmi.lobby.login.login" : "Login", + "vcmi.lobby.login.as" : "Login as %s", + "vcmi.lobby.login.spectator" : "Spectator", + "vcmi.lobby.header.rooms" : "Game Rooms - %d", + "vcmi.lobby.header.channels" : "Chat Channels", + "vcmi.lobby.header.chat.global" : "Global Game Chat - %s", // %s -> language name + "vcmi.lobby.header.chat.match" : "Chat from previous game on %s", // %s -> game start date & time + "vcmi.lobby.header.chat.player" : "Private chat with %s", // %s -> nickname of another player + "vcmi.lobby.header.history" : "Your Previous Games", + "vcmi.lobby.header.players" : "Players Online - %d", + "vcmi.lobby.match.solo" : "Singleplayer Game", + "vcmi.lobby.match.duel" : "Game with %s", // %s -> nickname of another player + "vcmi.lobby.match.multi" : "%d players", + "vcmi.lobby.room.create" : "Create New Room", + "vcmi.lobby.room.players.limit" : "Players Limit", + "vcmi.lobby.room.description.public" : "Any player can join public room.", + "vcmi.lobby.room.description.private" : "Only invited players can join private room.", + "vcmi.lobby.room.description.new" : "To start the game, select a scenario or set up a random map.", + "vcmi.lobby.room.description.load" : "To start the game, use one of your saved games.", + "vcmi.lobby.room.description.limit" : "Up to %d players can enter your room, including you.", + "vcmi.lobby.invite.header" : "Invite Players", + "vcmi.lobby.invite.notification" : "Player has invited you to their game room. You can now join their private room.", + "vcmi.lobby.preview.title" : "Join Game Room", + "vcmi.lobby.preview.subtitle" : "Game on %s, hosted by %s", //TL Note: 1) name of map or RMG template 2) nickname of game host + "vcmi.lobby.preview.version" : "Game version:", + "vcmi.lobby.preview.players" : "Players:", + "vcmi.lobby.preview.mods" : "Used mods:", + "vcmi.lobby.preview.allowed" : "Join the game room?", + "vcmi.lobby.preview.error.header" : "Unable to join this room.", + "vcmi.lobby.preview.error.playing" : "You need to leave your current game first.", + "vcmi.lobby.preview.error.full" : "The room is already full.", + "vcmi.lobby.preview.error.busy" : "The room no longer accepts new players.", + "vcmi.lobby.preview.error.invite" : "You were not invited to this room.", + "vcmi.lobby.preview.error.mods" : "You are using different set of mods.", + "vcmi.lobby.preview.error.version" : "You are using different version of VCMI.", + "vcmi.lobby.room.new" : "New Game", + "vcmi.lobby.room.load" : "Load Game", + "vcmi.lobby.room.type" : "Room Type", + "vcmi.lobby.room.mode" : "Game Mode", + "vcmi.lobby.room.state.public" : "Public", + "vcmi.lobby.room.state.private" : "Private", + "vcmi.lobby.room.state.busy" : "In Game", + "vcmi.lobby.room.state.invited" : "Invited", + "vcmi.lobby.mod.state.compatible" : "Compatible", + "vcmi.lobby.mod.state.disabled" : "Must be enabled", + "vcmi.lobby.mod.state.version" : "Version mismatch", + "vcmi.lobby.mod.state.excessive" : "Must be disabled", + "vcmi.lobby.mod.state.missing" : "Not installed", + "vcmi.lobby.pvp.coin.hover" : "Coin", + "vcmi.lobby.pvp.coin.help" : "Flips a coin", + "vcmi.lobby.pvp.randomTown.hover" : "Random town", + "vcmi.lobby.pvp.randomTown.help" : "Write a random town in the chat", + "vcmi.lobby.pvp.randomTownVs.hover" : "Random town vs.", + "vcmi.lobby.pvp.randomTownVs.help" : "Write two random towns in the chat", + "vcmi.lobby.pvp.versus" : "vs.", - "vcmi.systemOptions.fullscreenBorderless.hover": "Toàn màn hình (không viền)", - "vcmi.systemOptions.fullscreenBorderless.help": "{Toàn màn hình không viền}\n\nNếu chọn, VCMI sẽ chạy chế độ toàn màn hình không viền. Ở chế độ này, trò chơi sẽ luôn dùng độ phân giải của màn hình, bỏ qua độ phân giải đã chọn.", - "vcmi.systemOptions.fullscreenExclusive.hover": "Toàn màn hình (riêng biệt)", - "vcmi.systemOptions.fullscreenExclusive.help": "{Toàn màn hình}\n\nNếu chọn, VCMI sẽ chạy chế độ dành riêng cho toàn màn hình. Ở chế độ này, trò chơi sẽ chuyển độ phân giải của màn hình sang độ phân giải được chọn.", - "vcmi.systemOptions.resolutionButton.hover": "Độ phân giải: %wx%h", - "vcmi.systemOptions.resolutionButton.help": "{Chọn độ phân giải}\n\nĐổi độ phân giải trong trò chơi.", - "vcmi.systemOptions.resolutionMenu.hover": "Chọn độ phân giải", - "vcmi.systemOptions.resolutionMenu.help": "Đổi độ phân giải trong trò chơi.", - "vcmi.systemOptions.scalingButton.hover": "Phóng đại giao diện: %p%", - "vcmi.systemOptions.scalingButton.help": "{Phóng đại giao diện}\n\nĐổi độ phóng đại giao diện trong trò chơi.", - "vcmi.systemOptions.scalingMenu.hover": "Chọn độ phóng đại giao diện", - "vcmi.systemOptions.scalingMenu.help": "Đổi độ phóng đại giao diện trong trò chơi.", - "vcmi.systemOptions.longTouchButton.hover": "Khoảng thời gian chạm giữ: %d ms", - "vcmi.systemOptions.longTouchButton.help": "{Khoảng thời gian chạm giữ}\n\nKhi dùng màn hình cảm ứng, cửa sổ sẽ bật lên sau khi chạm màn hình trong 1 khoảng thời gian xác định, theo mili giây.", - "vcmi.systemOptions.longTouchMenu.hover": "Chọn khoảng thời gian chạm giữ", - "vcmi.systemOptions.longTouchMenu.help": "Đổi khoảng thời gian chạm giữ.", - "vcmi.systemOptions.longTouchMenu.entry": "%d mili giây", - "vcmi.systemOptions.framerateButton.hover": "Hiện FPS", - "vcmi.systemOptions.framerateButton.help": "{Hiện FPS}\n\nHiện khung hình mỗi giây ở góc cửa sổ trò chơi", - "vcmi.systemOptions.hapticFeedbackButton.hover": "Rung khi chạm", - "vcmi.systemOptions.hapticFeedbackButton.help": "{Rung khi chạm}\n\nBật/ tắt chế độ rung khi chạm.", + "vcmi.client.errors.invalidMap" : "{Bản đồ hoặc chiến dịch không hợp lệ}\n\nKhông thể bắt đầu trò chơi! Bản đồ hoặc chiến dịch đã chọn có thể không hợp lệ hoặc bị lỗi. Như sau:\n%s", + "vcmi.client.errors.missingCampaigns" : "{Thiếu tệp tin dữ liệu}\n\nKhông tìm thấy tệp tin dữ liệu của chiến dịch! Có thể bạn đang sử dụng các tệp tin dữ liệu Heroes 3 bị thiếu hoặc bị lỗi. Hãy thử cài đặt lại trò chơi.", + "vcmi.client.errors.modLoadingFailure" : "{Lỗi tải mod}\n\nĐã phát hiện ra lỗi nghiêm trọng khi tải mod! Trò chơi có thể không hoạt động chính xác hoặc bị văng! Hãy cập nhật hoặc tắt hóa các mod sau:\n\n", + "vcmi.server.errors.disconnected" : "{Mạng bị lỗi}\n\nĐã mất kết nối tới máy chủ trò chơi!", + "vcmi.server.errors.playerLeft" : "{Người chơi}\n\n%s đã ngắt kết nối khỏi trò chơi!", //%s -> player color + "vcmi.server.errors.existingProcess" : "Một chương trình máy chủ VCMI khác đang chạy. Hãy đóng nó trước khi bắt đầu một trò chơi mới.", + "vcmi.server.errors.modsToEnable" : "{Các mod sau đây là bắt buộc}", + "vcmi.server.errors.modsToDisable" : "{Bạn phải tắt các mod sau đây}", + "vcmi.server.errors.unknownEntity" : "Không tải được tệp tin đã lưu! Có lỗi chưa xác định trong tệp tin đã lưu '%s'! Tệp tin có thể không tương thích với phiên bản mod hiện đang cài đặt!", + "vcmi.server.errors.wrongIdentified" : "Bạn được chỉ định là người chơi %s trong khi bạn muốn %s", + "vcmi.server.errors.notAllowed" : "Bạn không được phép thực hiện hành động này!", + + "vcmi.dimensionDoor.seaToLandError" : "Không thể dùng phép Dimension Door để dịch chuyển từ dưới biển lên đất liền hoặc ngược lại.", - "vcmi.adventureOptions.infoBarPick.hover": "Hiện thông báo ở bảng thông tin", - "vcmi.adventureOptions.infoBarPick.help": "{Hiện thông báo ở bảng thông tin}\n\nThông báo từ các điểm đến thăm sẽ hiện ở bảng thông tin, thay vì trong cửa sổ bật lên.", - "vcmi.adventureOptions.numericQuantities.hover": "Số lượng quái", - "vcmi.adventureOptions.numericQuantities.help": "{Số lượng quái}\n\nHiện lượng quái đối phương dạng số A-B.", - "vcmi.adventureOptions.forceMovementInfo.hover": "Luôn hiện chi phí di chuyển", - "vcmi.adventureOptions.forceMovementInfo.help": "{Luôn hiện chi phí di chuyển}\n\nLuôn hiện điểm di chuyển trong thanh trạng thái. (Thay vì chỉ xem khi nhấn giữ phím ALT)", - "vcmi.adventureOptions.showGrid.hover": "Hiện ô kẻ", - "vcmi.adventureOptions.showGrid.help": "{Hiện ô kẻ}\n\nHiện đường biên giữa các ô trên bản đồ phiêu lưu.", - "vcmi.adventureOptions.borderScroll.hover": "Cuộn ở biên", - "vcmi.adventureOptions.borderScroll.help": "{Cuộn ở biên}\n\nCuộn bản đồ phiêu lưu ở biên. Nhấn giữ phím CTRL để tắt chức năng.", - "vcmi.adventureOptions.infoBarCreatureManagement.hover": "Quản lí quái ở bảng thông tin", - "vcmi.adventureOptions.infoBarCreatureManagement.help": "{Quản lí quái ở bảng thông tin}\n\nCho phép sắp xếp quái ở bảng thông tin thay vì luân chuyển giữa các mục mặc định.", - "vcmi.adventureOptions.leftButtonDrag.hover": "Chuột trái kéo bản đồ", - "vcmi.adventureOptions.leftButtonDrag.help": "{Chuột trái kéo bản đồ}\n\nGiữ chuột trái khi di chuyển sẽ dịch chuyển bản đồ phiêu lưu.", - "vcmi.adventureOptions.mapScrollSpeed1.hover": "", - "vcmi.adventureOptions.mapScrollSpeed5.hover": "", - "vcmi.adventureOptions.mapScrollSpeed6.hover": "", - "vcmi.adventureOptions.mapScrollSpeed1.help": "Đặt tốc độ cuộn bản đồ sang rất chậm", - "vcmi.adventureOptions.mapScrollSpeed5.help": "Đặt tốc độ cuộn bản đồ sang rất nhanh", - "vcmi.adventureOptions.mapScrollSpeed6.help": "Đặt tốc độ cuộn bản đồ sang tức thời.", + "vcmi.settingsMainWindow.generalTab.hover": "Chung", + "vcmi.settingsMainWindow.generalTab.help": "Chuyển sang bảng Chung, chứa các cài đặt liên quan đến phần chung trò chơi", + "vcmi.settingsMainWindow.battleTab.hover": "Chiến đấu", + "vcmi.settingsMainWindow.battleTab.help": "Chuyển sang bảng Chiến đấu, cho phép thiết lập hành vi trong trận đánh", + "vcmi.settingsMainWindow.adventureTab.hover": "Bản đồ", + "vcmi.settingsMainWindow.adventureTab.help": "Chuyển sang bảng Bản đồ (là nơi mà người chơi di chuyển tướng của họ)", - "vcmi.battleOptions.queueSizeLabel.hover": "Hiện thứ tự lượt", - "vcmi.battleOptions.queueSizeNoneButton.hover": "TẮT", - "vcmi.battleOptions.queueSizeAutoButton.hover": "TỰ ĐỘNG", - "vcmi.battleOptions.queueSizeSmallButton.hover": "NHỎ", - "vcmi.battleOptions.queueSizeBigButton.hover": "LỚN", - "vcmi.battleOptions.queueSizeNoneButton.help": "Không hiện thứ tự lượt.", - "vcmi.battleOptions.queueSizeAutoButton.help": "Tự động điều chỉnh kích thước thứ tự lượt theo độ phân giải (NHỎ được dùng khi chiều cao thấp hơn 700 px, ngược lại dùng LỚN).", - "vcmi.battleOptions.queueSizeSmallButton.help": "Đặt kích thước thứ tự lượt sang NHỎ.", - "vcmi.battleOptions.queueSizeBigButton.help": "Đặt kích thước thứ tự lượt sang LỚN (không hỗ trợ nếu chiều cao nhỏ hơn 700 px).", - "vcmi.battleOptions.animationsSpeed1.hover": "", - "vcmi.battleOptions.animationsSpeed5.hover": "", - "vcmi.battleOptions.animationsSpeed6.hover": "", - "vcmi.battleOptions.animationsSpeed1.help": "Đặt tốc độ hoạt ảnh sang rất chậm", - "vcmi.battleOptions.animationsSpeed5.help": "Đặt tốc độ hoạt ảnh sang rất nhanh", - "vcmi.battleOptions.animationsSpeed6.help": "Đặt tốc độ hoạt ảnh sang tức thời", - "vcmi.battleOptions.movementHighlightOnHover.hover": "Hiện di chuyển khi di chuột", - "vcmi.battleOptions.movementHighlightOnHover.help": "{Hiện di chuyển khi di chuột}\n\nHiện giới hạn di chuyển của quái khi di chuột lên chúng.", - "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Hiện tầm bắn của cung thủ", - "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Hiện tầm bắn của cung thủ khi di chuột}\n\nHiện tầm bắn của cung thủ khi di chuột lên chúng.", - "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Hiện thông số tướng", - "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Hiện thông số tướng}\n\nBật/ tắt bảng chỉ số cơ bản và năng lượng của tướng.", - "vcmi.battleOptions.skipBattleIntroMusic.hover": "Bỏ qua nhạc dạo đầu", - "vcmi.battleOptions.skipBattleIntroMusic.help": "{Bỏ qua nhạc dạo đầu}\n\nKhông cần chờ hết nhạc khởi đầu mỗi trận đánh", + "vcmi.systemOptions.videoGroup" : "Thiết lập phim ảnh", + "vcmi.systemOptions.audioGroup" : "Thiết lập âm thanh", + "vcmi.systemOptions.otherGroup" : "Thiết lập khác", // unused right now + "vcmi.systemOptions.townsGroup" : "Thành phố", - "vcmi.battleWindow.pressKeyToSkipIntro": "Nhấn phím bất kì để bắt đầu trận đánh", - "vcmi.battleWindow.damageEstimation.melee": "Tấn công %CREATURE (%DAMAGE).", - "vcmi.battleWindow.damageEstimation.meleeKills": "Tấn công %CREATURE (%DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.ranged": "Bắn %CREATURE (%SHOTS, %DAMAGE).", - "vcmi.battleWindow.damageEstimation.rangedKills": "Bắn %CREATURE (%SHOTS, %DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.shots": "Còn %d lần", - "vcmi.battleWindow.damageEstimation.shots.1": "Còn %d lần", - "vcmi.battleWindow.damageEstimation.damage": "%d sát thương", - "vcmi.battleWindow.damageEstimation.damage.1": "%d sát thương", - "vcmi.battleWindow.damageEstimation.kills": "%d sẽ bị diệt", - "vcmi.battleWindow.damageEstimation.kills.1": "%d sẽ bị diệt", + "vcmi.statisticWindow.statistics" : "Thống Kê", + "vcmi.statisticWindow.tsvCopy" : "Dữ liệu vào bộ nhớ tạm", + "vcmi.statisticWindow.selectView" : "Chọn chế độ xem", + "vcmi.statisticWindow.value" : "Giá trị", + "vcmi.statisticWindow.title.overview" : "Tổng quan", + "vcmi.statisticWindow.title.resources" : "Tài nguyên", + "vcmi.statisticWindow.title.income" : "Thu nhập", + "vcmi.statisticWindow.title.numberOfHeroes" : "Số tướng", + "vcmi.statisticWindow.title.numberOfTowns" : "Số thành", + "vcmi.statisticWindow.title.numberOfArtifacts" : "Sô báu vật", + "vcmi.statisticWindow.title.numberOfDwellings" : "Số nhà quân", + "vcmi.statisticWindow.title.numberOfMines" : "Số lượng mỏ", + "vcmi.statisticWindow.title.armyStrength" : "Sức mạnh quân đội", + "vcmi.statisticWindow.title.experience" : "Kinh nghiệm", + "vcmi.statisticWindow.title.resourcesSpentArmy" : "Chí phí mua quân", + "vcmi.statisticWindow.title.resourcesSpentBuildings" : "Chi phí xây dựng", + "vcmi.statisticWindow.title.mapExplored" : "Tỉ lệ mở bản đồ", + "vcmi.statisticWindow.param.playerName" : "Tên người chơi", + "vcmi.statisticWindow.param.daysSurvived" : "Số ngày còn sống", + "vcmi.statisticWindow.param.maxHeroLevel" : "Cấp độ tướng tối đa", + "vcmi.statisticWindow.param.battleWinRatioHero" : "Tỉ lệ thắng (vs. tướng)", + "vcmi.statisticWindow.param.battleWinRatioNeutral" : "Tỉ lệ thắng (vs. trung lập)", + "vcmi.statisticWindow.param.battlesHero" : "Trận chiến (vs. tướng)", + "vcmi.statisticWindow.param.battlesNeutral" : "Trận chiến (vs. trung lập)", + "vcmi.statisticWindow.param.maxArmyStrength" : "Quân đội mạnh nhất", + "vcmi.statisticWindow.param.tradeVolume" : "Số lượng trao đổi", + "vcmi.statisticWindow.param.obeliskVisited" : "Cột tháp đã đến", + "vcmi.statisticWindow.icon.townCaptured" : "Chiếm thành", + "vcmi.statisticWindow.icon.strongestHeroDefeated" : "Tướng mạnh nhất của đối thủ bị đánh bại", + "vcmi.statisticWindow.icon.grailFound" : "Tìm thấy Grail", + "vcmi.statisticWindow.icon.defeated" : "Đã bị đánh bại", - "vcmi.battleResultsWindow.applyResultsLabel": "Dùng kết quả trận đánh", + "vcmi.systemOptions.fullscreenBorderless.hover": "Toàn màn hình (không viền)", + "vcmi.systemOptions.fullscreenBorderless.help": "{Toàn màn hình không viền}\n\nNếu chọn, VCMI sẽ chạy chế độ toàn màn hình không viền. Ở chế độ này, trò chơi sẽ luôn dùng độ phân giải của màn hình, bỏ qua độ phân giải đã chọn.", + "vcmi.systemOptions.fullscreenExclusive.hover": "Toàn màn hình (riêng biệt)", + "vcmi.systemOptions.fullscreenExclusive.help": "{Toàn màn hình}\n\nNếu chọn, VCMI sẽ chạy chế độ dành riêng cho toàn màn hình. Ở chế độ này, trò chơi sẽ chuyển độ phân giải của màn hình sang độ phân giải được chọn.", + "vcmi.systemOptions.resolutionButton.hover": "Độ phân giải: %wx%h", + "vcmi.systemOptions.resolutionButton.help": "{Chọn độ phân giải}\n\nĐổi độ phân giải trong trò chơi.", + "vcmi.systemOptions.resolutionMenu.hover": "Chọn độ phân giải", + "vcmi.systemOptions.resolutionMenu.help": "Đổi độ phân giải trong trò chơi.", + "vcmi.systemOptions.scalingButton.hover": "Phóng đại giao diện: %p%", + "vcmi.systemOptions.scalingButton.help": "{Phóng đại giao diện}\n\nĐổi độ phóng đại giao diện trong trò chơi.", + "vcmi.systemOptions.scalingMenu.hover": "Chọn độ phóng đại giao diện", + "vcmi.systemOptions.scalingMenu.help": "Đổi độ phóng đại giao diện trong trò chơi.", + "vcmi.systemOptions.longTouchButton.hover": "Khoảng thời gian chạm giữ: %d ms", // Ghi chú dịch: "ms" = "mili giây" + "vcmi.systemOptions.longTouchButton.help": "{Khoảng thời gian chạm giữ}\n\nKhi dùng màn hình cảm ứng, cửa sổ sẽ bật lên sau khi chạm màn hình trong 1 khoảng thời gian xác định, theo mili giây.", + "vcmi.systemOptions.longTouchMenu.hover": "Chọn khoảng thời gian chạm giữ", + "vcmi.systemOptions.longTouchMenu.help": "Đổi khoảng thời gian chạm giữ.", + "vcmi.systemOptions.longTouchMenu.entry": "%d mili giây", + "vcmi.systemOptions.framerateButton.hover": "Hiện FPS", + "vcmi.systemOptions.framerateButton.help": "{Hiện FPS}\n\nHiện khung hình mỗi giây ở góc cửa sổ trò chơi", + "vcmi.systemOptions.hapticFeedbackButton.hover": "Rung khi chạm", + "vcmi.systemOptions.hapticFeedbackButton.help": "{Rung khi chạm}\n\nBật/ tắt chế độ rung khi chạm.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Cải thiện giao diện", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Cải thiện giao diện}\n\nThay đổi nhiều giao diện cho người chơi những trải nghiệm mới hơn. Chẳng hạn như nút ba lô, v.v. Tắt để có trải nghiệm cổ điển.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Sách phép rộng hơn", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Sách phép rộng hơn}\n\nChứa được nhiều phép thuật hơn trên mỗi trang. Hoạt ảnh thay đổi trang của sách phép không hoạt động khi bật cài đặt này.", + "vcmi.systemOptions.audioMuteFocus.hover" : "Tắt âm thanh", + "vcmi.systemOptions.audioMuteFocus.help" : "{Tắt âm thanh}\n\nTắt tiếng khi cửa sổ không hoạt động. Ngoại trừ tin nhắn trong trò chơi và âm thanh lượt mới.", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover": "Hiện quái được mua", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help": "{Hiện quái được mua}\n\nHiện quái được mua thay vì sinh trưởng trong sơ lược thành (góc trái dưới màn hình thành phố).", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover": "Hiện sinh trưởng quái hàng tuần", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help": "{Hiện sinh trưởng quái hàng tuần}\n\nHiện sinh trưởng quái thay vì lượng có sẵn trong sơ lược thành (góc trái dưới màn hình thành phố).", - "vcmi.otherOptions.compactTownCreatureInfo.hover": "Thu gọn thông tin quái", - "vcmi.otherOptions.compactTownCreatureInfo.help": "{Thu gọn thông tin quái}\n\nHiện thông tin quái nhỏ hơn trong sơ lược thành (góc trái dưới màn hình thành phố).", + "vcmi.adventureOptions.infoBarPick.hover": "Hiện thông báo ở bảng thông tin", + "vcmi.adventureOptions.infoBarPick.help": "{Hiện thông báo ở bảng thông tin}\n\nThông báo từ các điểm đến thăm sẽ hiện ở bảng thông tin, thay vì trong cửa sổ bật lên.", + "vcmi.adventureOptions.numericQuantities.hover": "Số lượng quân", + "vcmi.adventureOptions.numericQuantities.help": "{Số lượng quân}\n\nHiện số lượng quân của đối thủ dưới dạng số A-B.", + "vcmi.adventureOptions.forceMovementInfo.hover": "Luôn hiện chi phí di chuyển", + "vcmi.adventureOptions.forceMovementInfo.help": "{Luôn hiện chi phí di chuyển}\n\nLuôn hiện điểm di chuyển trong thanh trạng thái. (Thay vì chỉ xem khi nhấn giữ phím Alt)", + "vcmi.adventureOptions.showGrid.hover": "Hiện ô lưới", + "vcmi.adventureOptions.showGrid.help": "{Hiện ô lưới}\n\nHiện đường biên giữa các ô lưới trên bản đồ phiêu lưu.", + "vcmi.adventureOptions.borderScroll.hover": "Cuộn ở đường biên", + "vcmi.adventureOptions.borderScroll.help": "{Cuộn ở đường biên}\n\nCuộn bản đồ phiêu lưu ở đường biên. Nhấn giữ phím Ctrl để tạm dừng chức năng này.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover": "Quản lí quân ở bảng thông tin", + "vcmi.adventureOptions.infoBarCreatureManagement.help": "{Quản lí quân ở bảng thông tin}\n\nCho phép sắp xếp quân ở bảng thông tin thay vì luân chuyển giữa các mục mặc định.", + "vcmi.adventureOptions.leftButtonDrag.hover": "Kéo chuột trái", + "vcmi.adventureOptions.leftButtonDrag.help": "{Kéo chuột trái}\n\nGiữ và kéo chuột trái khi di chuyển sẽ dịch chuyển bản đồ phiêu lưu.", + "vcmi.adventureOptions.rightButtonDrag.hover" : "Kéo chuột phải", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Kéo chuột phải}\n\nGiữ và kéo chuột phải khi di chuyển sẽ dịch chuyển bản đồ phiêu lưu.", + "vcmi.adventureOptions.smoothDragging.hover" : "Kéo bản đồ dễ hơn", + "vcmi.adventureOptions.smoothDragging.help" : "{Kéo bản đồ dễ hơn}\n\nKhi kéo bản đồ có hiệu ứng mới sẽ mượt hơn.", + "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Bỏ đi hiệu ứng mờ dần", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Bỏ đi hiệu ứng mờ dần}\n\nKhi được bật, sẽ lược bỏ các hiệu ứng mờ dần của đối tượng (như thu thập tài nguyên, lên tàu, v.v.). Đặc biệt hữu ích trong các trò chơi PvP. Để có tốc độ di chuyển tối đa, bỏ đi hiệu ứng sẽ hoạt động bất kể cài đặt này.", + "vcmi.adventureOptions.mapScrollSpeed1.hover": "", + "vcmi.adventureOptions.mapScrollSpeed5.hover": "", + "vcmi.adventureOptions.mapScrollSpeed6.hover": "", + "vcmi.adventureOptions.mapScrollSpeed1.help": "Đặt tốc độ cuộn bản đồ sang rất chậm.", + "vcmi.adventureOptions.mapScrollSpeed5.help": "Đặt tốc độ cuộn bản đồ sang rất nhanh.", + "vcmi.adventureOptions.mapScrollSpeed6.help": "Đặt tốc độ cuộn bản đồ sang tức thời.", + "vcmi.adventureOptions.hideBackground.hover" : "Ẩn ở chế độ nền", + "vcmi.adventureOptions.hideBackground.help" : "{Ẩn ở chế độ nền}\n\nẨn bản đồ phiêu lưu ở chế độ nền và thay vào đó là hiển thị họa tiết.", - "vcmi.townHall.missingBase": "Căn cứ %s phải được xây trước", - "vcmi.townHall.noCreaturesToRecruit": "Không có quái để chiêu mộ!", + "vcmi.battleOptions.queueSizeLabel.hover": "Hiện thứ tự lượt", + "vcmi.battleOptions.queueSizeNoneButton.hover": "Tắt", + "vcmi.battleOptions.queueSizeAutoButton.hover": "Tự động", + "vcmi.battleOptions.queueSizeSmallButton.hover": "Nhỏ", + "vcmi.battleOptions.queueSizeBigButton.hover": "Lớn", + "vcmi.battleOptions.queueSizeNoneButton.help": "Không hiện thứ tự lượt.", + "vcmi.battleOptions.queueSizeAutoButton.help": "Tự động điều chỉnh kích thước thứ tự lượt theo độ phân giải (Nhỏ được dùng khi chiều cao thấp hơn 700 px, ngược lại dùng Lớn).", + "vcmi.battleOptions.queueSizeSmallButton.help": "Đặt kích thước thứ tự lượt sang Nhỏ.", + "vcmi.battleOptions.queueSizeBigButton.help": "Đặt kích thước thứ tự lượt sang Lớn (không hỗ trợ nếu chiều cao nhỏ hơn 700 px).", + "vcmi.battleOptions.animationsSpeed1.hover": "", + "vcmi.battleOptions.animationsSpeed5.hover": "", + "vcmi.battleOptions.animationsSpeed6.hover": "", + "vcmi.battleOptions.animationsSpeed1.help": "Đặt tốc độ hình ảnh sang rất chậm.", + "vcmi.battleOptions.animationsSpeed5.help": "Đặt tốc độ hình ảnh sang rất nhanh.", + "vcmi.battleOptions.animationsSpeed6.help": "Đặt tốc độ hình ảnh sang tức thời.", + "vcmi.battleOptions.movementHighlightOnHover.hover": "Hiện di chuyển khi di chuột", + "vcmi.battleOptions.movementHighlightOnHover.help": "{Hiện di chuyển khi di chuột}\n\nHiện giới hạn di chuyển của quân khi di chuột lên chúng.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Hiện tầm bắn của cung thủ", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Hiện tầm bắn của cung thủ khi di chuột}\n\nHiện tầm bắn của cung thủ khi di chuột lên chúng.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Hiện thông số tướng", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Hiện thông số tướng}\n\nBật/ tắt bảng chỉ số cơ bản và năng lượng của tướng.", + "vcmi.battleOptions.skipBattleIntroMusic.hover": "Bỏ qua nhạc dạo đầu", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Bỏ qua nhạc dạo đầu}\n\nKhông cần chờ hết nhạc khởi đầu mỗi trận đánh", + "vcmi.battleOptions.endWithAutocombat.hover": "Kết thúc trận chiến", + "vcmi.battleOptions.endWithAutocombat.help": "{Kết thúc trận chiến}\n\nTự động chiến đấu để kết thúc ngay lập tức", + "vcmi.battleOptions.showQuickSpell.hover": "Hiện bảng phép", + "vcmi.battleOptions.showQuickSpell.help": "{Hiện bảng phép}\n\nHiển thị bảng phép để chọn nhanh các phép thuật", - "vcmi.logicalExpressions.anyOf": "Bất kì cái sau:", - "vcmi.logicalExpressions.allOf": "Tất cả cái sau:", - "vcmi.logicalExpressions.noneOf": "Không có những cái sau:", + "vcmi.adventureMap.revisitObject.hover" : "Xem lại đối tượng", + "vcmi.adventureMap.revisitObject.help" : "{Xem lại đối tượng}\n\nNếu tướng đang đứng ở một đối tượng trên bản đồ, tướng có thể quay trở lại vị trí đó.", - "vcmi.heroWindow.openCommander.hover": "Mở cửa sổ thông tin chỉ huy", - "vcmi.heroWindow.openCommander.help": "Hiện chi tiết về chỉ huy tướng này", - "vcmi.heroWindow.openBackpack.hover": "Mở hành lí", - "vcmi.heroWindow.openBackpack.help": "Hành lí cho phép quản lí vật phẩm dễ dàng hơn.", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Bấm phím bất kì để bắt đầu trận đánh", + "vcmi.battleWindow.damageEstimation.melee": "Tấn công %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills": "Tấn công %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged": "Bắn %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills": "Bắn %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots": "Còn %d lần", + "vcmi.battleWindow.damageEstimation.shots.1": "Còn %d lần", + "vcmi.battleWindow.damageEstimation.damage": "%d sát thương", + "vcmi.battleWindow.damageEstimation.damage.1": "%d sát thương", + "vcmi.battleWindow.damageEstimation.kills": "%d sẽ bị diệt", + "vcmi.battleWindow.damageEstimation.kills.1": "%d sẽ bị diệt", + + "vcmi.battleWindow.damageRetaliation.will": "Sẽ phản đòn ", + "vcmi.battleWindow.damageRetaliation.may": "Có thể phản đòn ", + "vcmi.battleWindow.damageRetaliation.never": "Không phản đòn.", + "vcmi.battleWindow.damageRetaliation.damage": "(%DAMAGE).", + "vcmi.battleWindow.damageRetaliation.damageKills": "(%DAMAGE, %KILLS).", + + "vcmi.battleWindow.killed": "Đã bị giết", + "vcmi.battleWindow.accurateShot.resultDescription.0": "%d %s đã bị giết bằng những phát bắn chính xác!", + "vcmi.battleWindow.accurateShot.resultDescription.1": "%d %s đã bị giết bằng một phát bắn chính xác!", + "vcmi.battleWindow.accurateShot.resultDescription.2": "%d %s đã bị giết bằng những phát bắn chính xác!", + "vcmi.battleWindow.endWithAutocombat": "Bạn có muốn kết thúc trận đấu bằng chế độ đánh tự động không?", - "vcmi.commanderWindow.artifactMessage": "Bạn muốn trả lại vật phẩm này cho tướng?", + "vcmi.battleResultsWindow.applyResultsLabel" : "Chấp nhận kết quả trận đấu?", - "vcmi.creatureWindow.showBonuses.hover": "Chuyển sang phần tăng thêm", - "vcmi.creatureWindow.showBonuses.help": "Hiện thuộc tính tăng thêm của chỉ huy", - "vcmi.creatureWindow.showSkills.hover": "Chuyển sang phần kĩ năng", - "vcmi.creatureWindow.showSkills.help": "Hiện kĩ năng đã học của chỉ huy", - "vcmi.creatureWindow.returnArtifact.hover": "Trả vật phẩm", - "vcmi.creatureWindow.returnArtifact.help": "Nhấn nút này để trả vật phẩm cho tướng", + "vcmi.tutorialWindow.title" : "Màn Hình Cảm Ứng", + "vcmi.tutorialWindow.decription.RightClick" : "Chạm và giữ phần tử mà bạn muốn bấm chuột phải. Chạm vào vùng trống để đóng.", + "vcmi.tutorialWindow.decription.MapPanning" : "Chạm và kéo bằng một ngón tay để di chuyển bản đồ.", + "vcmi.tutorialWindow.decription.MapZooming" : "Chụm hai ngón tay để thay đổi mức độ phóng to hoặc thu nhỏ bản đồ.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Vuốt để mở thêm nhiều hành động khác nhau, chẳng hạn như quản lý quân/tướng và sắp xếp các thành.", + "vcmi.tutorialWindow.decription.BattleDirection" : "Để tấn công từ một hướng cụ thể, hãy vuốt theo hướng mà đòn tấn công sẽ đánh.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Chỉ hướng tấn công có thể bị hủy nếu ngón tay ở quá xa.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Chạm và giữ để hủy phép thuật.", - "vcmi.questLog.hideComplete.hover": "Ẩn nhiệm vụ đã làm", - "vcmi.questLog.hideComplete.help": "Ẩn các nhiệm vụ đã hoàn thành", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover": "Hiện quân được mua", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help": "{Hiện quân được mua}\n\nHiện quân được mua thay vì sinh trưởng trong sơ lược thành (góc trái dưới màn hình thành phố).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover": "Hiện sinh trưởng quân", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help": "{Hiện sinh trưởng quân}\n\nHiện sinh trưởng quân hàng tuần thay vì số lượng có sẵn trong sơ lược thành (góc trái dưới màn hình thành phố).", + "vcmi.otherOptions.compactTownCreatureInfo.hover": "Thu gọn thông tin quân", + "vcmi.otherOptions.compactTownCreatureInfo.help": "{Thu gọn thông tin quân}\n\nHiện thông tin quân nhỏ hơn trong sơ lược thành (góc trái dưới màn hình thành phố).", - "vcmi.randomMapTab.widgets.defaultTemplate": "(mặc định)", - "vcmi.randomMapTab.widgets.templateLabel": "Mẫu", - "vcmi.randomMapTab.widgets.teamAlignmentsButton": "Cài đặt...", - "vcmi.randomMapTab.widgets.teamAlignmentsLabel": "Sắp đội", - "vcmi.randomMapTab.widgets.roadTypesLabel": "Kiểu đường xá", + "vcmi.townHall.missingBase" : "Bạn phải xây %s trước đã", + "vcmi.townHall.noCreaturesToRecruit" : "Không có quân để chiêu mộ!", - "vcmi.optionsTab.chessFieldBase.hover" : "Thời gian thêm", - "vcmi.optionsTab.chessFieldTurn.hover" : "Thời gian lượt", - "vcmi.optionsTab.chessFieldBattle.hover" : "Thời gian trận đánh", - "vcmi.optionsTab.chessFieldUnit.hover" : "Thời gian lính", - "vcmi.optionsTab.chessFieldBase.help": "Bắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.", - "vcmi.optionsTab.chessFieldTurn.help": "Bắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.", - "vcmi.optionsTab.chessFieldBattle.help": "Đếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.", - "vcmi.optionsTab.chessFieldUnit.help": "Bắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.", + "vcmi.townStructure.bank.borrow" : "Nhìn thấy bạn vào nhà băng, một nhân viên đến gần bạn và nói: \"Chúng tôi có thể cho bạn vay 2500 vàng trong 5 ngày. Bạn phải trả lại cho chúng tôi 500 vàng mỗi ngày.\"", + "vcmi.townStructure.bank.payBack" : "Nhìn thấy bạn vào nhà băng, một nhân viên đến gần bạn và nói: \"Bạn đã nhận được khoản vay của mình. Hãy trả lại cho chúng tôi trước khi vay khoản mới.\"", - "vcmi.map.victoryCondition.daysPassed.toOthers": "Đối thủ đã xoay xở để sinh tồn đến ngày này. Họ giành chiến thắng!", - "vcmi.map.victoryCondition.daysPassed.toSelf": "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!", - "vcmi.map.victoryCondition.eliminateMonsters.toOthers": "Đối thủ đã diệt tất cả quái gây hại vùng này và giành chiến thắng!", - "vcmi.map.victoryCondition.eliminateMonsters.toSelf": "Chúc mừng! Bạn đã diệt tất cả quái gây hại vùng này và giành chiến thắng!", - "vcmi.map.victoryCondition.collectArtifacts.message": "Đoạt 3 vật phẩm", - "vcmi.map.victoryCondition.angelicAlliance.toSelf": "Chúc mừng! Tất cả đối thủ bị đánh bại và bạn có Angelic Alliance! Chiến thắng thuộc về bạn!", - "vcmi.map.victoryCondition.angelicAlliance.message": "Đánh bại tất cả đối thủ và tạo Angelic Alliance", + "vcmi.logicalExpressions.anyOf" : "Bất kì cái nào sau đây:", + "vcmi.logicalExpressions.allOf" : "Tất cả những cái sau:", + "vcmi.logicalExpressions.noneOf" : "Không có những cái sau:", - "vcmi.stackExperience.description": "» K I N H N G H I Ệ M «\n\nLoại Quái ................... : %s\nCấp Kinh Nghiệm ................. : %s (%i)\nĐiểm Kinh Nghiệm ............... : %i\nĐiểm Kinh Nghiệm Để Lên Cấp .. : %i\nKinh Nghiệm Tối Đa Mỗi Trận Đánh ... : %i%% (%i)\nSố Lượng Quái .... : %i\nTối Đa Mua Mới\n không bị giảm cấp .... : %i\nHệ Số Kinh Nghiệm ........... : %.2f\nHệ Số Nâng Cấp .............. : %.2f\nKinh Nghiệm Sau Cấp 10 ........ : %i\nTối Đa Mua Mới để vẫn ở\n Mức Tối Đa Kinh Nghiệm Cấp 10 : %i", - "vcmi.stackExperience.rank.0": "Lính Mới", - "vcmi.stackExperience.rank.1": "Tập Sự", - "vcmi.stackExperience.rank.2": "Lành Nghề", - "vcmi.stackExperience.rank.3": "Khéo Léo", - "vcmi.stackExperience.rank.4": "Thông Thạo", - "vcmi.stackExperience.rank.5": "Kì Cựu", - "vcmi.stackExperience.rank.6": "Lão Luyện", - "vcmi.stackExperience.rank.7": "Chuyên Gia", - "vcmi.stackExperience.rank.8": "Tinh Hoa", - "vcmi.stackExperience.rank.9": "Bậc Thầy", - "vcmi.stackExperience.rank.10": "Thiên Tài", + "vcmi.heroWindow.openCommander.hover" : "Mở cửa sổ thông tin chỉ huy", + "vcmi.heroWindow.openCommander.help" : "Hiện thông tin chi tiết về chỉ huy của tướng này.", + "vcmi.heroWindow.openBackpack.hover" : "Mở cửa sổ ba lô báu vật", + "vcmi.heroWindow.openBackpack.help" : "Mở cửa sổ để quản lý ba lô báu vật dễ dàng hơn.", + "vcmi.heroWindow.sortBackpackByCost.hover" : "Sắp xếp theo giá", + "vcmi.heroWindow.sortBackpackByCost.help" : "Sắp xếp các báu vật trong ba lô theo giá.", + "vcmi.heroWindow.sortBackpackBySlot.hover" : "Sắp xếp theo vị trí", + "vcmi.heroWindow.sortBackpackBySlot.help" : "Sắp xếp báu vật trong ba lô theo ô được trang bị.", + "vcmi.heroWindow.sortBackpackByClass.hover" : "Sắp xếp theo loại", + "vcmi.heroWindow.sortBackpackByClass.help" : "Sắp xếp các báu vật trong ba lô theo loại: Chính, Phụ, Cổ đại và Quý hiếm.", + "vcmi.heroWindow.fusingArtifact.fusing" : "Bạn đã sở hữu tất cả các món đồ cần thiết để hợp nhất %s. Bạn có muốn hợp nhất không? {Tất cả các món đồ sẽ được sử dụng khi hợp nhất.}", - "core.bonus.ADDITIONAL_ATTACK.name": "Đánh 2 lần", - "core.bonus.ADDITIONAL_ATTACK.description": "Tấn công 2 lần", - "core.bonus.ADDITIONAL_RETALIATION.name": "Thêm phản công", - "core.bonus.ADDITIONAL_RETALIATION.description": "Phản công thêm ${val} lần", - "core.bonus.AIR_IMMUNITY.name": "Kháng Khí", - "core.bonus.AIR_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Khí", - "core.bonus.ATTACKS_ALL_ADJACENT.name": "Đánh xung quanh", - "core.bonus.ATTACKS_ALL_ADJACENT.description": "Tấn công tất cả đối phương xung quanh", - "core.bonus.BLOCKS_RETALIATION.name": "Ngăn phản công", - "core.bonus.BLOCKS_RETALIATION.description": "Đối phương không thể phản công", - "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Ngăn bắn phản công", - "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Đối phương không thể bắn phản công", - "core.bonus.CATAPULT.name": "Công thành", - "core.bonus.CATAPULT.description": "Tấn công tường thành", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Giảm yêu cầu năng lượng (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Giảm ${val} năng lượng cần làm phép", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tăng yêu cầu năng lượng (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Tăng ${val} năng lượng đối phương cần làm phép", - "core.bonus.CHARGE_IMMUNITY.name": "Kháng đột kích", - "core.bonus.CHARGE_IMMUNITY.description": "Kháng đột kích của Cavalier và Champion", - "core.bonus.DARKNESS.name": "Màn tối", - "core.bonus.DARKNESS.description": "Tạo màn bóng tối bán kính ${val}", - "core.bonus.DEATH_STARE.name": "Cái nhìn chết chóc (${val}%)", - "core.bonus.DEATH_STARE.description": "${val}% cơ hội diệt thêm quái", - "core.bonus.DEFENSIVE_STANCE.name": "Tăng Thủ", - "core.bonus.DEFENSIVE_STANCE.description": "+${val} Thủ khi đang thế thủ", - "core.bonus.DESTRUCTION.name": "Hủy diệt", - "core.bonus.DESTRUCTION.description": "${val}% cơ hội diệt thêm quái", - "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Đòn chí mạng", - "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% cơ hội nhân đôi sát thương khi tấn công", - "core.bonus.DRAGON_NATURE.name": "Rồng", - "core.bonus.DRAGON_NATURE.description": "Quái có chất Rồng", - "core.bonus.EARTH_IMMUNITY.name": "Kháng Đất", - "core.bonus.EARTH_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Đất", - "core.bonus.ENCHANTER.name": "Bùa chú", - "core.bonus.ENCHANTER.description": "Ếm ${subtype.spell} mỗi lượt", - "core.bonus.ENCHANTED.name": "Chúc phúc", - "core.bonus.ENCHANTED.description": "Nhận phép vĩnh cửu ${subtype.spell}", - "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Xuyên giáp (${val}%)", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Khi tấn công, bỏ qua ${val}% Thủ", - "core.bonus.FIRE_IMMUNITY.name": "Kháng Lửa", - "core.bonus.FIRE_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Lửa", - "core.bonus.FIRE_SHIELD.name": "Khiên lửa (${val}%)", - "core.bonus.FIRE_SHIELD.description": "Phản ${val}% sát thương cận chiến", - "core.bonus.FIRST_STRIKE.name": "Đánh trước", - "core.bonus.FIRST_STRIKE.description": "Quái phản công trước khi bị tấn công", - "core.bonus.FEAR.name": "Hãi hùng", - "core.bonus.FEAR.description": "Gây Hoảng Sợ lên quái đối phương", - "core.bonus.FEARLESS.name": "Can đảm", - "core.bonus.FEARLESS.description": "Không bị hoảng sợ", - "core.bonus.FLYING.name": "Bay", - "core.bonus.FLYING.description": "Vượt chướng ngại vật", - "core.bonus.FREE_SHOOTING.name": "Bắn gần", - "core.bonus.FREE_SHOOTING.description": "Bắn kể cả khi cận chiến", - "core.bonus.GARGOYLE.name": "Gargoyle", - "core.bonus.GARGOYLE.description": "Không thể hồi sinh", - "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Giảm sát thương (${val}%)", - "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Giảm ${val}% sát thương vật lí", - "core.bonus.HATE.name": "Ghét ${subtype.creature}", - "core.bonus.HATE.description": "Tăng thêm ${val}% sát thương cho ${subtype.creature}", - "core.bonus.HEALER.name": "Hồi máu", - "core.bonus.HEALER.description": "Hồi máu đồng đội", - "core.bonus.HP_REGENERATION.name": "Tự hồi máu", - "core.bonus.HP_REGENERATION.description": "Hồi ${SHval} máu mỗi lượt", - "core.bonus.JOUSTING.name": "Đột kích", - "core.bonus.JOUSTING.description": "+${val}% sát thương cho mỗi ô đi qua", - "core.bonus.KING.name": "Khổng lồ", - "core.bonus.KING.description": "Dễ tổn thương bởi Diệt Khổng Lồ cấp ${val} hoặc cao hơn", - "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Kháng phép 1-${val}", - "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Kháng phép cấp 1 - ${val}", - "core.bonus.LIMITED_SHOOTING_RANGE.name": "Tầm bắn", - "core.bonus.LIMITED_SHOOTING_RANGE.description": "Không thể nhắm bắn quái xa hơn ${val} ô", - "core.bonus.LIFE_DRAIN.name": "Hút máu (${val}%)", - "core.bonus.LIFE_DRAIN.description": "Hồi máu ${val}% sát thương gây ra", - "core.bonus.MANA_CHANNELING.name": "Chuyển năng lượng ${val}%", - "core.bonus.MANA_CHANNELING.description": "Cho tướng ${val}% năng lượng dùng bởi đối phương", - "core.bonus.MANA_DRAIN.name": "Hút năng lượng", - "core.bonus.MANA_DRAIN.description": "Hút ${val} năng lượng mỗi lượt", - "core.bonus.MAGIC_MIRROR.name": "Phản phép (${val}%)", - "core.bonus.MAGIC_MIRROR.description": "${val}% cơ hội phản phép tấn công đến quái đối phương", - "core.bonus.MAGIC_RESISTANCE.name": "Né phép (${val}%)", - "core.bonus.MAGIC_RESISTANCE.description": "${val}% cơ hội tránh phép của đối phương", - "core.bonus.MIND_IMMUNITY.name": "Kháng phép tinh thần", - "core.bonus.MIND_IMMUNITY.description": "Kháng ma thuật về tinh thần", - "core.bonus.NO_DISTANCE_PENALTY.name": "Bắn xa", - "core.bonus.NO_DISTANCE_PENALTY.description": "Gây trọn sát thương ở bất kì khoảng cách nào", - "core.bonus.NO_MELEE_PENALTY.name": "Đánh gần", - "core.bonus.NO_MELEE_PENALTY.description": "Quái không bị giảm sát thương khi cận chiến", - "core.bonus.NO_MORALE.name": "Bình tĩnh", - "core.bonus.NO_MORALE.description": "Quái không ảnh hưởng bởi sĩ khí", - "core.bonus.NO_WALL_PENALTY.name": "Bỏ qua tường", - "core.bonus.NO_WALL_PENALTY.description": "Gây trọn sát thương khi công thành", - "core.bonus.NON_LIVING.name": "Vô sinh", - "core.bonus.NON_LIVING.description": "Kháng nhiều hiệu ứng", - "core.bonus.RANDOM_SPELLCASTER.name": "Ếm ngẫu nhiên", - "core.bonus.RANDOM_SPELLCASTER.description": "Ếm phép ngẫu nhiên", - "core.bonus.RANGED_RETALIATION.name": "Phản công tầm xa", - "core.bonus.RANGED_RETALIATION.description": "Phản công khi bị bắn", - "core.bonus.RECEPTIVE.name": "Tiếp thu", - "core.bonus.RECEPTIVE.description": "Không kháng phép có lợi", - "core.bonus.REBIRTH.name": "Tái sinh (${val}%)", - "core.bonus.REBIRTH.description": "${val}% số lượng sẽ hồi sinh sau khi chết", - "core.bonus.RETURN_AFTER_STRIKE.name": "Du kích", - "core.bonus.RETURN_AFTER_STRIKE.description": "Trở về sau khi đánh", - "core.bonus.SHOOTER.name": "Cung thủ", - "core.bonus.SHOOTER.description": "Quái có thể tấn công tầm xa", - "core.bonus.SHOOTS_ALL_ADJACENT.name": "Bắn xung quanh", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "Bắn tất cả quái trong phạm vi nhỏ", - "core.bonus.SOUL_STEAL.name": "Hút hồn", - "core.bonus.SOUL_STEAL.description": "Tăng ${val} quái mới với mỗi quái đối phương bị diệt", - "core.bonus.SPELLCASTER.name": "Pháp sư", - "core.bonus.SPELLCASTER.description": "Có thể ếm phép ${subtype.spell}", - "core.bonus.SPELL_AFTER_ATTACK.name": "Ếm sau khi đánh", - "core.bonus.SPELL_AFTER_ATTACK.description": "${val}% cơ hội ếm phép ${subtype.spell} sau khi tấn công", - "core.bonus.SPELL_BEFORE_ATTACK.name": "Ếm trước khi đánh", - "core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% cơ hội ếm phép ${subtype.spell} trước khi tấn công", - "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Kháng phép", - "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Sát thương phép giảm ${val}%", - "core.bonus.SPELL_IMMUNITY.name": "Miễn dịch", - "core.bonus.SPELL_IMMUNITY.description": "Miễn dịch với phép ${subtype.spell}", - "core.bonus.SPELL_LIKE_ATTACK.name": "Đánh phép", - "core.bonus.SPELL_LIKE_ATTACK.description": "Tấn công bằng phép ${subtype.spell}", - "core.bonus.SPELL_RESISTANCE_AURA.name": "Hào quang kháng phép", - "core.bonus.SPELL_RESISTANCE_AURA.description": "Quái ở gần nhận ${val}% kháng ma thuật", - "core.bonus.SUMMON_GUARDIANS.name": "Gọi bảo vệ", - "core.bonus.SUMMON_GUARDIANS.description": "Đầu trận gọi quái ${subtype.creature} (${val}%)", - "core.bonus.SYNERGY_TARGET.name": "Hợp lực", - "core.bonus.SYNERGY_TARGET.description": "Quái này dễ bị ảnh hưởng hợp lực", - "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Hơi thở", - "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Tấn công 2 ô", - "core.bonus.THREE_HEADED_ATTACK.name": "Ba đầu", - "core.bonus.THREE_HEADED_ATTACK.description": "Tấn công cả quái liền kề mục tiêu", - "core.bonus.TRANSMUTATION.name": "Biến đổi", - "core.bonus.TRANSMUTATION.description": "${val}% cơ hội biến đổi quái mục tiêu thành dạng khác", - "core.bonus.UNDEAD.name": "Thây ma", - "core.bonus.UNDEAD.description": "Quái là thây ma", - "core.bonus.UNLIMITED_RETALIATIONS.name": "Phản công vô hạn", - "core.bonus.UNLIMITED_RETALIATIONS.description": "Không giới hạn số lần phản công", - "core.bonus.WATER_IMMUNITY.name": "Kháng Nước", - "core.bonus.WATER_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Nước", - "core.bonus.WIDE_BREATH.name": "Hơi thở sâu", - "core.bonus.WIDE_BREATH.description": "Tấn công nhiều ô" + "vcmi.tavernWindow.inviteHero" : "Mới thêm tướng", + + "vcmi.commanderWindow.artifactMessage" : "Bạn có muốn đưa báu vật này cho tướng không?", + + "vcmi.creatureWindow.showBonuses.hover" : "Chuyển sang chế độ xem phần thưởng", + "vcmi.creatureWindow.showBonuses.help" : "Hiển thị tất cả các phần thưởng của chỉ huy.", + "vcmi.creatureWindow.showSkills.hover" : "Chuyển sang chế độ xem kỹ năng", + "vcmi.creatureWindow.showSkills.help" : "Hiển thị tất cả các kỹ năng đã học của chỉ huy.", + "vcmi.creatureWindow.returnArtifact.hover" : "Trả lại báu vật", + "vcmi.creatureWindow.returnArtifact.help" : "Bấm vào nút này để trả lại báu vật về ba lô của tướng.", + + "vcmi.questLog.hideComplete.hover" : "Ẩn nhiệm vụ đã hoàn thành", + "vcmi.questLog.hideComplete.help" : "Ẩn tất cả các nhiệm vụ đã hoàn thành.", + + "vcmi.randomMapTab.widgets.randomTemplate" : "(Random)", + "vcmi.randomMapTab.widgets.templateLabel" : "Mẫu có sẵn", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Cài đặt...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Chọn đội", + "vcmi.randomMapTab.widgets.roadTypesLabel" : "Loại đường", + + "vcmi.optionsTab.turnOptions.hover" : "Tùy chọn lượt", + "vcmi.optionsTab.turnOptions.help" : "Cài đặt tùy chọn hẹn giờ và lượt đi cùng lúc", + + "vcmi.optionsTab.chessFieldBase.hover" : "Thời gian thêm", + "vcmi.optionsTab.chessFieldTurn.hover" : "Thời gian lượt", + "vcmi.optionsTab.chessFieldBattle.hover" : "Thời gian trận đấu", + "vcmi.optionsTab.chessFieldUnit.hover" : "Thời gian quân", + "vcmi.optionsTab.chessFieldBase.help" : "Dùng khi {Thời gian lượt} giảm về 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm về 0 thì lượt của người chơi sẽ kết thúc.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Dùng trên bản đồ hoặc khi {Thời gian thêm} hết. Được đặt lại ở mỗi lượt. Còn thời gian cộng vào {Thời gian thêm} khi kết thúc lượt.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Dùng trên bản đồ hoặc khi {Thời gian chiến đấu} hết. Được đặt lại ở mỗi lượt. Thời gian chưa sử dụng sẽ bị mất.", + "vcmi.optionsTab.chessFieldBattle.help" : "Dùng trong trận chiến với AI hoặc trong trận đấu pvp khi {Thời gian quân} hết. Đặt lại sau mỗi trận.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Dùng khi chọn hành động cho quân trong trận đấu PvP. Còn thời gian cộng vào {Thời gian trận đấu} khi kết thúc lượt của quân.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Dùng khi chọn hành động cho quân trong trận đấu PvP. Đặt lại khi bắt đầu lượt của mỗi đơn vị. Thời gian chưa sử dụng sẽ bị mất.", + + "vcmi.optionsTab.accumulate" : "Tích lũy", + + "vcmi.optionsTab.simturnsTitle" : "Lượt đi cùng lúc", + "vcmi.optionsTab.simturnsMin.hover" : "Tối thiểu là", + "vcmi.optionsTab.simturnsMax.hover" : "Tối đa là", + "vcmi.optionsTab.simturnsAI.hover" : "(Thử nghiệm) Lượt chơi AI cùng lúc", + "vcmi.optionsTab.simturnsMin.help" : "Chơi cùng lượt trong số ngày được chỉ định. Cấm các hành vi thù địch giữa những người chơi trong thời gian này.", + "vcmi.optionsTab.simturnsMax.help" : "Chơi cùng lượt trong số ngày được chỉ định hoặc cho đến khi thực hiện hành vi thù địch với người chơi khác.", + "vcmi.optionsTab.simturnsAI.help" : "{Lượt đi của AI cùng lúc}\nTùy chọn Thử nghiệm. AI sẽ thực hiện các hành động cùng lúc với người chơi khi bật chế độ lượt đi cùng lúc.", + + "vcmi.optionsTab.turnTime.select" : "Chọn cài đặt giờ cho lượt", + "vcmi.optionsTab.turnTime.unlimited" : "Không giới hạn thời gian", + "vcmi.optionsTab.turnTime.classic.1" : "Thời gian: 1 phút", + "vcmi.optionsTab.turnTime.classic.2" : "Thời gian: 2 phút", + "vcmi.optionsTab.turnTime.classic.5" : "Thời gian: 5 phút", + "vcmi.optionsTab.turnTime.classic.10" : "Thời gian: 10 phút", + "vcmi.optionsTab.turnTime.classic.20" : "Thời gian: 20 phút", + "vcmi.optionsTab.turnTime.classic.30" : "Thời gian: 30 phút", + "vcmi.optionsTab.turnTime.chess.20" : "Time: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Time: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Time: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Time: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Time: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Time: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Cài đặt giờ cho lượt đi cùng lúc", + "vcmi.optionsTab.simturns.none" : "Không có lượt đi cùng lúc", + "vcmi.optionsTab.simturns.tillContactMax" : "Turns: Cho đến khi xâm chiếm", + "vcmi.optionsTab.simturns.tillContact1" : "Turns: 1 tuần, dừng khi xâm chiếm", + "vcmi.optionsTab.simturns.tillContact2" : "Turns: 2 tuần, dừng khi xâm chiếm", + "vcmi.optionsTab.simturns.tillContact4" : "Turns: 1 tháng, dừng khi xâm chiếm", + "vcmi.optionsTab.simturns.blocked1" : "Turns: 1 tuần, cấm xâm chiếm", + "vcmi.optionsTab.simturns.blocked2" : "Turns: 2 tuần, cấm xâm chiếm", + "vcmi.optionsTab.simturns.blocked4" : "Turns: 1 tháng, cấm xâm chiếm", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : " %d ngày", + "vcmi.optionsTab.simturns.days.1" : " %d ngày", + "vcmi.optionsTab.simturns.days.2" : " %d ngày", + "vcmi.optionsTab.simturns.weeks.0" : " %d tuần", + "vcmi.optionsTab.simturns.weeks.1" : " %d tuần", + "vcmi.optionsTab.simturns.weeks.2" : " %d tuần", + "vcmi.optionsTab.simturns.months.0" : " %d tháng", + "vcmi.optionsTab.simturns.months.1" : " %d tháng", + "vcmi.optionsTab.simturns.months.2" : " %d tháng", + + "vcmi.optionsTab.extraOptions.hover" : "Tùy chọn mở rộng", + "vcmi.optionsTab.extraOptions.help" : "Mở thêm cài đặt bổ sung trong trò chơi", + + "vcmi.optionsTab.cheatAllowed.hover" : "Cho phép sử dụng mã gian lận", + "vcmi.optionsTab.unlimitedReplay.hover" : "Xem lại trận đấu không giới hạn", + "vcmi.optionsTab.cheatAllowed.help" : "{Dùng mã gian lận}\nCho phép nhập các mã gian lận trong trò chơi.", + "vcmi.optionsTab.unlimitedReplay.help" : "{Xem lại trận đấu}\nKhông giới hạn số lần xem lại trận đấu.", + + // Custom victory conditions for H3 campaigns and HotA maps + "vcmi.map.victoryCondition.daysPassed.toOthers" : "Đối thủ đã xoay xở để sinh tồn đến ngày hôm này. Họ giành chiến thắng!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Đối thủ đã tiêu diệt tất cả quái vật gây hại cho vùng đất này và họ giành chiến thắng!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Chúc mừng! Bạn đã đánh bại tất cả quái vật gây hại cho vùng đất này và giành chiến thắng!", + "vcmi.map.victoryCondition.collectArtifacts.message" : "Thu thập ba báu vật", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Chúc mừng bạn có đủ ba báu vật! Tất cả đối thủ đã bị đánh bại và bạn có Angelic Alliance! Chiến thắng thuộc về bạn!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Đánh bại tất cả đối thủ và hớp nhất Angelic Alliance", + "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Tiêu rồi, bạn đã để mất một phần của Angelic Alliance! Mọi thứ đã kết thúc!", + + // few strings from WoG used by vcmi + "vcmi.stackExperience.description" : "» Kinh Nghiệm Của Đạo Quân «\n\nTên loại quân ................... : %s\nXếp hạng kinh nghiệm ................. : %s (%i)\nĐiểm kinh nghiệm ............... : %i\nĐiểm kinh nghiệm để lên hạng .. : %i\nKinh nghiệm tối đa mỗi trận đánh ... : %i%% (%i)\nSố quân hiện tại của đạo .... : %i\nChiêu mộ tối đa mới\n không bị giảm cấp .... : %i\nHệ số kinh nghiệm ........... : %.2f\nHệ số nâng cấp .............. : %.2f\nKinh nghiệm sau hạng 10 ........ : %i\nChiêu mộ mới tối đa còn lại ở hạng 10: %i", + "vcmi.stackExperience.rank.0" : "Lính Mới", + "vcmi.stackExperience.rank.1" : "Tập Sự", + "vcmi.stackExperience.rank.2" : "Lành Nghề", + "vcmi.stackExperience.rank.3" : "Khéo Léo", + "vcmi.stackExperience.rank.4" : "Thông Thạo", + "vcmi.stackExperience.rank.5" : "Kì Cựu", + "vcmi.stackExperience.rank.6" : "Lão Luyện", + "vcmi.stackExperience.rank.7" : "Chuyên Gia", + "vcmi.stackExperience.rank.8" : "Tinh Hoa", + "vcmi.stackExperience.rank.9" : "Bậc Thầy", + "vcmi.stackExperience.rank.10" : "Thiên Tài", + + // Strings for HotA Seer Hut / Quest Guards + "core.seerhut.quest.heroClass.complete.0" : "Bạn đúng là %s. Đây là món quà tôi dành cho bạn. Bạn có chấp nhận không?", + "core.seerhut.quest.heroClass.complete.1" : "Bạn đúng là %s. Đây là món quà tôi dành cho bạn. Bạn có chấp nhận không?", + "core.seerhut.quest.heroClass.complete.2" : "Bạn đúng là %s. Đây là món quà tôi dành cho bạn. Bạn có chấp nhận không?", + "core.seerhut.quest.heroClass.complete.3" : "Những lính gác nhận ra bạn là %s. Bạn có muốn đi qua ngay bây giờ không?", + "core.seerhut.quest.heroClass.complete.4" : "Những lính gác nhận ra bạn là %s. Bạn có muốn đi qua ngay bây giờ không?", + "core.seerhut.quest.heroClass.complete.5" : "Những lính gác nhận ra bạn là %s. Bạn có muốn đi qua ngay bây giờ không?", + "core.seerhut.quest.heroClass.description.0" : "Tìm %s cho %s", + "core.seerhut.quest.heroClass.description.1" : "Tìm %s cho %s", + "core.seerhut.quest.heroClass.description.2" : "Tìm %s cho %s", + "core.seerhut.quest.heroClass.description.3" : "Tìm %s để mở cổng", + "core.seerhut.quest.heroClass.description.4" : "Tìm %s để mở cổng", + "core.seerhut.quest.heroClass.description.5" : "Tìm %s để mở cổng", + "core.seerhut.quest.heroClass.hover.0" : "(tìm tướng thuộc nhóm %s)", + "core.seerhut.quest.heroClass.hover.1" : "(tìm tướng thuộc nhóm %s)", + "core.seerhut.quest.heroClass.hover.2" : "(tìm tướng thuộc nhóm %s)", + "core.seerhut.quest.heroClass.hover.3" : "(tìm tướng thuộc nhóm %s)", + "core.seerhut.quest.heroClass.hover.4" : "(tìm tướng thuộc nhóm %s)", + "core.seerhut.quest.heroClass.hover.5" : "(tìm tướng thuộc nhóm %s)", + "core.seerhut.quest.heroClass.receive.0" : "Tôi có một món quà cho %s.", + "core.seerhut.quest.heroClass.receive.1" : "Tôi có một món quà cho %s.", + "core.seerhut.quest.heroClass.receive.2" : "Tôi có một món quà cho %s.", + "core.seerhut.quest.heroClass.receive.3" : "Những người lính gác ở đây nói rằng họ chỉ cho %s đi qua.", + "core.seerhut.quest.heroClass.receive.4" : "Những người lính gác ở đây nói rằng họ chỉ cho %s đi qua.", + "core.seerhut.quest.heroClass.receive.5" : "Những người lính gác ở đây nói rằng họ chỉ cho %s đi qua.", + "core.seerhut.quest.heroClass.visit.0" : "Bạn không phải là %s. Tôi không có gì cho bạn. Cút đi!", + "core.seerhut.quest.heroClass.visit.1" : "Bạn không phải là %s. Tôi không có gì cho bạn. Cút đi!", + "core.seerhut.quest.heroClass.visit.2" : "Bạn không phải là %s. Tôi không có gì cho bạn. Cút đi!", + "core.seerhut.quest.heroClass.visit.3" : "Những người lính gác ở đây chỉ cho %s đi qua.", + "core.seerhut.quest.heroClass.visit.4" : "Những người lính gác ở đây chỉ cho %s đi qua.", + "core.seerhut.quest.heroClass.visit.5" : "Những người lính gác ở đây chỉ cho %s đi qua.", + + "core.seerhut.quest.reachDate.complete.0" : "Bây giờ tôi đang rảnh. Đây là thứ tôi có cho bạn. Bạn có muốn nhận nó không?", + "core.seerhut.quest.reachDate.complete.1" : "Bây giờ tôi đang rảnh. Đây là thứ tôi có cho bạn. Bạn có muốn nhận nó không?", + "core.seerhut.quest.reachDate.complete.2" : "Bây giờ tôi đang rảnh. Đây là thứ tôi có cho bạn. Bạn có muốn nhận nó không?", + "core.seerhut.quest.reachDate.complete.3" : "Bạn được phép đi qua ngay bây giờ. Bạn có muốn đi qua không?", + "core.seerhut.quest.reachDate.complete.4" : "Bạn được phép đi qua ngay bây giờ. Bạn có muốn đi qua không?", + "core.seerhut.quest.reachDate.complete.5" : "Bạn được phép đi qua ngay bây giờ. Bạn có muốn đi qua không?", + "core.seerhut.quest.reachDate.description.0" : "Đợi đến %s %s", + "core.seerhut.quest.reachDate.description.1" : "Đợi đến %s %s", + "core.seerhut.quest.reachDate.description.2" : "Đợi đến %s %s", + "core.seerhut.quest.reachDate.description.3" : "Đợi đến %s để mở cổng", + "core.seerhut.quest.reachDate.description.4" : "Đợi đến %s để mở cổng", + "core.seerhut.quest.reachDate.description.5" : "Đợi đến %s để mở cổng", + "core.seerhut.quest.reachDate.hover.0" : "(Hãy quay lại đây vào %s)", + "core.seerhut.quest.reachDate.hover.1" : "(Hãy quay lại đây vào %s)", + "core.seerhut.quest.reachDate.hover.2" : "(Hãy quay lại đây vào %s)", + "core.seerhut.quest.reachDate.hover.3" : "(Hãy quay lại đây vào %s)", + "core.seerhut.quest.reachDate.hover.4" : "(Hãy quay lại đây vào %s)", + "core.seerhut.quest.reachDate.hover.5" : "(Hãy quay lại đây vào %s)", + "core.seerhut.quest.reachDate.receive.0" : "Tôi đang bận. Hãy quay lại đây vào %s", + "core.seerhut.quest.reachDate.receive.1" : "Tôi đang bận. Hãy quay lại đây vào %s", + "core.seerhut.quest.reachDate.receive.2" : "Tôi đang bận. Hãy quay lại đây vào %s", + "core.seerhut.quest.reachDate.receive.3" : "Đóng cổng đến %s.", + "core.seerhut.quest.reachDate.receive.4" : "Đóng cổng đến %s.", + "core.seerhut.quest.reachDate.receive.5" : "Đóng cổng đến %s.", + "core.seerhut.quest.reachDate.visit.0" : "Tôi đang bận. Hãy quay lại đây vào %s.", + "core.seerhut.quest.reachDate.visit.1" : "Tôi đang bận. Hãy quay lại đây vào %s.", + "core.seerhut.quest.reachDate.visit.2" : "Tôi đang bận. Hãy quay lại đây vào %s.", + "core.seerhut.quest.reachDate.visit.3" : "Đóng cổng đến %s.", + "core.seerhut.quest.reachDate.visit.4" : "Đóng cổng đến %s.", + "core.seerhut.quest.reachDate.visit.5" : "Đóng cổng đến %s.", + + "mapObject.core.hillFort.object.description" : "Nâng cấp quân cấp 1 - 4 với chi phí thấp hơn ở trong thành.", + + "core.bonus.ADDITIONAL_ATTACK.name": "Đánh 2 lần", + "core.bonus.ADDITIONAL_ATTACK.description": "Tấn công hai lần", + "core.bonus.ADDITIONAL_RETALIATION.name": "Phản đòn thêm", + "core.bonus.ADDITIONAL_RETALIATION.description": "Có thể phàn đòn thêm ${val} lần", + "core.bonus.AIR_IMMUNITY.name": "Kháng gió", + "core.bonus.AIR_IMMUNITY.description": "Kháng tất cả phép gió trong trường học phép thuật", + "core.bonus.ATTACKS_ALL_ADJACENT.name": "Đánh xung quanh", + "core.bonus.ATTACKS_ALL_ADJACENT.description": "Tấn công tất cả kẻ địch xung quanh", + "core.bonus.BLOCKS_RETALIATION.name": "Không bị phản đòn", + "core.bonus.BLOCKS_RETALIATION.description": "Kẻ địch không thể phản đòn", + "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Bắn không phản", + "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Kẻ địch không thể bắn phản đòn", + "core.bonus.CATAPULT.name": "Công thành", + "core.bonus.CATAPULT.description": "Tấn công tường thành", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Giảm (${val}) năng lượng", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Giảm ${val} năng lượng khi tướng dùng phép", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tốn thêm (${val}) năng lượng", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Tướng địch dùng phép tốn thêm ${val} năng lượng", + "core.bonus.CHARGE_IMMUNITY.name": "Bộ binh", + "core.bonus.CHARGE_IMMUNITY.description": "Kháng lại kỹ năng đặc biệt của Cavalier và Champion", + "core.bonus.DARKNESS.name": "Bóng tối che phủ", + "core.bonus.DARKNESS.description": "Tạo ra bóng tối với bán kính ${val}", + "core.bonus.DEATH_STARE.name": "Ánh mắt tử thần (${val}%)", + "core.bonus.DEATH_STARE.description": "Có ${val}% cơ hội giết chết kẻ địch", + "core.bonus.DEFENSIVE_STANCE.name": "Thưởng phòng thủ", + "core.bonus.DEFENSIVE_STANCE.description": "+${val} Phòng thủ khi khi đang thế thủ", + "core.bonus.DESTRUCTION.name": "Đòn tận diệt", + "core.bonus.DESTRUCTION.description": "Có ${val}% cơ hội giết thêm kẻ địch sau khi đánh", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Đòn chí mạng", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Có ${val}% cơ hội gây sát thương gấp đôi khi đánh", + "core.bonus.DRAGON_NATURE.name": "Rồng", + "core.bonus.DRAGON_NATURE.description": "Quân có thuộc tính Rồng", + "core.bonus.EARTH_IMMUNITY.name": "Kháng đất", + "core.bonus.EARTH_IMMUNITY.description": "Kháng tất cả phép đất trong trường học phép thuật", + "core.bonus.ENCHANTER.name": "Niệm phép", + "core.bonus.ENCHANTER.description": "Có thể dùng phép mass ${subtype.spell} mỗi lượt", + "core.bonus.ENCHANTED.name": "Niệm phép", + "core.bonus.ENCHANTED.description": "Bị ảnh hưởng vĩnh viễn bởi phép ${subtype.spell}", + "core.bonus.ENEMY_ATTACK_REDUCTION.name": "Giảm tấn công (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "Khi bị tấn công, giảm ${val}% tấn công của kẻ địch", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Giảm phòng thủ (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Khi tấn công, giảm ${val}% phòng thủ của kẻ địch", + "core.bonus.FIRE_IMMUNITY.name": "Kháng lửa", + "core.bonus.FIRE_IMMUNITY.description": "Kháng tất cả phép lửa trong trường học phép thuật", + "core.bonus.FIRE_SHIELD.name": "Khiên lửa (${val}%)", + "core.bonus.FIRE_SHIELD.description": "Phản lại một phần sát thương khi cận chiến", + "core.bonus.FIRST_STRIKE.name": "Đòn đánh phủ đầu", + "core.bonus.FIRST_STRIKE.description": "Đạo quân này phản đòn trước khi bị tấn công", + "core.bonus.FEAR.name": "Sợ hãi", + "core.bonus.FEAR.description": "Gây sợ hãi cho một đạo quân địch", + "core.bonus.FEARLESS.name": "Không sợ", + "core.bonus.FEARLESS.description": "Kháng lại kỹ năng gây sợ hãi", + "core.bonus.FEROCITY.name": "Hung ác", + "core.bonus.FEROCITY.description": "Tấn công thêm ${val} lần nữa nếu giết chết kẻ địch", + "core.bonus.FLYING.name": "Bay", + "core.bonus.FLYING.description": "Bay khi di chuyển (vượt chướng ngại vật)", + "core.bonus.FREE_SHOOTING.name": "Bắn gần", + "core.bonus.FREE_SHOOTING.description": "Có thể bắn khi cận chiến", + "core.bonus.GARGOYLE.name": "Gargoyle", + "core.bonus.GARGOYLE.description": "Không thể hồi sinh hoặc chữa lành", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Giảm (${val}%) sát thương", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Giảm sát thương vật lý từ các đòn đánh xa hoặc cận chiến", + "core.bonus.HATE.name": "Ghét ${subtype.creature}", + "core.bonus.HATE.description": "Gây thêm ${val}% sát thương cho ${subtype.creature}", + "core.bonus.HEALER.name": "Hồi máu", + "core.bonus.HEALER.description": "Hồi máu cho các đơn vị đồng minh", + "core.bonus.HP_REGENERATION.name": "Tự hồi máu", + "core.bonus.HP_REGENERATION.description": "Tự hồi ${val} máu mỗi lượt", + "core.bonus.JOUSTING.name": "Kị binh", + "core.bonus.JOUSTING.description": "Tăng +${val}% sát thương theo bước đi qua mỗi ô lục giác", + "core.bonus.KING.name": "King", + "core.bonus.KING.description": "Dễ bị kích sát ở cấp ${val} hoặc cao hơn", + "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Kháng phép 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Kháng phép cấp 1-${val}", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Hạn chế tầm bắn", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Không thể bắn các mục tiêu xa hơn ${val} ô", + "core.bonus.LIFE_DRAIN.name": "Hút sinh lực (${val}%)", + "core.bonus.LIFE_DRAIN.description": "Hút ${val}% sát thương gây ra chuyển thành sinh lực", + "core.bonus.MANA_CHANNELING.name": "Chuyển năng lượng ${val}%", + "core.bonus.MANA_CHANNELING.description": "Cho tướng của bạn ${val}% năng lượng khi tướng địch sử dụng", + "core.bonus.MANA_DRAIN.name": "Hút năng lượng", + "core.bonus.MANA_DRAIN.description": "Hút ${val} năng lượng mỗi lượt", + "core.bonus.MAGIC_MIRROR.name": "Gương ma thuật (${val}%)", + "core.bonus.MAGIC_MIRROR.description": "Có ${val}% cơ hội phản lại một đòn phép sang một đơn vị quân địch", + "core.bonus.MAGIC_RESISTANCE.name": "Kháng phép (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "Có ${val}% cơ hội kháng lại một phép của kẻ địch", + "core.bonus.MIND_IMMUNITY.name": "Kháng phép tâm trí", + "core.bonus.MIND_IMMUNITY.description": "Kháng tất cả các phép tâm trí", + "core.bonus.NO_DISTANCE_PENALTY.name": "Bắn xa", + "core.bonus.NO_DISTANCE_PENALTY.description": "Gây ra sát thương tối đa ở mọi khoảng cách", + "core.bonus.NO_MELEE_PENALTY.name": "Cận chiến", + "core.bonus.NO_MELEE_PENALTY.description": "Quân không bị giảm sát thương khi cận chiến", + "core.bonus.NO_MORALE.name": "Nhuệ khí", + "core.bonus.NO_MORALE.description": "Quân không bị ảnh hưởng bởi nhuệ khí", + "core.bonus.NO_WALL_PENALTY.name": "Bắn xuyên tường", + "core.bonus.NO_WALL_PENALTY.description": "Gây ra sát thương tối đa khi công thành", + "core.bonus.NON_LIVING.name": "Non living", + "core.bonus.NON_LIVING.description": "Miễn nhiễm với nhiều hiệu ứng", + "core.bonus.RANDOM_SPELLCASTER.name": "Dùng phép ngẫu nhiên", + "core.bonus.RANDOM_SPELLCASTER.description": "Có thể dùng phép ngẫu nhiên trong trận đấu", + "core.bonus.RANGED_RETALIATION.name": "Phản đòn từ xa", + "core.bonus.RANGED_RETALIATION.description": "Có thể phản đòn từ xa khi bị bắn", + "core.bonus.RECEPTIVE.name": "Hấp thụ", + "core.bonus.RECEPTIVE.description": "Hập thụ các phép thuật thân thiện", + "core.bonus.REBIRTH.name": "Tái sinh (${val}%)", + "core.bonus.REBIRTH.description": "${val}% số quân sẽ hồi sinh sau khi chết", + "core.bonus.RETURN_AFTER_STRIKE.name": "Đánh và Trở lại", + "core.bonus.RETURN_AFTER_STRIKE.description": "Quay trở lại vị trí ban đầu sau khi đánh", + "core.bonus.REVENGE.name": "Trả thù", + "core.bonus.REVENGE.description": "Gây thêm sát thương theo lượng máu của kẻ địch bị mất", + "core.bonus.SHOOTER.name": "Bắn xa", + "core.bonus.SHOOTER.description": "Quân có thể bắn từ xa", + "core.bonus.SHOOTS_ALL_ADJACENT.name": "Bắn xung quanh", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "Bắn tất cả các mục tiêu trong một khu vực nhỏ", + "core.bonus.SOUL_STEAL.name": "Bắt linh hồn", + "core.bonus.SOUL_STEAL.description": "Tăng thêm ${val} quân mới với mỗi quân địch bị giết", + "core.bonus.SPELLCASTER.name": "Dùng phép có ích", + "core.bonus.SPELLCASTER.description": "Có thể dùng phép ${subtype.spell}", + "core.bonus.SPELL_AFTER_ATTACK.name": "Dùng phép trước", + "core.bonus.SPELL_AFTER_ATTACK.description": "Có ${val}% cơ hội dùng phép ${subtype.spell} trước khi đánh", + "core.bonus.SPELL_BEFORE_ATTACK.name": "Dùng phép sau", + "core.bonus.SPELL_BEFORE_ATTACK.description": "Có ${val}% cơ hội dùng phép ${subtype.spell} sau khi đánh", + "core.bonus.SPELL_IMMUNITY.name": "Kháng phép", + "core.bonus.SPELL_IMMUNITY.description": "Kháng phép ${subtype.spell}", + "core.bonus.SPELL_LIKE_ATTACK.name": "Đánh bằng phép", + "core.bonus.SPELL_LIKE_ATTACK.description": "Tấn công bằng phép ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name": "Hào quang kháng phép", + "core.bonus.SPELL_RESISTANCE_AURA.description": "Quân ở gần sẽ nhận được ${val}% kháng phép", + "core.bonus.SUMMON_GUARDIANS.name": "Gọi bảo vệ", + "core.bonus.SUMMON_GUARDIANS.description": "Khi bắt đầu trận sẽ triệu hồi ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name": "Hợp lực", + "core.bonus.SYNERGY_TARGET.description": "Quân này dễ bị ảnh hưởng bởi nhiều hiệu ứng", + "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Đánh hai ô", + "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Tấn công bằng hơi thở (xuyên 2 ô)", + "core.bonus.THREE_HEADED_ATTACK.name": "Ba đầu", + "core.bonus.THREE_HEADED_ATTACK.description": "Tấn công cả quân liền kề mục tiêu", + "core.bonus.TRANSMUTATION.name": "Biến đổi", + "core.bonus.TRANSMUTATION.description": "Có ${val}% cơ hội biến đổi quân mục tiêu thành dạng khác", + "core.bonus.UNDEAD.name": "Âm binh", + "core.bonus.UNDEAD.description": "Quân là âm binh", + "core.bonus.UNLIMITED_RETALIATIONS.name": "Phản đòn vô hạn", + "core.bonus.UNLIMITED_RETALIATIONS.description": "Có thể phản đòn không giới hạn", + "core.bonus.WATER_IMMUNITY.name": "Kháng nước", + "core.bonus.WATER_IMMUNITY.description": "Kháng tất cả phép nước trong trường học phép thuật", + "core.bonus.WIDE_BREATH.name": "Đánh nhiều ô", + "core.bonus.WIDE_BREATH.description": "Tấn công bằng hơi thở (xuyên nhiều ô)", + "core.bonus.DISINTEGRATE.name": "Hủy xác", + "core.bonus.DISINTEGRATE.description": "Xác chết biến mất sau khi bị giết", + "core.bonus.INVINCIBLE.name": "Bất bại", + "core.bonus.INVINCIBLE.description": "Miễn nhiễm với mọi hiệu ứng", + "core.bonus.MECHANICAL.name": "Máy", + "core.bonus.MECHANICAL.description": "Miễn nhiễm với nhiều hiệu ứng, có thể sửa chữa", + "core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Đánh ba hướng", + "core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Tấn công bằng hơi thở (ra ba hướng)", + "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Kháng phép", + "core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Kháng phép gió", + "core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Kháng phép lửa", + "core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Kháng phép nước", + "core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Kháng phép đất", + "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Giảm ${val}% sát thương của tất cả phép thuật.", + "core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Giảm ${val}% sát thương từ phép gió.", + "core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Giảm ${val}% sát thương từ phép lửa.", + "core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Giảm ${val}% sát thương từ phép nước.", + "core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Giảm ${val}% sát thương từ phép đất.", + "core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Kháng phép", + "core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Kháng phép gió", + "core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Kháng phép lửa", + "core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Kháng phép nước", + "core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Kháng phép đất", + "core.bonus.SPELL_SCHOOL_IMMUNITY.description": "Đơn vị này kháng tất cả phép thuật", + "core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "Đơn vị này kháng tất cả phép gió", + "core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "Đơn vị này kháng tất cả phép lửa", + "core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "Đơn vị này kháng tất cả phép nước", + "core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "Đơn vị này kháng tất cả phép đất", + "core.bonus.OPENING_BATTLE_SPELL.name": "Khởi đầu với phép", + "core.bonus.OPENING_BATTLE_SPELL.description": "Dùng phép ${subtype.spell} khi bắt đầu trận chiến" } diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index 0d1d6b65d..89aaa9301 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -111,9 +111,9 @@ }, "vietnamese": { - "name": "VCMI essential files", + "name": "VCMI - Tập tin chính", "description": "Các tập tin cần thiết để chạy VCMI", - "author": "Vũ Đắc Hoàng Ân", + "author": "Hunter8x - Bé Còi", "skipValidation": true, "translations": [ "config/vietnamese.json" diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index cf1c896a1..9b6482b47 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -821,7 +821,7 @@ void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); PlayerColor playerToCall; //pack.player that will move activated stack - if(activated->hasBonusOfType(BonusType::HYPNOTIZED)) + if(activated->isHypnotized()) { playerToCall = gs.getBattle(pack.battleID)->getSide(BattleSide::ATTACKER).color == activated->unitOwner() ? gs.getBattle(pack.battleID)->getSide(BattleSide::DEFENDER).color diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index f3702101c..609d204e0 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -76,7 +76,13 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode) { OBJECT_CONSTRUCTION; - background = std::make_shared(ImagePath::fromJson(config["background"])); + const auto& bgConfig = config["background"]; + if (bgConfig.isVector() && !bgConfig.Vector().empty()) + background = std::make_shared(ImagePath::fromJson(*RandomGeneratorUtil::nextItem(bgConfig.Vector(), CRandomGenerator::getDefault()))); + + if (bgConfig.isString()) + background = std::make_shared(ImagePath::fromJson(bgConfig)); + if(config["scalable"].Bool()) background->scaleTo(GH.screenDimensions()); diff --git a/client/mapView/MapOverlayLogVisualizer.cpp b/client/mapView/MapOverlayLogVisualizer.cpp index 7e1678230..f75a1cf91 100644 --- a/client/mapView/MapOverlayLogVisualizer.cpp +++ b/client/mapView/MapOverlayLogVisualizer.cpp @@ -17,6 +17,7 @@ #include "../render/Colors.h" #include "../render/EFont.h" #include "../render/IFont.h" +#include "../render/IScreenHandler.h" #include "../render/IRenderHandler.h" #include "../render/Graphics.h" #include "../gui/TextAlignment.h" @@ -30,24 +31,20 @@ MapOverlayLogVisualizer::MapOverlayLogVisualizer(Canvas & target, std::shared_pt void MapOverlayLogVisualizer::drawLine(int3 start, int3 end) { - const Point offset = Point(30, 30); - auto level = model->getLevel(); if(start.z != level || end.z != level) return; - auto pStart = model->getTargetTileArea(start).topLeft(); - auto pEnd = model->getTargetTileArea(end).topLeft(); - auto viewPort = target.getRenderArea(); + int scaling = GH.screenHandler().getScalingFactor(); + auto pStart = model->getTargetTileArea(start).center(); + auto pEnd = model->getTargetTileArea(end).center(); + Rect viewPortRaw = target.getRenderArea(); + Rect viewPort(viewPortRaw.topLeft() / scaling, viewPortRaw.dimensions() / scaling ); - pStart.x += 3; - pEnd.x -= 3; + Point workaroundOffset(8,8); // not sure why it is needed. Removing leads to incorrect clipping near view edges - pStart += offset; - pEnd += offset; - - if(viewPort.isInside(pStart) && viewPort.isInside(pEnd)) + if(viewPort.isInside(pStart + workaroundOffset) && viewPort.isInside(pEnd + workaroundOffset)) { target.drawLine(pStart, pEnd, ColorRGBA(255, 255, 0), ColorRGBA(255, 0, 0)); } diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 522cca227..3c2cccf84 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -65,7 +65,7 @@ void BasicMapView::render(Canvas & target, bool fullUpdate) tilesCache->update(controller->getContext()); tilesCache->render(controller->getContext(), targetClipped, fullUpdate); - MapOverlayLogVisualizer r(target, model); + MapOverlayLogVisualizer r(targetClipped, model); logVisual->visualize(r); } diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 8ba26d5e1..84edc6943 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -59,8 +59,18 @@ static si64 lodSeek(void * opaque, si64 pos, int whence) return data->seek(pos); } +static void logFFmpegError(int errorCode) +{ + std::array errorMessage{}; + av_strerror(errorCode, errorMessage.data(), errorMessage.size()); + + logGlobal->warn("Failed to open video file! Reason: %s", errorMessage.data()); +} + [[noreturn]] static void throwFFmpegError(int errorCode) { + logFFmpegError(errorCode); + std::array errorMessage{}; av_strerror(errorCode, errorMessage.data(), errorMessage.size()); @@ -95,7 +105,7 @@ bool FFMpegStream::openInput(const VideoPath & videoToOpen) return input != nullptr; } -void FFMpegStream::openContext() +bool FFMpegStream::openContext() { static const int BUFFER_SIZE = 4096; input->seek(0); @@ -109,13 +119,21 @@ void FFMpegStream::openContext() int avfopen = avformat_open_input(&formatContext, "dummyFilename", nullptr, nullptr); if(avfopen != 0) - throwFFmpegError(avfopen); + { + logFFmpegError(avfopen); + return false; + } // Retrieve stream information int findStreamInfo = avformat_find_stream_info(formatContext, nullptr); if(avfopen < 0) - throwFFmpegError(findStreamInfo); + { + logFFmpegError(findStreamInfo); + return false; + } + + return true; } void FFMpegStream::openCodec(int desiredStreamIndex) @@ -169,10 +187,13 @@ const AVFrame * FFMpegStream::getCurrentFrame() const return frame; } -void CVideoInstance::openVideo() +bool CVideoInstance::openVideo() { - openContext(); + if (!openContext()) + return false; + openCodec(findVideoStream()); + return true; } void CVideoInstance::prepareOutput(float scaleFactor, bool useTextureOutput) @@ -526,7 +547,9 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide { if (!openInput(videoToOpen)) return { nullptr, 0}; - openContext(); + + if (!openContext()) + return { nullptr, 0}; int audioStreamIndex = findAudioStream(); if (audioStreamIndex == -1) @@ -653,7 +676,9 @@ std::unique_ptr CVideoPlayer::open(const VideoPath & name, float if (!result->openInput(name)) return nullptr; - result->openVideo(); + if (!result->openVideo()) + return nullptr; + result->prepareOutput(scaleFactor, false); result->loadNextFrame(); // prepare 1st frame diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index 5047ba01a..a4229074b 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -42,7 +42,7 @@ class FFMpegStream : boost::noncopyable AVFrame * frame = nullptr; protected: - void openContext(); + bool openContext(); void openCodec(int streamIndex); int findVideoStream() const; @@ -91,7 +91,7 @@ public: CVideoInstance(); ~CVideoInstance(); - void openVideo(); + bool openVideo(); bool loadNextFrame(); double timeStamp() final; diff --git a/client/renderSDL/CTrueTypeFont.cpp b/client/renderSDL/CTrueTypeFont.cpp index 94faf6502..311ee7d48 100644 --- a/client/renderSDL/CTrueTypeFont.cpp +++ b/client/renderSDL/CTrueTypeFont.cpp @@ -20,8 +20,6 @@ #include "../../lib/filesystem/Filesystem.h" #include "../../lib/texts/TextOperations.h" -#include - std::pair, ui64> CTrueTypeFont::loadData(const JsonNode & config) { std::string filename = "Data/" + config["file"].String(); diff --git a/client/renderSDL/CTrueTypeFont.h b/client/renderSDL/CTrueTypeFont.h index 87a9ac484..5062aa253 100644 --- a/client/renderSDL/CTrueTypeFont.h +++ b/client/renderSDL/CTrueTypeFont.h @@ -11,14 +11,14 @@ #include "../render/IFont.h" +#include + VCMI_LIB_NAMESPACE_BEGIN class JsonNode; VCMI_LIB_NAMESPACE_END class CBitmapFont; -using TTF_Font = struct _TTF_Font; - class CTrueTypeFont final : public IFont { const std::pair, ui64> data; diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index 1c47b3506..935ed8f4b 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -350,7 +350,7 @@ EUpscalingFilter ScreenHandler::loadUpscalingFilter() const }; auto filterName = settings["video"]["upscalingFilter"].String(); - auto filter = upscalingFilterTypes.at(filterName); + auto filter = upscalingFilterTypes.count(filterName) ? upscalingFilterTypes.at(filterName) : EUpscalingFilter::AUTO; if (filter != EUpscalingFilter::AUTO) return filter; diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 1c1f0bacf..590931c4f 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -19,9 +19,11 @@ #include "../render/IImage.h" #include "../renderSDL/RenderHandler.h" #include "../widgets/CComponentHolder.h" +#include "../widgets/Slider.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" #include "../widgets/GraphicalPrimitiveCanvas.h" +#include "../eventsSDL/InputHandler.h" #include "../../lib/IGameSettings.h" #include "../../lib/entities/hero/CHeroHandler.h" @@ -99,7 +101,9 @@ void CHeroOverview::genControls() // hero biography r = Rect(borderOffset, 5 * borderOffset + yOffset + 148, 284, 130); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); - labelHeroBiography = std::make_shared(r.resize(-borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIdx]->getBiographyTranslated()); + labelHeroBiography = std::make_shared((*CGI->heroh)[heroIdx]->getBiographyTranslated(), r.resize(-borderOffset), CSlider::EStyle::BROWN, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + if(labelHeroBiography->slider && GH.input().getCurrentInputMode() != InputMode::TOUCH) + labelHeroBiography->slider->clearScrollBounds(); // speciality name r = Rect(2 * borderOffset + 44, 6 * borderOffset + yOffset + 278, 235, 44); @@ -115,7 +119,9 @@ void CHeroOverview::genControls() // speciality description r = Rect(borderOffset, 7 * borderOffset + yOffset + 322, 284, 85); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); - labelSpecialityDescription = std::make_shared(r.resize(-borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIdx]->getSpecialtyDescriptionTranslated()); + labelSpecialityDescription = std::make_shared((*CGI->heroh)[heroIdx]->getSpecialtyDescriptionTranslated(), r.resize(-borderOffset), CSlider::EStyle::BROWN, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + if(labelSpecialityDescription->slider && GH.input().getCurrentInputMode() != InputMode::TOUCH) + labelSpecialityDescription->slider->clearScrollBounds(); // army title r = Rect(302, borderOffset + yOffset, 292, 30); diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index 38e5fa683..001b58594 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -39,7 +39,7 @@ class CHeroOverview : public CWindowObject std::shared_ptr labelTitle; std::shared_ptr imageHero; std::shared_ptr labelHeroName; - std::shared_ptr labelHeroBiography; + std::shared_ptr labelHeroBiography; std::shared_ptr labelHeroClass; std::shared_ptr labelHeroSpeciality; std::shared_ptr imageSpeciality; @@ -47,7 +47,7 @@ class CHeroOverview : public CWindowObject std::vector> imageSkill; std::vector> labelSkillFooter; std::shared_ptr labelSpecialityName; - std::shared_ptr labelSpecialityDescription; + std::shared_ptr labelSpecialityDescription; std::shared_ptr labelArmyTitle; std::vector> imageArmy; diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index ed7f09fe3..e92fe9362 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -28,6 +28,7 @@ #include "../../lib/texts/TextOperations.h" constexpr int RIGHT_CLICK_POPUP_MIN_SIZE = 100; +constexpr int RIGHT_CLICK_POPUP_MAX_HEIGHT_TEXTONLY = 450; constexpr int SIDE_MARGIN = 11; constexpr int TOP_MARGIN = 20; constexpr int BOTTOM_MARGIN = 16; @@ -260,9 +261,17 @@ void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor play if(ret->buttons.empty() && !ret->components) { // use more compact form for right-click popup with no buttons / components - - ret->pos.w = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->label->textSize.x + 2 * SIDE_MARGIN); - ret->pos.h = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->label->textSize.y + TOP_MARGIN + BOTTOM_MARGIN); + if(ret->text->slider) + { + ret->text->resize(Point(ret->text->pos.w, std::min(ret->text->label->textSize.y, RIGHT_CLICK_POPUP_MAX_HEIGHT_TEXTONLY))); + ret->pos.w = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->pos.w + 2 * SIDE_MARGIN); + ret->pos.h = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->pos.h + TOP_MARGIN + BOTTOM_MARGIN); + } + else + { + ret->pos.w = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->label->textSize.x + 2 * SIDE_MARGIN); + ret->pos.h = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->label->textSize.y + TOP_MARGIN + BOTTOM_MARGIN); + } } else { diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 71bb9dd0a..7283a1968 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -31,12 +31,13 @@ #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" -#include "../ConditionalWait.h" #include "../../lib/gameState/InfoAboutArmy.h" #include "../../lib/mapObjects/CGCreature.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjects/CQuest.h" #include "../../lib/mapObjects/MiscObjects.h" +#include "../ConditionalWait.h" CSelWindow::CSelWindow( const std::string & Text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector>> & Buttons, QueryID askID) { @@ -333,12 +334,10 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature) fitToScreen(10); } -TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * teleporter) - : CWindowObject(BORDERED | RCLICK_POPUP) +MinimapWithIcons::MinimapWithIcons(const Point & position) { OBJECT_CONSTRUCTION; - pos.w = 322; - pos.h = 200; + pos += position; Rect areaSurface(11, 41, 144, 144); Rect areaUnderground(167, 41, 144, 144); @@ -346,16 +345,14 @@ TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * tele Rect borderSurface(10, 40, 147, 147); Rect borderUnderground(166, 40, 147, 147); - bool singleLevelMap = LOCPLINT->cb->getMapSize().y == 0; + bool singleLevelMap = LOCPLINT->cb->getMapSize().z == 1; if (singleLevelMap) { - areaSurface.x += 144; - borderSurface.x += 144; + areaSurface.x += 78; + borderSurface.x += 78; } - filledBackground = std::make_shared(Rect(0, 0, pos.w, pos.h)); - backgroundSurface = std::make_shared(borderSurface, Colors::TRANSPARENCY, Colors::YELLOW); surface = std::make_shared(areaSurface.topLeft(), areaSurface.dimensions(), 0); @@ -364,8 +361,43 @@ TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * tele backgroundUnderground = std::make_shared(borderUnderground, Colors::TRANSPARENCY, Colors::YELLOW); undergroud = std::make_shared(areaUnderground.topLeft(), areaUnderground.dimensions(), 1); } +} +void MinimapWithIcons::addIcon(const int3 & coordinates, const ImagePath & image ) +{ + OBJECT_CONSTRUCTION; + + Rect areaSurface(11, 41, 144, 144); + Rect areaUnderground(167, 41, 144, 144); + bool singleLevelMap = LOCPLINT->cb->getMapSize().z == 1; + if (singleLevelMap) + areaSurface.x += 78; + + int positionX = 144 * coordinates.x / LOCPLINT->cb->getMapSize().x; + int positionY = 144 * coordinates.y / LOCPLINT->cb->getMapSize().y; + + Point iconPosition(positionX, positionY); + + iconPosition -= Point(8,8); // compensate for 16x16 icon half-size + + if (coordinates.z == 0) + iconPosition += areaSurface.topLeft(); + else + iconPosition += areaUnderground.topLeft(); + + iconsOverlay.push_back(std::make_shared(image, iconPosition)); +} + +TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * teleporter) + : CWindowObject(BORDERED | RCLICK_POPUP) +{ + OBJECT_CONSTRUCTION; + pos.w = 322; + pos.h = 200; + + filledBackground = std::make_shared(Rect(0, 0, pos.w, pos.h)); labelTitle = std::make_shared(pos.w / 2, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, teleporter->getPopupText(LOCPLINT->playerID)); + minimap = std::make_shared(Point(0,0)); const auto & entrances = teleporter->getAllEntrances(); const auto & exits = teleporter->getAllExits(); @@ -382,32 +414,89 @@ TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * tele continue; int3 position = exitObject->visitablePos(); - - int positionX = 144 * position.x / LOCPLINT->cb->getMapSize().x; - int positionY = 144 * position.y / LOCPLINT->cb->getMapSize().y; - - Point iconPosition(positionX, positionY); - - iconPosition -= Point(8,8); // compensate for 16x16 icon half-size - - if (position.z == 0) - iconPosition += areaSurface.topLeft(); - else - iconPosition += areaUnderground.topLeft(); - ImagePath image; if (!vstd::contains(entrances, exit)) - image = ImagePath::builtin("portalExit"); + image = ImagePath::builtin("minimapIcons/portalExit"); else if (!vstd::contains(exits, exit)) - image = ImagePath::builtin("portalEntrance"); + image = ImagePath::builtin("minimapIcons/portalEntrance"); else - image = ImagePath::builtin("portalBidirectional"); + image = ImagePath::builtin("minimapIcons/portalBidirectional"); - iconsOverlay.push_back(std::make_shared(image, iconPosition)); + minimap->addIcon(position, image); } - center(position); + fitToScreen(10); +} + +KeymasterPopup::KeymasterPopup(const Point & position, const CGKeys * keymasterOrGuard) + : CWindowObject(BORDERED | RCLICK_POPUP) +{ + OBJECT_CONSTRUCTION; + pos.w = 322; + pos.h = 220; + + filledBackground = std::make_shared(Rect(0, 0, pos.w, pos.h)); + labelTitle = std::make_shared(pos.w / 2, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, keymasterOrGuard->getObjectName()); + labelDescription = std::make_shared(pos.w / 2, 40, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, keymasterOrGuard->getObjectDescription(LOCPLINT->playerID)); + minimap = std::make_shared(Point(0,20)); + + const auto allObjects = LOCPLINT->cb->getAllVisitableObjs(); + + for (const auto mapObject : allObjects) + { + if (!mapObject) + continue; + + switch (mapObject->ID) + { + case Obj::KEYMASTER: + if (mapObject->subID == keymasterOrGuard->subID) + minimap->addIcon(mapObject->visitablePos(), ImagePath::builtin("minimapIcons/keymaster")); + break; + case Obj::BORDERGUARD: + if (mapObject->subID == keymasterOrGuard->subID) + minimap->addIcon(mapObject->visitablePos(), ImagePath::builtin("minimapIcons/borderguard")); + break; + case Obj::BORDER_GATE: + if (mapObject->subID == keymasterOrGuard->subID) + minimap->addIcon(mapObject->visitablePos(), ImagePath::builtin("minimapIcons/bordergate")); + break; + } + } + center(position); + fitToScreen(10); +} + +ObeliskPopup::ObeliskPopup(const Point & position, const CGObelisk * obelisk) + : CWindowObject(BORDERED | RCLICK_POPUP) +{ + OBJECT_CONSTRUCTION; + pos.w = 322; + pos.h = 220; + + filledBackground = std::make_shared(Rect(0, 0, pos.w, pos.h)); + labelTitle = std::make_shared(pos.w / 2, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, obelisk->getObjectName()); + labelDescription = std::make_shared(pos.w / 2, 40, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, obelisk->getObjectDescription(LOCPLINT->playerID)); + minimap = std::make_shared(Point(0,20)); + + const auto allObjects = LOCPLINT->cb->getAllVisitableObjs(); + + for (const auto mapObject : allObjects) + { + if (!mapObject) + continue; + + if (mapObject->ID != Obj::OBELISK) + continue; + + if (mapObject->wasVisited(LOCPLINT->playerID)) + minimap->addIcon(mapObject->visitablePos(), ImagePath::builtin("minimapIcons/obeliskVisited")); + else + minimap->addIcon(mapObject->visitablePos(), ImagePath::builtin("minimapIcons/obelisk")); + } + center(position); + fitToScreen(10); } std::shared_ptr @@ -439,6 +528,12 @@ CRClickPopup::createCustomInfoWindow(Point position, const CGObjectInstance * sp case Obj::SUBTERRANEAN_GATE: case Obj::WHIRLPOOL: return std::make_shared(position, dynamic_cast(specific)); + case Obj::KEYMASTER: + case Obj::BORDERGUARD: + case Obj::BORDER_GATE: + return std::make_shared(position, dynamic_cast(specific)); + case Obj::OBELISK: + return std::make_shared(position, dynamic_cast(specific)); default: return std::shared_ptr(); } diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index 1fb3f0546..ad171da8b 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -21,6 +21,8 @@ class CGHeroInstance; class CGGarrison; class CGCreature; class CGTeleport; +class CGKeys; +class CGObelisk; VCMI_LIB_NAMESPACE_END @@ -116,20 +118,50 @@ public: CSelWindow(const std::string & text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID); }; -class TeleporterPopup : public CWindowObject +class MinimapWithIcons : public CIntObject { - std::shared_ptr filledBackground; - std::shared_ptr backgroundSurface; std::shared_ptr backgroundUnderground; std::shared_ptr surface; std::shared_ptr undergroud; + std::vector> iconsOverlay; + +public: + MinimapWithIcons(const Point & position); + + void addIcon(const int3 & coordinates, const ImagePath & image); +}; + +class TeleporterPopup : public CWindowObject +{ + std::shared_ptr filledBackground; + std::shared_ptr minimap; std::shared_ptr labelTitle; - std::vector> iconsOverlay; public: TeleporterPopup(const Point & position, const CGTeleport * teleporter); }; +class KeymasterPopup : public CWindowObject +{ + std::shared_ptr filledBackground; + std::shared_ptr minimap; + std::shared_ptr labelTitle; + std::shared_ptr labelDescription; + +public: + KeymasterPopup(const Point & position, const CGKeys * keymasterOrGuard); +}; + +class ObeliskPopup : public CWindowObject +{ + std::shared_ptr filledBackground; + std::shared_ptr minimap; + std::shared_ptr labelTitle; + std::shared_ptr labelDescription; + +public: + ObeliskPopup(const Point & position, const CGObelisk * obelisk); +}; diff --git a/config/ai/nkai/nkai-settings.json b/config/ai/nkai/nkai-settings.json index 55cfe9cb5..25cbcda38 100644 --- a/config/ai/nkai/nkai-settings.json +++ b/config/ai/nkai/nkai-settings.json @@ -38,13 +38,14 @@ "maxPriorityPass" : 10, "mainHeroTurnDistanceLimit" : 10, "scoutHeroTurnDistanceLimit" : 5, + "threatTurnDistanceLimit" : 1, "maxGoldPressure" : 0.3, - "updateHitmapOnTileReveal" : false, + "updateHitmapOnTileReveal" : true, "useTroopsFromGarrisons" : true, "openMap": true, - "allowObjectGraph": true, - "pathfinderBucketsCount" : 4, // old value: 3, - "pathfinderBucketSize" : 8, // old value: 7, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 3, + "pathfinderBucketSize" : 7, "retreatThresholdRelative" : 0, "retreatThresholdAbsolute" : 0, "safeAttackRatio" : 1.1, @@ -58,13 +59,14 @@ "maxPriorityPass" : 10, "mainHeroTurnDistanceLimit" : 10, "scoutHeroTurnDistanceLimit" : 5, + "threatTurnDistanceLimit" : 4, "maxGoldPressure" : 0.3, - "updateHitmapOnTileReveal" : false, + "updateHitmapOnTileReveal" : true, "useTroopsFromGarrisons" : true, "openMap": true, - "allowObjectGraph": true, - "pathfinderBucketsCount" : 4, // old value: 3, - "pathfinderBucketSize" : 8, // old value: 7, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 3, + "pathfinderBucketSize" : 7, "retreatThresholdRelative" : 0.1, "retreatThresholdAbsolute" : 5000, "safeAttackRatio" : 1.1, @@ -78,13 +80,14 @@ "maxPriorityPass" : 10, "mainHeroTurnDistanceLimit" : 10, "scoutHeroTurnDistanceLimit" : 5, + "threatTurnDistanceLimit" : 5, "maxGoldPressure" : 0.3, - "updateHitmapOnTileReveal" : false, + "updateHitmapOnTileReveal" : true, "useTroopsFromGarrisons" : true, "openMap": true, - "allowObjectGraph": true, - "pathfinderBucketsCount" : 4, // old value: 3, - "pathfinderBucketSize" : 8, // old value: 7, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 3, + "pathfinderBucketSize" : 7, "retreatThresholdRelative" : 0.3, "retreatThresholdAbsolute" : 10000, "safeAttackRatio" : 1.1, @@ -98,13 +101,14 @@ "maxPriorityPass" : 10, "mainHeroTurnDistanceLimit" : 10, "scoutHeroTurnDistanceLimit" : 5, + "threatTurnDistanceLimit" : 5, "maxGoldPressure" : 0.3, - "updateHitmapOnTileReveal" : false, + "updateHitmapOnTileReveal" : true, "useTroopsFromGarrisons" : true, "openMap": true, - "allowObjectGraph": true, - "pathfinderBucketsCount" : 4, // old value: 3, - "pathfinderBucketSize" : 8, // old value: 7, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 3, + "pathfinderBucketSize" : 7, "retreatThresholdRelative" : 0.3, "retreatThresholdAbsolute" : 10000, "safeAttackRatio" : 1.1, @@ -118,13 +122,14 @@ "maxPriorityPass" : 10, "mainHeroTurnDistanceLimit" : 10, "scoutHeroTurnDistanceLimit" : 5, + "threatTurnDistanceLimit" : 5, "maxGoldPressure" : 0.3, - "updateHitmapOnTileReveal" : false, + "updateHitmapOnTileReveal" : true, "useTroopsFromGarrisons" : true, "openMap": true, - "allowObjectGraph": true, - "pathfinderBucketsCount" : 4, // old value: 3, - "pathfinderBucketSize" : 8, // old value: 7, + "allowObjectGraph": false, + "pathfinderBucketsCount" : 3, + "pathfinderBucketSize" : 7, "retreatThresholdRelative" : 0.3, "retreatThresholdAbsolute" : 10000, "safeAttackRatio" : 1.1, diff --git a/config/bonuses.json b/config/bonuses.json index eef884656..ab929482a 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -305,7 +305,7 @@ { "graphics": { - "icon": "zvs/Lib1.res/E_SHOOT" + "icon": "zvs/Lib1.res/LIM_SHOOT" } }, diff --git a/config/gameConfig.json b/config/gameConfig.json index ac40a419f..92e869a0b 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -305,7 +305,11 @@ // if heroes are invitable in tavern "tavernInvite" : false, // minimal primary skills for heroes - "minimalPrimarySkills": [ 0, 0, 1, 1] + "minimalPrimarySkills": [ 0, 0, 1, 1], + /// movement points hero can get on start of the turn when on land, depending on speed of slowest creature (0-based list) + "movementPointsLand" : [ 1500, 1500, 1500, 1500, 1560, 1630, 1700, 1760, 1830, 1900, 1960, 2000 ], + /// movement points hero can get on start of the turn when on sea, depending on speed of slowest creature (0-based list) + "movementPointsSea" : [ 1500 ] }, "towns": @@ -560,29 +564,6 @@ "type" : "MANA_PER_KNOWLEDGE_PERCENTAGE", //1000% mana per knowledge "val" : 1000, "valueType" : "BASE_NUMBER" - }, - "landMovement" : - { - "type" : "MOVEMENT", //Basic land movement - "subtype" : "heroMovementLand", - "val" : 1300, - "valueType" : "BASE_NUMBER", - "updater" : { - "type" : "ARMY_MOVEMENT", //Enable army movement bonus - "parameters" : [ - 20, // Movement points for lowest speed numerator - 3, // Movement points for lowest speed denominator - 10, // Resulting value, rounded down, will be multiplied by this number - 700 // All army movement bonus cannot be higher than this value (so, max movement will be 1300 + 700 for this settings) - ] - } - }, - "seaMovement" : - { - "type" : "MOVEMENT", //Basic sea movement - "subtype" : "heroMovementSea", - "val" : 1500, - "valueType" : "BASE_NUMBER" } } }, diff --git a/config/schemas/gameSettings.json b/config/schemas/gameSettings.json index 9d1b55b1e..7888037a2 100644 --- a/config/schemas/gameSettings.json +++ b/config/schemas/gameSettings.json @@ -44,7 +44,9 @@ "startingStackChances" : { "type" : "array" }, "backpackSize" : { "type" : "number" }, "tavernInvite" : { "type" : "boolean" }, - "minimalPrimarySkills" : { "type" : "array" } + "minimalPrimarySkills" : { "type" : "array" }, + "movementPointsLand" : { "type" : "array" }, + "movementPointsSea" : { "type" : "array" } } }, "towns" : { diff --git a/debian/changelog b/debian/changelog index 66664c960..15ea7d463 100644 --- a/debian/changelog +++ b/debian/changelog @@ -4,6 +4,12 @@ vcmi (1.7.0) jammy; urgency=medium -- Ivan Savenko Fri, 30 May 2025 12:00:00 +0200 +vcmi (1.6.3) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Fri, 10 Jan 2025 12:00:00 +0200 + vcmi (1.6.2) jammy; urgency=medium * New upstream release diff --git a/docs/Readme.md b/docs/Readme.md index 9ee5ef59b..deba259d2 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,9 +1,9 @@ # VCMI Project [![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.0) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.1) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.2) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.3/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.3) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities. diff --git a/include/vcmi/Creature.h b/include/vcmi/Creature.h index a1334652d..6e493905a 100644 --- a/include/vcmi/Creature.h +++ b/include/vcmi/Creature.h @@ -23,9 +23,10 @@ class DLL_LINKAGE ACreature: public AFactionMember { public: bool isLiving() const; //non-undead, non-non living or alive - ui32 getMovementRange(int turn) const; //get speed (in moving tiles) of creature with all modificators - ui32 getMovementRange() const; //get speed (in moving tiles) of creature with all modificators + virtual ui32 getMovementRange(int turn) const; //get speed (in moving tiles) of creature with all modificators + virtual ui32 getMovementRange() const; //get speed (in moving tiles) of creature with all modificators virtual ui32 getMaxHealth() const; //get max HP of stack with all modifiers + virtual int32_t getInitiative(int turn = 0) const; }; template diff --git a/include/vcmi/FactionMember.h b/include/vcmi/FactionMember.h index c87049ecb..28d86d355 100644 --- a/include/vcmi/FactionMember.h +++ b/include/vcmi/FactionMember.h @@ -44,10 +44,6 @@ public: Returns defence of creature or hero. */ virtual int getDefense(bool ranged) const; - /** - Returns primskill of creature or hero. - */ - int getPrimSkillLevel(PrimarySkill id) const; /** Returns morale of creature or hero. Taking absolute bonuses into account. For now, uses range from EGameSettings diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 278453aac..c9fd96bb7 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -91,6 +91,7 @@ vcmilauncher.desktop + diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index ff171e594..367114ca9 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -74,7 +74,7 @@ Configuration files directory - Verzeichnis der Konfiguarions-Dateien + Verzeichnis der Konfigurationsdateien @@ -492,7 +492,7 @@ Installation erfolgreich heruntergeladen? Handle back as right mouse button - + Behandle "Zurück" als rechte Maustaste @@ -823,7 +823,7 @@ Exklusiver Vollbildmodus - das Spiel nimmt den gesamten Bildschirm ein und verwe Heroes Chronicles %1 - %2 - Heroes Chronicles %1 - %2 + Heroes Chronicles %1 - %2 @@ -1163,11 +1163,13 @@ Fehlerursache: Exe (%n bytes): %1 param is hash - + + SHA1-Hash der bereitgestellten Dateien: +Exe (%n Bytes): +%1 SHA1-Hash der bereitgestellten Dateien: Exe (%n Bytes): %1 - @@ -1176,11 +1178,13 @@ Exe (%n Bytes): Bin (%n bytes): %1 param is hash - + + +Bin (%n Bytes): +%1 Bin (%n Bytes): %1 - @@ -1357,7 +1361,7 @@ Bin (%n Bytes): Error starting executable - Fehler beim Starten der ausführbaren Datei + Fehler beim Starten der ausführbaren Datei @@ -1454,7 +1458,7 @@ Bin (%n Bytes): Mod data was not found - + Mod-Daten wurden nicht gefunden diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 8f37ae8d3..5768444aa 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -492,7 +492,7 @@ Zainstalować pomyślnie pobrane? Handle back as right mouse button - + Przycisk wstecz jako prawy przycisk myszy @@ -1360,7 +1360,7 @@ Bin (%n bajtów): Error starting executable - Błąd podczas uruchamiania pliku wykonywalnego + Błąd podczas uruchamiania pliku wykonywalnego @@ -1457,7 +1457,7 @@ Bin (%n bajtów): Mod data was not found - + Nie znaleziono danych moda diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index 03bbe2a48..d64fa0ff7 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -69,14 +69,6 @@ int AFactionMember::getMaxDamage(bool ranged) const return getBonusBearer()->valOfBonuses(selector, cachingStr); } -int AFactionMember::getPrimSkillLevel(PrimarySkill id) const -{ - auto allSkills = getBonusBearer()->getBonusesOfType(BonusType::PRIMARY_SKILL); - int ret = allSkills->valOfBonuses(Selector::subtype()(BonusSubtypeID(id))); - int minSkillValue = VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, id.getNum()); - return std::max(ret, minSkillValue); //otherwise, some artifacts may cause negative skill value effect, sp=0 works in old saves -} - int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const { int32_t maxGoodMorale = VLC->engineSettings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE).size(); @@ -160,6 +152,19 @@ ui32 ACreature::getMovementRange() const return getBonusBearer()->valOfBonuses(BonusType::STACKS_SPEED); } +int32_t ACreature::getInitiative(int turn) const +{ + if (turn == 0) + { + return getBonusBearer()->valOfBonuses(BonusType::STACKS_SPEED); + } + else + { + const std::string cachingStrSS = "type_STACKS_SPEED_turns_" + std::to_string(turn); + return getBonusBearer()->valOfBonuses(Selector::type()(BonusType::STACKS_SPEED).And(Selector::turns(turn)), cachingStrSS); + } +} + ui32 ACreature::getMovementRange(int turn) const { if (turn == 0) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 0e94a25e3..bd7d3ea1d 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -65,12 +65,12 @@ set(lib_MAIN_SRCS battle/Unit.cpp bonuses/Bonus.cpp + bonuses/BonusCache.cpp bonuses/BonusEnum.cpp bonuses/BonusList.cpp bonuses/BonusParams.cpp bonuses/BonusSelector.cpp bonuses/BonusCustomTypes.cpp - bonuses/CBonusProxy.cpp bonuses/CBonusSystemNode.cpp bonuses/IBonusBearer.cpp bonuses/Limiters.cpp @@ -435,12 +435,12 @@ set(lib_MAIN_HEADERS battle/Unit.h bonuses/Bonus.h + bonuses/BonusCache.h bonuses/BonusEnum.h bonuses/BonusList.h bonuses/BonusParams.h bonuses/BonusSelector.h bonuses/BonusCustomTypes.h - bonuses/CBonusProxy.h bonuses/CBonusSystemNode.h bonuses/IBonusBearer.h bonuses/Limiters.h diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index a8df9add3..c1e688490 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -76,6 +76,8 @@ const std::vector GameSettings::settingProperties = {EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, "heroes", "retreatOnWinWithoutTroops" }, {EGameSettings::HEROES_STARTING_STACKS_CHANCES, "heroes", "startingStackChances" }, {EGameSettings::HEROES_TAVERN_INVITE, "heroes", "tavernInvite" }, + {EGameSettings::HEROES_MOVEMENT_POINTS_LAND, "heroes", "movementPointsLand" }, + {EGameSettings::HEROES_MOVEMENT_POINTS_SEA, "heroes", "movementPointsSea" }, {EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" }, {EGameSettings::MAP_FORMAT_CHRONICLES, "mapFormat", "chronicles" }, {EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS, "mapFormat", "hornOfTheAbyss" }, diff --git a/lib/IGameSettings.h b/lib/IGameSettings.h index 3cb92e127..b4a0f6f72 100644 --- a/lib/IGameSettings.h +++ b/lib/IGameSettings.h @@ -49,6 +49,8 @@ enum class EGameSettings HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, HEROES_STARTING_STACKS_CHANCES, HEROES_TAVERN_INVITE, + HEROES_MOVEMENT_POINTS_LAND, + HEROES_MOVEMENT_POINTS_SEA, MAP_FORMAT_ARMAGEDDONS_BLADE, MAP_FORMAT_CHRONICLES, MAP_FORMAT_HORN_OF_THE_ABYSS, diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index f3431394a..1473051db 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -151,36 +151,6 @@ std::vector TerrainTypeHandler::loadLegacyData() return result; } -bool TerrainType::isLand() const -{ - return !isWater(); -} - -bool TerrainType::isWater() const -{ - return passabilityType & PassabilityType::WATER; -} - -bool TerrainType::isRock() const -{ - return passabilityType & PassabilityType::ROCK; -} - -bool TerrainType::isPassable() const -{ - return !isRock(); -} - -bool TerrainType::isSurface() const -{ - return passabilityType & PassabilityType::SURFACE; -} - -bool TerrainType::isUnderground() const -{ - return passabilityType & PassabilityType::SUBTERRANEAN; -} - bool TerrainType::isTransitionRequired() const { return transitionRequired; diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index 3d0715cb2..2af6db3ca 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -83,14 +83,13 @@ public: TerrainType() = default; - bool isLand() const; - bool isWater() const; - bool isRock() const; + inline bool isLand() const; + inline bool isWater() const; + inline bool isRock() const; + inline bool isPassable() const; + inline bool isSurface() const; + inline bool isUnderground() const; - bool isPassable() const; - - bool isSurface() const; - bool isUnderground() const; bool isTransitionRequired() const; }; @@ -112,4 +111,34 @@ public: std::vector loadLegacyData() override; }; +inline bool TerrainType::isLand() const +{ + return !isWater(); +} + +inline bool TerrainType::isWater() const +{ + return passabilityType & PassabilityType::WATER; +} + +inline bool TerrainType::isRock() const +{ + return passabilityType & PassabilityType::ROCK; +} + +inline bool TerrainType::isPassable() const +{ + return !isRock(); +} + +inline bool TerrainType::isSurface() const +{ + return passabilityType & PassabilityType::SURFACE; +} + +inline bool TerrainType::isUnderground() const +{ + return passabilityType & PassabilityType::SUBTERRANEAN; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index b14389fdc..a0bf78d14 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -714,18 +714,7 @@ bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker) const if (!attacker->canShoot()) return false; - //forgetfulness - TConstBonusListPtr forgetfulList = attacker->getBonusesOfType(BonusType::FORGETFULL); - if(!forgetfulList->empty()) - { - int forgetful = forgetfulList->totalValue(); - - //advanced+ level - if(forgetful > 1) - return false; - } - - return !battleIsUnitBlocked(attacker) || attacker->hasBonusOfType(BonusType::FREE_SHOOTING); + return attacker->canShootBlocked() || !battleIsUnitBlocked(attacker); } bool CBattleInfoCallback::battleCanTargetEmptyHex(const battle::Unit * attacker) const @@ -1732,9 +1721,6 @@ bool CBattleInfoCallback::battleIsUnitBlocked(const battle::Unit * unit) const { RETURN_IF_NOT_BATTLE(false); - if(unit->hasBonusOfType(BonusType::SIEGE_WEAPON)) //siege weapons cannot be blocked - return false; - for(const auto * adjacent : battleAdjacentUnits(unit)) { if(adjacent->unitOwner() != unit->unitOwner()) //blocked by enemy stack diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index b9f23468b..20f02120e 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -404,7 +404,7 @@ PlayerColor CBattleInfoEssentials::battleGetOwner(const battle::Unit * unit) con PlayerColor initialOwner = getBattle()->getSidePlayer(unit->unitSide()); - if(unit->hasBonusOfType(BonusType::HYPNOTIZED)) + if(unit->isHypnotized()) return otherPlayer(initialOwner); else return initialOwner; diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 994705b25..aa1508472 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -26,7 +26,7 @@ namespace battle CAmmo::CAmmo(const battle::Unit * Owner, CSelector totalSelector): used(0), owner(Owner), - totalProxy(Owner, std::move(totalSelector)) + totalProxy(Owner, totalSelector) { reset(); } @@ -34,7 +34,6 @@ CAmmo::CAmmo(const battle::Unit * Owner, CSelector totalSelector): CAmmo & CAmmo::operator= (const CAmmo & other) { used = other.used; - totalProxy = other.totalProxy; return *this; } @@ -60,7 +59,7 @@ void CAmmo::reset() int32_t CAmmo::total() const { - return totalProxy->totalValue(); + return totalProxy.getValue(); } void CAmmo::use(int32_t amount) @@ -85,20 +84,13 @@ void CAmmo::serializeJson(JsonSerializeFormat & handler) ///CShots CShots::CShots(const battle::Unit * Owner) : CAmmo(Owner, Selector::type()(BonusType::SHOTS)), - shooter(Owner, BonusType::SHOOTER) + shooter(Owner, Selector::type()(BonusType::SHOOTER)) { } -CShots & CShots::operator=(const CShots & other) -{ - CAmmo::operator=(other); - shooter = other.shooter; - return *this; -} - bool CShots::isLimited() const { - return !shooter.getHasBonus() || !env->unitHasAmmoCart(owner); + return !shooter.hasBonus() || !env->unitHasAmmoCart(owner); } void CShots::setEnv(const IUnitEnvironment * env_) @@ -108,7 +100,7 @@ void CShots::setEnv(const IUnitEnvironment * env_) int32_t CShots::total() const { - if(shooter.getHasBonus()) + if(shooter.hasBonus()) return CAmmo::total(); else return 0; @@ -124,23 +116,23 @@ CCasts::CCasts(const battle::Unit * Owner): CRetaliations::CRetaliations(const battle::Unit * Owner) : CAmmo(Owner, Selector::type()(BonusType::ADDITIONAL_RETALIATION)), totalCache(0), - noRetaliation(Owner, Selector::type()(BonusType::SIEGE_WEAPON).Or(Selector::type()(BonusType::HYPNOTIZED)).Or(Selector::type()(BonusType::NO_RETALIATION)), "CRetaliations::noRetaliation"), - unlimited(Owner, BonusType::UNLIMITED_RETALIATIONS) + noRetaliation(Owner, Selector::type()(BonusType::SIEGE_WEAPON).Or(Selector::type()(BonusType::HYPNOTIZED)).Or(Selector::type()(BonusType::NO_RETALIATION))), + unlimited(Owner, Selector::type()(BonusType::UNLIMITED_RETALIATIONS)) { } bool CRetaliations::isLimited() const { - return !unlimited.getHasBonus() || noRetaliation.getHasBonus(); + return !unlimited.hasBonus() || noRetaliation.hasBonus(); } int32_t CRetaliations::total() const { - if(noRetaliation.getHasBonus()) + if(noRetaliation.hasBonus()) return 0; //after dispel bonus should remain during current round - int32_t val = 1 + totalProxy->totalValue(); + int32_t val = 1 + totalProxy.getValue(); vstd::amax(totalCache, val); return totalCache; } @@ -341,13 +333,9 @@ CUnitState::CUnitState(): counterAttacks(this), health(this), shots(this), - totalAttacks(this, Selector::type()(BonusType::ADDITIONAL_ATTACK), 1), - minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin)), 0), - maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax)), 0), - attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)), 0), - defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)), 0), - inFrenzy(this, Selector::type()(BonusType::IN_FRENZY)), - cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::CLONE)))), "CUnitState::cloneLifetimeMarker"), + stackSpeedPerTurn(this, Selector::type()(BonusType::STACKS_SPEED), BonusCacheMode::VALUE), + immobilizedPerTurn(this, Selector::type()(BonusType::SIEGE_WEAPON).Or(Selector::type()(BonusType::BIND_EFFECT)), BonusCacheMode::PRESENCE), + bonusCache(this), cloneID(-1) { @@ -372,15 +360,8 @@ CUnitState & CUnitState::operator=(const CUnitState & other) waitedThisTurn = other.waitedThisTurn; casts = other.casts; counterAttacks = other.counterAttacks; - health = other.health; shots = other.shots; - totalAttacks = other.totalAttacks; - minDamage = other.minDamage; - maxDamage = other.maxDamage; - attack = other.attack; - defence = other.defence; - inFrenzy = other.inFrenzy; - cloneLifetimeMarker = other.cloneLifetimeMarker; + health = other.health; cloneID = other.cloneID; position = other.position; return *this; @@ -542,9 +523,16 @@ bool CUnitState::isCaster() const return casts.total() > 0;//do not check specific cast abilities here } +bool CUnitState::canShootBlocked() const +{ + return bonusCache.getBonusValue(UnitBonusValuesProxy::HAS_FREE_SHOOTING); +} + bool CUnitState::canShoot() const { - return shots.canUse(1); + return + shots.canUse(1) && + bonusCache.getBonusValue(UnitBonusValuesProxy::FORGETFULL) <= 1; //advanced+ level } bool CUnitState::isShooter() const @@ -579,6 +567,11 @@ int64_t CUnitState::getTotalHealth() const return health.total(); } +uint32_t CUnitState::getMaxHealth() const +{ + return std::max(1, bonusCache.getBonusValue(UnitBonusValuesProxy::STACK_HEALTH)); +} + BattleHex CUnitState::getPosition() const { return position; @@ -591,11 +584,20 @@ void CUnitState::setPosition(BattleHex hex) int32_t CUnitState::getInitiative(int turn) const { - if (turn == 0) - return valOfBonuses(BonusType::STACKS_SPEED); + return stackSpeedPerTurn.getValue(turn); +} - std::string cachingStr = "type_STACKS_SPEED_turns_" + std::to_string(turn); - return valOfBonuses(Selector::type()(BonusType::STACKS_SPEED).And(Selector::turns(turn)), cachingStr); +ui32 CUnitState::getMovementRange(int turn) const +{ + if (immobilizedPerTurn.getValue(0) != 0) + return 0; + + return stackSpeedPerTurn.getValue(0); +} + +ui32 CUnitState::getMovementRange() const +{ + return getMovementRange(0); } uint8_t CUnitState::getRangedFullDamageDistance() const @@ -693,47 +695,69 @@ BattlePhases::Type CUnitState::battleQueuePhase(int turn) const } } +bool CUnitState::isHypnotized() const +{ + return bonusCache.getBonusValue(UnitBonusValuesProxy::HYPNOTIZED); +} + int CUnitState::getTotalAttacks(bool ranged) const { - return ranged ? totalAttacks.getRangedValue() : totalAttacks.getMeleeValue(); + return 1 + (ranged ? + bonusCache.getBonusValue(UnitBonusValuesProxy::TOTAL_ATTACKS_RANGED): + bonusCache.getBonusValue(UnitBonusValuesProxy::TOTAL_ATTACKS_MELEE)); } int CUnitState::getMinDamage(bool ranged) const { - return ranged ? minDamage.getRangedValue() : minDamage.getMeleeValue(); + return ranged ? + bonusCache.getBonusValue(UnitBonusValuesProxy::MIN_DAMAGE_RANGED): + bonusCache.getBonusValue(UnitBonusValuesProxy::MIN_DAMAGE_MELEE); + } int CUnitState::getMaxDamage(bool ranged) const { - return ranged ? maxDamage.getRangedValue() : maxDamage.getMeleeValue(); + return ranged ? + bonusCache.getBonusValue(UnitBonusValuesProxy::MAX_DAMAGE_RANGED): + bonusCache.getBonusValue(UnitBonusValuesProxy::MAX_DAMAGE_MELEE); } int CUnitState::getAttack(bool ranged) const { - int ret = ranged ? attack.getRangedValue() : attack.getMeleeValue(); + int attack = ranged ? + bonusCache.getBonusValue(UnitBonusValuesProxy::ATTACK_RANGED): + bonusCache.getBonusValue(UnitBonusValuesProxy::ATTACK_MELEE); - if(!inFrenzy->empty()) + int frenzy = bonusCache.getBonusValue(UnitBonusValuesProxy::IN_FRENZY); + if(frenzy != 0) { - double frenzyPower = static_cast(inFrenzy->totalValue()) / 100; - frenzyPower *= static_cast(ranged ? defence.getRangedValue() : defence.getMeleeValue()); - ret += static_cast(frenzyPower); + int defence = ranged ? + bonusCache.getBonusValue(UnitBonusValuesProxy::DEFENCE_RANGED): + bonusCache.getBonusValue(UnitBonusValuesProxy::DEFENCE_MELEE); + + int frenzyBonus = frenzy * defence / 100; + attack += frenzyBonus; } - vstd::amax(ret, 0); - return ret; + vstd::amax(attack, 0); + return attack; } int CUnitState::getDefense(bool ranged) const { - if(!inFrenzy->empty()) + int frenzy = bonusCache.getBonusValue(UnitBonusValuesProxy::IN_FRENZY); + + if(frenzy != 0) { return 0; } else { - int ret = ranged ? defence.getRangedValue() : defence.getMeleeValue(); - vstd::amax(ret, 0); - return ret; + int defence = ranged ? + bonusCache.getBonusValue(UnitBonusValuesProxy::DEFENCE_RANGED): + bonusCache.getBonusValue(UnitBonusValuesProxy::DEFENCE_MELEE); + vstd::amax(defence, 0); + return defence; } } @@ -886,7 +910,7 @@ void CUnitState::afterNewRound() if(alive() && isClone()) { - if(!cloneLifetimeMarker.getHasBonus()) + if(!bonusCache.hasBonus(UnitBonusValuesProxy::CLONE_MARKER)) makeGhost(); } } diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index 9bb9570a1..d018c6625 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -11,7 +11,7 @@ #pragma once #include "Unit.h" -#include "../bonuses/CBonusProxy.h" +#include "../bonuses/BonusCache.h" VCMI_LIB_NAMESPACE_BEGIN @@ -32,10 +32,6 @@ class DLL_LINKAGE CAmmo public: explicit CAmmo(const battle::Unit * Owner, CSelector totalSelector); - //only copy construction is allowed for acquire(), serializeJson should be used for any other "assignment" - CAmmo(const CAmmo & other) = default; - CAmmo(CAmmo && other) = delete; - CAmmo & operator=(const CAmmo & other); CAmmo & operator=(CAmmo && other) = delete; @@ -50,15 +46,14 @@ public: protected: int32_t used; const battle::Unit * owner; - CBonusProxy totalProxy; + BonusValueCache totalProxy; }; class DLL_LINKAGE CShots : public CAmmo { public: explicit CShots(const battle::Unit * Owner); - CShots(const CShots & other) = default; - CShots & operator=(const CShots & other); + bool isLimited() const override; int32_t total() const override; @@ -66,23 +61,20 @@ public: private: const IUnitEnvironment * env; - CCheckProxy shooter; + BonusValueCache shooter; }; class DLL_LINKAGE CCasts : public CAmmo { public: explicit CCasts(const battle::Unit * Owner); - CCasts(const CCasts & other) = default; - CCasts & operator=(const CCasts & other) = default; }; class DLL_LINKAGE CRetaliations : public CAmmo { public: explicit CRetaliations(const battle::Unit * Owner); - CRetaliations(const CRetaliations & other) = default; - CRetaliations & operator=(const CRetaliations & other) = default; + bool isLimited() const override; int32_t total() const override; void reset() override; @@ -91,8 +83,8 @@ public: private: mutable int32_t totalCache; - CCheckProxy noRetaliation; - CCheckProxy unlimited; + BonusValueCache noRetaliation; + BonusValueCache unlimited; }; class DLL_LINKAGE CHealth @@ -154,11 +146,6 @@ public: CHealth health; CShots shots; - CTotalsProxy totalAttacks; - - CTotalsProxy minDamage; - CTotalsProxy maxDamage; - ///id of alive clone of this stack clone if any si32 cloneID; @@ -205,11 +192,14 @@ public: bool isFrozen() const override; bool isValidTarget(bool allowDead = false) const override; + bool isHypnotized() const override; + bool isClone() const override; bool hasClone() const override; bool canCast() const override; bool isCaster() const override; + bool canShootBlocked() const override; bool canShoot() const override; bool isShooter() const override; @@ -218,6 +208,7 @@ public: int32_t getFirstHPleft() const override; int64_t getAvailableHealth() const override; int64_t getTotalHealth() const override; + uint32_t getMaxHealth() const override; BattleHex getPosition() const override; void setPosition(BattleHex hex) override; @@ -225,6 +216,9 @@ public: uint8_t getRangedFullDamageDistance() const; uint8_t getShootingRangeDistance() const; + ui32 getMovementRange(int turn) const override; + ui32 getMovementRange() const override; + bool canMove(int turn = 0) const override; bool defended(int turn = 0) const override; bool moved(int turn = 0) const override; @@ -268,11 +262,9 @@ public: private: const IUnitEnvironment * env; - CTotalsProxy attack; - CTotalsProxy defence; - CBonusProxy inFrenzy; - - CCheckProxy cloneLifetimeMarker; + BonusCachePerTurn immobilizedPerTurn; + BonusCachePerTurn stackSpeedPerTurn; + UnitBonusValuesProxy bonusCache; void reset(); }; @@ -282,13 +274,12 @@ class DLL_LINKAGE CUnitStateDetached : public CUnitState public: explicit CUnitStateDetached(const IUnitInfo * unit_, const IBonusBearer * bonus_); - TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, - const std::string & cachingStr = "") const override; + CUnitStateDetached & operator= (const CUnitState & other); + + TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, const std::string & cachingStr = "") const override; int64_t getTreeVersion() const override; - CUnitStateDetached & operator= (const CUnitState & other); - uint32_t unitId() const override; BattleSide unitSide() const override; @@ -297,7 +288,6 @@ public: SlotID unitSlot() const override; - int32_t unitBaseAmount() const override; void spendMana(ServerCallback * server, const int spellCost) const override; diff --git a/lib/battle/Unit.h b/lib/battle/Unit.h index cbe585593..574a111be 100644 --- a/lib/battle/Unit.h +++ b/lib/battle/Unit.h @@ -84,11 +84,14 @@ public: bool isTurret() const; virtual bool isValidTarget(bool allowDead = false) const = 0; //non-turret non-ghost stacks (can be attacked or be object of magic effect) + virtual bool isHypnotized() const = 0; + virtual bool isClone() const = 0; virtual bool hasClone() const = 0; virtual bool canCast() const = 0; virtual bool isCaster() const = 0; + virtual bool canShootBlocked() const = 0; virtual bool canShoot() const = 0; virtual bool isShooter() const = 0; @@ -112,8 +115,6 @@ public: virtual BattleHex getPosition() const = 0; virtual void setPosition(BattleHex hex) = 0; - virtual int32_t getInitiative(int turn = 0) const = 0; - virtual bool canMove(int turn = 0) const = 0; //if stack can move virtual bool defended(int turn = 0) const = 0; virtual bool moved(int turn = 0) const = 0; //if stack was already moved this turn diff --git a/lib/bonuses/BonusCache.cpp b/lib/bonuses/BonusCache.cpp new file mode 100644 index 000000000..8be61dd69 --- /dev/null +++ b/lib/bonuses/BonusCache.cpp @@ -0,0 +1,212 @@ +/* + * BonusCache.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 "BonusCache.h" +#include "IBonusBearer.h" + +#include "BonusSelector.h" +#include "BonusList.h" + +#include "../VCMI_Lib.h" +#include "../IGameSettings.h" + +VCMI_LIB_NAMESPACE_BEGIN + +int BonusCacheBase::getBonusValueImpl(BonusCacheEntry & currentValue, const CSelector & selector, BonusCacheMode mode) const +{ + if (target->getTreeVersion() == currentValue.version) + { + return currentValue.value; + } + else + { + // NOTE: following code theoretically can fail if bonus tree was changed by another thread between two following lines + // However, this situation should not be possible - gamestate modification should only happen in single-treaded mode with locked gamestate mutex + int newValue; + + if (mode == BonusCacheMode::VALUE) + newValue = target->valOfBonuses(selector); + else + newValue = target->hasBonus(selector); + currentValue.value = newValue; + currentValue.version = target->getTreeVersion(); + + return newValue; + } +} + +BonusValueCache::BonusValueCache(const IBonusBearer * target, const CSelector & selector) + :BonusCacheBase(target),selector(selector) +{} + +int BonusValueCache::getValue() const +{ + return getBonusValueImpl(value, selector, BonusCacheMode::VALUE); +} + +bool BonusValueCache::hasBonus() const +{ + return getBonusValueImpl(value, selector, BonusCacheMode::PRESENCE); +} + +MagicSchoolMasteryCache::MagicSchoolMasteryCache(const IBonusBearer * target) + :target(target) +{} + +void MagicSchoolMasteryCache::update() const +{ + static const CSelector allBonusesSelector = Selector::type()(BonusType::MAGIC_SCHOOL_SKILL); + static const std::array schoolsSelector = { + Selector::subtype()(SpellSchool::ANY), + Selector::subtype()(SpellSchool::AIR), + Selector::subtype()(SpellSchool::FIRE), + Selector::subtype()(SpellSchool::WATER), + Selector::subtype()(SpellSchool::EARTH), + }; + + auto list = target->getBonuses(allBonusesSelector); + for (int i = 0; i < schoolsSelector.size(); ++i) + schools[i] = list->valOfBonuses(schoolsSelector[i]); + + version = target->getTreeVersion(); +} + +int32_t MagicSchoolMasteryCache::getMastery(const SpellSchool & school) const +{ + if (target->getTreeVersion() != version) + update(); + return schools[school.num + 1]; +} + +PrimarySkillsCache::PrimarySkillsCache(const IBonusBearer * target) + :target(target) +{} + +void PrimarySkillsCache::update() const +{ + static const CSelector primarySkillsSelector = Selector::type()(BonusType::PRIMARY_SKILL); + static const CSelector attackSelector = Selector::subtype()(PrimarySkill::ATTACK); + static const CSelector defenceSelector = Selector::subtype()(PrimarySkill::DEFENSE); + static const CSelector spellPowerSelector = Selector::subtype()(PrimarySkill::SPELL_POWER); + static const CSelector knowledgeSelector = Selector::subtype()(PrimarySkill::KNOWLEDGE); + + std::array minValues = { + VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, PrimarySkill::ATTACK), + VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, PrimarySkill::DEFENSE), + VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, PrimarySkill::SPELL_POWER), + VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, PrimarySkill::KNOWLEDGE) + }; + + auto list = target->getBonuses(primarySkillsSelector); + skills[PrimarySkill::ATTACK] = std::max(minValues[PrimarySkill::ATTACK], list->valOfBonuses(attackSelector)); + skills[PrimarySkill::DEFENSE] = std::max(minValues[PrimarySkill::DEFENSE], list->valOfBonuses(defenceSelector)); + skills[PrimarySkill::SPELL_POWER] = std::max(minValues[PrimarySkill::SPELL_POWER], list->valOfBonuses(spellPowerSelector)); + skills[PrimarySkill::KNOWLEDGE] = std::max(minValues[PrimarySkill::KNOWLEDGE], list->valOfBonuses(knowledgeSelector)); + + version = target->getTreeVersion(); +} + +const std::array, 4> & PrimarySkillsCache::getSkills() const +{ + if (target->getTreeVersion() != version) + update(); + return skills; +} + +int BonusCachePerTurn::getValueUncached(int turns) const +{ + std::lock_guard lock(bonusListMutex); + + int nodeTreeVersion = target->getTreeVersion(); + + if (bonusListVersion != nodeTreeVersion) + { + bonusList = target->getBonuses(selector); + bonusListVersion = nodeTreeVersion; + } + + if (mode == BonusCacheMode::VALUE) + { + if (turns != 0) + return bonusList->valOfBonuses(Selector::turns(turns)); + else + return bonusList->totalValue(); + } + else + { + if (turns != 0) + return bonusList->getFirst(Selector::turns(turns)) != nullptr; + else + return !bonusList->empty(); + } +} + +int BonusCachePerTurn::getValue(int turns) const +{ + int nodeTreeVersion = target->getTreeVersion(); + + if (turns < cachedTurns) + { + auto & entry = cache[turns]; + if (entry.version == nodeTreeVersion) + { + // best case: value is in cache and up-to-date + return entry.value; + } + else + { + // else - compute value and update it in the cache + int newValue = getValueUncached(turns); + entry.value = newValue; + entry.version = nodeTreeVersion; + return newValue; + } + } + else + { + // non-cacheable value - compute and return (should be 0 / close to 0 calls) + return getValueUncached(turns); + } +} + +const UnitBonusValuesProxy::SelectorsArray * UnitBonusValuesProxy::generateSelectors() +{ + static const CSelector additionalAttack = Selector::type()(BonusType::ADDITIONAL_ATTACK); + static const CSelector selectorMelee = Selector::effectRange()(BonusLimitEffect::NO_LIMIT).Or(Selector::effectRange()(BonusLimitEffect::ONLY_MELEE_FIGHT)); + static const CSelector selectorRanged = Selector::effectRange()(BonusLimitEffect::NO_LIMIT).Or(Selector::effectRange()(BonusLimitEffect::ONLY_DISTANCE_FIGHT)); + static const CSelector minDamage = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin)); + static const CSelector maxDamage = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax)); + static const CSelector attack = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)); + static const CSelector defence = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)); + + static const UnitBonusValuesProxy::SelectorsArray selectors = { + additionalAttack.And(selectorMelee), //TOTAL_ATTACKS_MELEE, + additionalAttack.And(selectorRanged), //TOTAL_ATTACKS_RANGED, + minDamage.And(selectorMelee), //MIN_DAMAGE_MELEE, + minDamage.And(selectorRanged), //MIN_DAMAGE_RANGED, + maxDamage.And(selectorMelee), //MAX_DAMAGE_MELEE, + maxDamage.And(selectorRanged), //MAX_DAMAGE_RANGED, + attack.And(selectorMelee),//ATTACK_MELEE, + attack.And(selectorRanged),//ATTACK_RANGED, + defence.And(selectorMelee),//DEFENCE_MELEE, + defence.And(selectorRanged),//DEFENCE_RANGED, + Selector::type()(BonusType::IN_FRENZY),//IN_FRENZY, + Selector::type()(BonusType::HYPNOTIZED),//HYPNOTIZED, + Selector::type()(BonusType::FORGETFULL),//FORGETFULL, + Selector::type()(BonusType::FREE_SHOOTING).Or(Selector::type()(BonusType::SIEGE_WEAPON)),//HAS_FREE_SHOOTING, + Selector::type()(BonusType::STACK_HEALTH),//STACK_HEALTH, + Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::CLONE)))) + }; + + return &selectors; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusCache.h b/lib/bonuses/BonusCache.h new file mode 100644 index 000000000..753c54905 --- /dev/null +++ b/lib/bonuses/BonusCache.h @@ -0,0 +1,201 @@ +/* + * BonusCache.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 "BonusSelector.h" + +VCMI_LIB_NAMESPACE_BEGIN + +enum class BonusCacheMode : int8_t +{ + VALUE, // total value of bonus will be cached + PRESENCE, // presence of bonus will be cached +}; + +/// Internal base class with no own cache +class BonusCacheBase +{ +protected: + const IBonusBearer * target; + + explicit BonusCacheBase(const IBonusBearer * target): + target(target) + {} + + struct BonusCacheEntry + { + std::atomic version = 0; + std::atomic value = 0; + + BonusCacheEntry() = default; + BonusCacheEntry(const BonusCacheEntry & other) + : version(other.version.load()) + , value(other.value.load()) + { + } + BonusCacheEntry & operator =(const BonusCacheEntry & other) + { + version = other.version.load(); + value = other.value.load(); + return *this; + } + }; + + int getBonusValueImpl(BonusCacheEntry & currentValue, const CSelector & selector, BonusCacheMode) const; +}; + +/// Cache that tracks a single query to bonus system +class BonusValueCache : public BonusCacheBase +{ + CSelector selector; + mutable BonusCacheEntry value; +public: + BonusValueCache(const IBonusBearer * target, const CSelector & selector); + int getValue() const; + bool hasBonus() const; +}; + +/// Cache that can track a list of queries to bonus system +template +class BonusValuesArrayCache : public BonusCacheBase +{ +public: + using SelectorsArray = std::array; + + BonusValuesArrayCache(const IBonusBearer * target, const SelectorsArray * selectors) + : BonusCacheBase(target) + , selectors(selectors) + {} + + int getBonusValue(int index) const + { + return getBonusValueImpl(cache[index], (*selectors)[index], BonusCacheMode::VALUE); + } + + int hasBonus(int index) const + { + return getBonusValueImpl(cache[index], (*selectors)[index], BonusCacheMode::PRESENCE); + } + +private: + using CacheArray = std::array; + + const SelectorsArray * selectors; + mutable CacheArray cache; +}; + +class UnitBonusValuesProxy +{ +public: + enum ECacheKeys : int8_t + { + TOTAL_ATTACKS_MELEE, + TOTAL_ATTACKS_RANGED, + + MIN_DAMAGE_MELEE, + MIN_DAMAGE_RANGED, + MAX_DAMAGE_MELEE, + MAX_DAMAGE_RANGED, + + ATTACK_MELEE, + ATTACK_RANGED, + + DEFENCE_MELEE, + DEFENCE_RANGED, + + IN_FRENZY, + HYPNOTIZED, + FORGETFULL, + HAS_FREE_SHOOTING, + STACK_HEALTH, + + CLONE_MARKER, + + TOTAL_KEYS, + }; + static constexpr size_t KEYS_COUNT = static_cast(ECacheKeys::TOTAL_KEYS); + + using SelectorsArray = BonusValuesArrayCache::SelectorsArray; + + UnitBonusValuesProxy(const IBonusBearer * Target): + cache(Target, generateSelectors()) + {} + + int getBonusValue(ECacheKeys which) const + { + auto index = static_cast(which); + return cache.getBonusValue(index); + } + + int hasBonus(ECacheKeys which) const + { + auto index = static_cast(which); + return cache.hasBonus(index); + } + +private: + const SelectorsArray * generateSelectors(); + + BonusValuesArrayCache cache; +}; + +/// Cache that tracks values of primary skill values in bonus system +class PrimarySkillsCache +{ + const IBonusBearer * target; + mutable std::atomic version = 0; + mutable std::array, 4> skills; + + void update() const; +public: + PrimarySkillsCache(const IBonusBearer * target); + + const std::array, 4> & getSkills() const; +}; + +/// Cache that tracks values of spell school mastery in bonus system +class MagicSchoolMasteryCache +{ + const IBonusBearer * target; + mutable std::atomic version = 0; + mutable std::array, 4+1> schools; + + void update() const; +public: + MagicSchoolMasteryCache(const IBonusBearer * target); + + int32_t getMastery(const SpellSchool & school) const; +}; + +/// Cache that tracks values for different values of bonus duration +class BonusCachePerTurn : public BonusCacheBase +{ + static constexpr int cachedTurns = 8; + + const CSelector selector; + mutable TConstBonusListPtr bonusList; + mutable std::mutex bonusListMutex; + mutable std::atomic bonusListVersion = 0; + mutable std::array cache; + const BonusCacheMode mode; + + int getValueUncached(int turns) const; +public: + BonusCachePerTurn(const IBonusBearer * target, const CSelector & selector, BonusCacheMode mode) + : BonusCacheBase(target) + , selector(selector) + , mode(mode) + {} + + int getValue(int turns) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusList.cpp b/lib/bonuses/BonusList.cpp index 1739f560a..5e4048573 100644 --- a/lib/bonuses/BonusList.cpp +++ b/lib/bonuses/BonusList.cpp @@ -83,10 +83,10 @@ void BonusList::stackBonuses() } } -int BonusList::totalValue() const +int BonusList::totalValue(int baseValue) const { if (bonuses.empty()) - return 0; + return baseValue; struct BonusCollection { @@ -104,6 +104,7 @@ int BonusList::totalValue() const }; BonusCollection accumulated; + accumulated.base = baseValue; int indexMaxCount = 0; int indexMinCount = 0; @@ -208,12 +209,12 @@ void BonusList::getAllBonuses(BonusList &out) const out.push_back(b); } -int BonusList::valOfBonuses(const CSelector &select) const +int BonusList::valOfBonuses(const CSelector &select, int baseValue) const { BonusList ret; CSelector limit = nullptr; getBonuses(ret, select, limit); - return ret.totalValue(); + return ret.totalValue(baseValue); } JsonNode BonusList::toJsonNode() const diff --git a/lib/bonuses/BonusList.h b/lib/bonuses/BonusList.h index 66b4f2b42..cc266494f 100644 --- a/lib/bonuses/BonusList.h +++ b/lib/bonuses/BonusList.h @@ -58,14 +58,14 @@ public: // BonusList functions void stackBonuses(); - int totalValue() const; + int totalValue(int baseValue = 0) const; void getBonuses(BonusList &out, const CSelector &selector, const CSelector &limit = nullptr) const; void getAllBonuses(BonusList &out) const; //special find functions std::shared_ptr getFirst(const CSelector &select); std::shared_ptr getFirst(const CSelector &select) const; - int valOfBonuses(const CSelector &select) const; + int valOfBonuses(const CSelector &select, int baseValue = 0) const; // conversion / output JsonNode toJsonNode() const; diff --git a/lib/bonuses/CBonusProxy.cpp b/lib/bonuses/CBonusProxy.cpp deleted file mode 100644 index aa99e9a7d..000000000 --- a/lib/bonuses/CBonusProxy.cpp +++ /dev/null @@ -1,221 +0,0 @@ -/* - * CBonusProxy.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 "BonusList.h" -#include "CBonusProxy.h" -#include "IBonusBearer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -///CBonusProxy -CBonusProxy::CBonusProxy(const IBonusBearer * Target, CSelector Selector): - bonusListCachedLast(0), - target(Target), - selector(std::move(Selector)), - currentBonusListIndex(0) -{ -} - -CBonusProxy::CBonusProxy(const CBonusProxy & other): - bonusListCachedLast(other.bonusListCachedLast), - target(other.target), - selector(other.selector), - currentBonusListIndex(other.currentBonusListIndex) -{ - bonusList[currentBonusListIndex] = other.bonusList[currentBonusListIndex]; -} - -CBonusProxy::CBonusProxy(CBonusProxy && other) noexcept: - bonusListCachedLast(0), - target(other.target), - currentBonusListIndex(0) -{ - std::swap(bonusListCachedLast, other.bonusListCachedLast); - std::swap(selector, other.selector); - std::swap(bonusList, other.bonusList); - std::swap(currentBonusListIndex, other.currentBonusListIndex); -} - -CBonusProxy & CBonusProxy::operator=(const CBonusProxy & other) -{ - boost::lock_guard lock(swapGuard); - - selector = other.selector; - swapBonusList(other.bonusList[other.currentBonusListIndex]); - bonusListCachedLast = other.bonusListCachedLast; - - return *this; -} - -CBonusProxy & CBonusProxy::operator=(CBonusProxy && other) noexcept -{ - std::swap(bonusListCachedLast, other.bonusListCachedLast); - std::swap(selector, other.selector); - std::swap(bonusList, other.bonusList); - std::swap(currentBonusListIndex, other.currentBonusListIndex); - - return *this; -} - -void CBonusProxy::swapBonusList(TConstBonusListPtr other) const -{ - // The idea here is to avoid changing active bonusList while it can be read by a different thread. - // Because such use of shared ptr is not thread safe - // So to avoid this we change the second offline instance and swap active index - auto newCurrent = 1 - currentBonusListIndex; - bonusList[newCurrent] = std::move(other); - currentBonusListIndex = newCurrent; -} - -TConstBonusListPtr CBonusProxy::getBonusList() const -{ - auto needUpdateBonusList = [&]() -> bool - { - return target->getTreeVersion() != bonusListCachedLast || !bonusList[currentBonusListIndex]; - }; - - // avoid locking if everything is up-to-date - if(needUpdateBonusList()) - { - boost::lock_guardlock(swapGuard); - - if(needUpdateBonusList()) - { - //TODO: support limiters - swapBonusList(target->getAllBonuses(selector, Selector::all)); - bonusListCachedLast = target->getTreeVersion(); - } - } - - return bonusList[currentBonusListIndex]; -} - -const BonusList * CBonusProxy::operator->() const -{ - return getBonusList().get(); -} - -CTotalsProxy::CTotalsProxy(const IBonusBearer * Target, CSelector Selector, int InitialValue): - CBonusProxy(Target, std::move(Selector)), - initialValue(InitialValue), - meleeCachedLast(0), - meleeValue(0), - rangedCachedLast(0), - rangedValue(0) -{ -} - -CTotalsProxy::CTotalsProxy(const CTotalsProxy & other) - : CBonusProxy(other), - initialValue(other.initialValue), - meleeCachedLast(other.meleeCachedLast), - meleeValue(other.meleeValue), - rangedCachedLast(other.rangedCachedLast), - rangedValue(other.rangedValue) -{ -} - -int CTotalsProxy::getValue() const -{ - const auto treeVersion = target->getTreeVersion(); - - if(treeVersion != valueCachedLast) - { - auto bonuses = getBonusList(); - - value = initialValue + bonuses->totalValue(); - valueCachedLast = treeVersion; - } - return value; -} - -int CTotalsProxy::getValueAndList(TConstBonusListPtr & outBonusList) const -{ - const auto treeVersion = target->getTreeVersion(); - outBonusList = getBonusList(); - - if(treeVersion != valueCachedLast) - { - value = initialValue + outBonusList->totalValue(); - valueCachedLast = treeVersion; - } - return value; -} - -int CTotalsProxy::getMeleeValue() const -{ - static const auto limit = Selector::effectRange()(BonusLimitEffect::NO_LIMIT).Or(Selector::effectRange()(BonusLimitEffect::ONLY_MELEE_FIGHT)); - - const auto treeVersion = target->getTreeVersion(); - - if(treeVersion != meleeCachedLast) - { - auto bonuses = target->getBonuses(selector, limit); - meleeValue = initialValue + bonuses->totalValue(); - meleeCachedLast = treeVersion; - } - - return meleeValue; -} - -int CTotalsProxy::getRangedValue() const -{ - static const auto limit = Selector::effectRange()(BonusLimitEffect::NO_LIMIT).Or(Selector::effectRange()(BonusLimitEffect::ONLY_DISTANCE_FIGHT)); - - const auto treeVersion = target->getTreeVersion(); - - if(treeVersion != rangedCachedLast) - { - auto bonuses = target->getBonuses(selector, limit); - rangedValue = initialValue + bonuses->totalValue(); - rangedCachedLast = treeVersion; - } - - return rangedValue; -} - -///CCheckProxy -CCheckProxy::CCheckProxy(const IBonusBearer * Target, BonusType bonusType): - target(Target), - selector(Selector::type()(bonusType)), - cachingStr("type_" + std::to_string(static_cast(bonusType))), - cachedLast(0), - hasBonus(false) -{ - -} - -CCheckProxy::CCheckProxy(const IBonusBearer * Target, CSelector Selector, const std::string & cachingStr): - target(Target), - selector(std::move(Selector)), - cachedLast(0), - cachingStr(cachingStr), - hasBonus(false) -{ -} - -//This constructor should be placed here to avoid side effects -CCheckProxy::CCheckProxy(const CCheckProxy & other) = default; - -bool CCheckProxy::getHasBonus() const -{ - const auto treeVersion = target->getTreeVersion(); - - if(treeVersion != cachedLast) - { - hasBonus = target->hasBonus(selector, cachingStr); - cachedLast = treeVersion; - } - - return hasBonus; -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/CBonusProxy.h b/lib/bonuses/CBonusProxy.h deleted file mode 100644 index f7e3d6cbf..000000000 --- a/lib/bonuses/CBonusProxy.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * CBonusProxy.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 "Bonus.h" -#include "BonusSelector.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE CBonusProxy -{ -public: - CBonusProxy(const IBonusBearer * Target, CSelector Selector); - CBonusProxy(const CBonusProxy & other); - CBonusProxy(CBonusProxy && other) noexcept; - - CBonusProxy & operator=(CBonusProxy && other) noexcept; - CBonusProxy & operator=(const CBonusProxy & other); - const BonusList * operator->() const; - TConstBonusListPtr getBonusList() const; - -protected: - CSelector selector; - const IBonusBearer * target; - mutable int64_t bonusListCachedLast; - mutable TConstBonusListPtr bonusList[2]; - mutable int currentBonusListIndex; - mutable boost::mutex swapGuard; - void swapBonusList(TConstBonusListPtr other) const; -}; - -class DLL_LINKAGE CTotalsProxy : public CBonusProxy -{ -public: - CTotalsProxy(const IBonusBearer * Target, CSelector Selector, int InitialValue); - CTotalsProxy(const CTotalsProxy & other); - CTotalsProxy(CTotalsProxy && other) = delete; - - CTotalsProxy & operator=(const CTotalsProxy & other) = default; - CTotalsProxy & operator=(CTotalsProxy && other) = delete; - - int getMeleeValue() const; - int getRangedValue() const; - int getValue() const; - /** - Returns total value of all selected bonuses and sets bonusList as a pointer to the list of selected bonuses - @param bonusList is the out list of all selected bonuses - @return total value of all selected bonuses and 0 otherwise - */ - int getValueAndList(TConstBonusListPtr & bonusList) const; - -private: - int initialValue; - - mutable int64_t valueCachedLast = 0; - mutable int value = 0; - - mutable int64_t meleeCachedLast; - mutable int meleeValue; - - mutable int64_t rangedCachedLast; - mutable int rangedValue; -}; - -class DLL_LINKAGE CCheckProxy -{ -public: - CCheckProxy(const IBonusBearer * Target, CSelector Selector, const std::string & cachingStr); - CCheckProxy(const IBonusBearer * Target, BonusType bonusType); - CCheckProxy(const CCheckProxy & other); - CCheckProxy& operator= (const CCheckProxy & other) = default; - - bool getHasBonus() const; - -private: - const IBonusBearer * target; - std::string cachingStr; - CSelector selector; - - mutable int64_t cachedLast; - mutable bool hasBonus; -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/IBonusBearer.cpp b/lib/bonuses/IBonusBearer.cpp index 2e6f29d12..532e6ef14 100644 --- a/lib/bonuses/IBonusBearer.cpp +++ b/lib/bonuses/IBonusBearer.cpp @@ -105,7 +105,7 @@ bool IBonusBearer::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const bool IBonusBearer::hasBonusFrom(BonusSource source, BonusSourceID sourceID) const { - std::string cachingStr = "source_" + std::to_string(static_cast(source)) + "_" + sourceID.toString(); + std::string cachingStr = "source_" + std::to_string(static_cast(source)) + "_" + std::to_string(sourceID.getNum()); return hasBonus(Selector::source(source,sourceID), cachingStr); } diff --git a/lib/bonuses/Updaters.cpp b/lib/bonuses/Updaters.cpp index 064818a58..7ce42981a 100644 --- a/lib/bonuses/Updaters.cpp +++ b/lib/bonuses/Updaters.cpp @@ -111,18 +111,6 @@ ArmyMovementUpdater::ArmyMovementUpdater(int base, int divider, int multiplier, std::shared_ptr ArmyMovementUpdater::createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const { - if(b->type == BonusType::MOVEMENT && context.getNodeType() == CBonusSystemNode::HERO) - { - auto speed = static_cast(context).getLowestCreatureSpeed(); - si32 armySpeed = speed * base / divider; - auto counted = armySpeed * multiplier; - auto newBonus = std::make_shared(*b); - newBonus->source = BonusSource::ARMY; - newBonus->val += vstd::amin(counted, max); - return newBonus; - } - if(b->type != BonusType::MOVEMENT) - logGlobal->error("ArmyMovementUpdater should only be used for MOVEMENT bonus!"); return b; } diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 7d94bb1b8..af916ca84 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -171,6 +171,26 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader ret.outroVideo = VideoPath::fromJson(reader["outroVideo"]); } +JsonNode CampaignHandler::writeHeaderToJson(CampaignHeader & header) +{ + JsonNode node; + node["version"].Integer() = static_cast(CampaignVersion::VCMI); + node["regions"] = CampaignRegions::toJson(header.campaignRegions); + node["name"].String() = header.name.toString(); + node["description"].String() = header.description.toString(); + node["author"].String() = header.author.toString(); + node["authorContact"].String() = header.authorContact.toString(); + node["campaignVersion"].String() = header.campaignVersion.toString(); + node["creationDateTime"].Integer() = header.creationDateTime; + node["allowDifficultySelection"].Bool() = header.difficultyChosenByPlayer; + node["music"].String() = header.music.getName(); + node["loadingBackground"].String() = header.loadingBackground.getName(); + node["videoRim"].String() = header.videoRim.getName(); + node["introVideo"].String() = header.introVideo.getName(); + node["outroVideo"].String() = header.outroVideo.getName(); + return node; +} + CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) { auto prologEpilogReader = [](JsonNode & identifier) -> CampaignScenarioPrologEpilog @@ -203,56 +223,86 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) return ret; } +JsonNode CampaignHandler::writeScenarioToJson(const CampaignScenario & scenario) +{ + auto prologEpilogWriter = [](const CampaignScenarioPrologEpilog & elem) -> JsonNode + { + JsonNode node; + if(elem.hasPrologEpilog) + { + node["video"].String() = elem.prologVideo.getName(); + node["music"].String() = elem.prologMusic.getName(); + node["voice"].String() = elem.prologVoice.getName(); + node["text"].String() = elem.prologText.toString(); + } + return node; + }; + + JsonNode node; + node["map"].String() = scenario.mapName; + for(auto & g : scenario.preconditionRegions) + node["preconditions"].Vector().push_back(JsonNode(static_cast(g))); + node["color"].Integer() = scenario.regionColor; + node["difficulty"].Integer() = scenario.difficulty; + node["regionText"].String() = scenario.regionText.toString(); + node["prolog"] = prologEpilogWriter(scenario.prolog); + node["epilog"] = prologEpilogWriter(scenario.epilog); + + writeScenarioTravelToJson(node, scenario.travelOptions); + + return node; +} + +static const std::map startOptionsMap = { + {"none", CampaignStartOptions::NONE}, + {"bonus", CampaignStartOptions::START_BONUS}, + {"crossover", CampaignStartOptions::HERO_CROSSOVER}, + {"hero", CampaignStartOptions::HERO_OPTIONS} +}; + +static const std::map bonusTypeMap = { + {"spell", CampaignBonusType::SPELL}, + {"creature", CampaignBonusType::MONSTER}, + {"building", CampaignBonusType::BUILDING}, + {"artifact", CampaignBonusType::ARTIFACT}, + {"scroll", CampaignBonusType::SPELL_SCROLL}, + {"primarySkill", CampaignBonusType::PRIMARY_SKILL}, + {"secondarySkill", CampaignBonusType::SECONDARY_SKILL}, + {"resource", CampaignBonusType::RESOURCE}, + //{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO}, + //{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO}, +}; + +static const std::map primarySkillsMap = { + {"attack", 0}, + {"defence", 8}, + {"spellpower", 16}, + {"knowledge", 24}, +}; + +static const std::map heroSpecialMap = { + {"strongest", 0xFFFD}, + {"generated", 0xFFFE}, + {"random", 0xFFFF} +}; + +static const std::map resourceTypeMap = { + //FD - wood+ore + //FE - mercury+sulfur+crystal+gem + {"wood", 0}, + {"mercury", 1}, + {"ore", 2}, + {"sulfur", 3}, + {"crystal", 4}, + {"gems", 5}, + {"gold", 6}, + {"common", 0xFD}, + {"rare", 0xFE} +}; + CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) { CampaignTravel ret; - - std::map startOptionsMap = { - {"none", CampaignStartOptions::NONE}, - {"bonus", CampaignStartOptions::START_BONUS}, - {"crossover", CampaignStartOptions::HERO_CROSSOVER}, - {"hero", CampaignStartOptions::HERO_OPTIONS} - }; - - std::map bonusTypeMap = { - {"spell", CampaignBonusType::SPELL}, - {"creature", CampaignBonusType::MONSTER}, - {"building", CampaignBonusType::BUILDING}, - {"artifact", CampaignBonusType::ARTIFACT}, - {"scroll", CampaignBonusType::SPELL_SCROLL}, - {"primarySkill", CampaignBonusType::PRIMARY_SKILL}, - {"secondarySkill", CampaignBonusType::SECONDARY_SKILL}, - {"resource", CampaignBonusType::RESOURCE}, - //{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO}, - //{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO}, - }; - - std::map primarySkillsMap = { - {"attack", 0}, - {"defence", 8}, - {"spellpower", 16}, - {"knowledge", 24}, - }; - - std::map heroSpecialMap = { - {"strongest", 0xFFFD}, - {"generated", 0xFFFE}, - {"random", 0xFFFF} - }; - - std::map resourceTypeMap = { - //FD - wood+ore - //FE - mercury+sulfur+crystal+gem - {"wood", 0}, - {"mercury", 1}, - {"ore", 2}, - {"sulfur", 3}, - {"crystal", 4}, - {"gems", 5}, - {"gold", 6}, - {"common", 0xFD}, - {"rare", 0xFE} - }; for(auto & k : reader["heroKeeps"].Vector()) { @@ -278,7 +328,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) logGlobal->warn("VCMP Loading: keepArtifacts contains unresolved identifier %s", k.String()); } - ret.startOptions = startOptionsMap[reader["startOptions"].String()]; + ret.startOptions = startOptionsMap.at(reader["startOptions"].String()); switch(ret.startOptions) { case CampaignStartOptions::NONE: @@ -290,11 +340,11 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) for(auto & bjson : reader["bonuses"].Vector()) { CampaignBonus bonus; - bonus.type = bonusTypeMap[bjson["what"].String()]; + bonus.type = bonusTypeMap.at(bjson["what"].String()); switch (bonus.type) { case CampaignBonusType::RESOURCE: - bonus.info1 = resourceTypeMap[bjson["type"].String()]; + bonus.info1 = resourceTypeMap.at(bjson["type"].String()); bonus.info2 = bjson["amount"].Integer(); break; @@ -305,7 +355,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) break; default: - if(int heroId = heroSpecialMap[bjson["hero"].String()]) + if(int heroId = heroSpecialMap.at(bjson["hero"].String())) bonus.info1 = heroId; else if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String())) @@ -368,7 +418,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) bonus.type = CampaignBonusType::HERO; bonus.info1 = bjson["playerColor"].Integer(); //player color - if(int heroId = heroSpecialMap[bjson["hero"].String()]) + if(int heroId = heroSpecialMap.at(bjson["hero"].String())) bonus.info2 = heroId; else if (auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String())) @@ -390,6 +440,109 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) return ret; } +void CampaignHandler::writeScenarioTravelToJson(JsonNode & node, const CampaignTravel & travel) +{ + if(travel.whatHeroKeeps.experience) + node["heroKeeps"].Vector().push_back(JsonNode("experience")); + if(travel.whatHeroKeeps.primarySkills) + node["heroKeeps"].Vector().push_back(JsonNode("primarySkills")); + if(travel.whatHeroKeeps.secondarySkills) + node["heroKeeps"].Vector().push_back(JsonNode("secondarySkills")); + if(travel.whatHeroKeeps.spells) + node["heroKeeps"].Vector().push_back(JsonNode("spells")); + if(travel.whatHeroKeeps.artifacts) + node["heroKeeps"].Vector().push_back(JsonNode("artifacts")); + for(auto & c : travel.monstersKeptByHero) + node["keepCreatures"].Vector().push_back(JsonNode(CreatureID::encode(c))); + for(auto & a : travel.artifactsKeptByHero) + node["keepArtifacts"].Vector().push_back(JsonNode(ArtifactID::encode(a))); + node["startOptions"].String() = vstd::reverseMap(startOptionsMap)[travel.startOptions]; + + switch(travel.startOptions) + { + case CampaignStartOptions::NONE: + break; + case CampaignStartOptions::START_BONUS: + { + node["playerColor"].String() = PlayerColor::encode(travel.playerColor); + for(auto & bonus : travel.bonusesToChoose) + { + JsonNode bnode; + bnode["what"].String() = vstd::reverseMap(bonusTypeMap)[bonus.type]; + switch (bonus.type) + { + case CampaignBonusType::RESOURCE: + bnode["type"].String() = vstd::reverseMap(resourceTypeMap)[bonus.info1]; + bnode["amount"].Integer() = bonus.info2; + break; + case CampaignBonusType::BUILDING: + bnode["type"].String() = EBuildingType::names[bonus.info1]; + break; + default: + if(vstd::contains(vstd::reverseMap(heroSpecialMap), bonus.info1)) + bnode["hero"].String() = vstd::reverseMap(heroSpecialMap)[bonus.info1]; + else + bnode["hero"].String() = HeroTypeID::encode(bonus.info1); + bnode["amount"].Integer() = bonus.info3; + switch(bonus.type) + { + case CampaignBonusType::SPELL: + bnode["type"].String() = SpellID::encode(bonus.info2); + break; + case CampaignBonusType::MONSTER: + bnode["type"].String() = CreatureID::encode(bonus.info2); + break; + case CampaignBonusType::SECONDARY_SKILL: + bnode["type"].String() = SecondarySkill::encode(bonus.info2); + break; + case CampaignBonusType::ARTIFACT: + bnode["type"].String() = ArtifactID::encode(bonus.info2); + break; + case CampaignBonusType::SPELL_SCROLL: + bnode["type"].String() = SpellID::encode(bonus.info2); + break; + case CampaignBonusType::PRIMARY_SKILL: + for(auto & ps : primarySkillsMap) + bnode[ps.first].Integer() = (bonus.info2 >> ps.second) & 0xff; + break; + default: + bnode["type"].Integer() = bonus.info2; + } + break; + } + node["bonuses"].Vector().push_back(bnode); + } + break; + } + case CampaignStartOptions::HERO_CROSSOVER: + { + for(auto & bonus : travel.bonusesToChoose) + { + JsonNode bnode; + bnode["playerColor"].Integer() = bonus.info1; + bnode["scenario"].Integer() = bonus.info2; + node["bonuses"].Vector().push_back(bnode); + } + break; + } + case CampaignStartOptions::HERO_OPTIONS: + { + for(auto & bonus : travel.bonusesToChoose) + { + JsonNode bnode; + bnode["playerColor"].Integer() = bonus.info1; + + if(vstd::contains(vstd::reverseMap(heroSpecialMap), bonus.info2)) + bnode["hero"].String() = vstd::reverseMap(heroSpecialMap)[bonus.info2]; + else + bnode["hero"].String() = HeroTypeID::encode(bonus.info2); + + node["bonuses"].Vector().push_back(bnode); + } + break; + } + } +} void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding ) { diff --git a/lib/campaign/CampaignHandler.h b/lib/campaign/CampaignHandler.h index cda84fb05..bd97b2ed5 100644 --- a/lib/campaign/CampaignHandler.h +++ b/lib/campaign/CampaignHandler.h @@ -26,6 +26,9 @@ class DLL_LINKAGE CampaignHandler static CampaignScenario readScenarioFromJson(JsonNode & reader); static CampaignTravel readScenarioTravelFromJson(JsonNode & reader); + //writer for VCMI campaigns (*.vcmp) + static void writeScenarioTravelToJson(JsonNode & node, const CampaignTravel & travel); + //parsers for original H3C campaigns static void readHeaderFromMemory(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding); static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, CampaignHeader & header); @@ -43,6 +46,10 @@ public: static std::unique_ptr getHeader( const std::string & name); //name - name of appropriate file static std::shared_ptr getCampaign(const std::string & name); //name - name of appropriate file + + //writer for VCMI campaigns (*.vcmp) + static JsonNode writeHeaderToJson(CampaignHeader & header); + static JsonNode writeScenarioToJson(const CampaignScenario & scenario); }; VCMI_LIB_NAMESPACE_END diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index fee9b1085..71d0fffe4 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -45,6 +45,22 @@ CampaignRegions::RegionDescription CampaignRegions::RegionDescription::fromJson( return rd; } +JsonNode CampaignRegions::RegionDescription::toJson(CampaignRegions::RegionDescription & rd) +{ + JsonNode node; + node["infix"].String() = rd.infix; + node["x"].Float() = rd.pos.x; + node["y"].Float() = rd.pos.y; + if(rd.labelPos != std::nullopt) + { + node["labelPos"]["x"].Float() = (*rd.labelPos).x; + node["labelPos"]["y"].Float() = (*rd.labelPos).y; + } + else + node["labelPos"].clear(); + return node; +} + CampaignRegions CampaignRegions::fromJson(const JsonNode & node) { CampaignRegions cr; @@ -59,6 +75,25 @@ CampaignRegions CampaignRegions::fromJson(const JsonNode & node) return cr; } +JsonNode CampaignRegions::toJson(CampaignRegions cr) +{ + JsonNode node; + node["prefix"].String() = cr.campPrefix; + node["colorSuffixLength"].Float() = cr.colorSuffixLength; + if(!cr.campSuffix.size()) + node["suffix"].clear(); + else + node["suffix"].Vector() = JsonVector{ JsonNode(cr.campSuffix[0]), JsonNode(cr.campSuffix[1]), JsonNode(cr.campSuffix[2]) }; + if(cr.campBackground.empty()) + node["background"].clear(); + else + node["background"].String() = cr.campBackground; + node["desc"].Vector() = JsonVector(); + for(auto & region : cr.regions) + node["desc"].Vector().push_back(CampaignRegions::RegionDescription::toJson(region)); + return node; +} + CampaignRegions CampaignRegions::getLegacy(int campId) { static std::vector campDescriptions; diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index c4103c128..78017482d 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -59,6 +59,7 @@ class DLL_LINKAGE CampaignRegions } static CampaignRegions::RegionDescription fromJson(const JsonNode & node); + static JsonNode toJson(CampaignRegions::RegionDescription & rd); }; std::vector regions; @@ -86,6 +87,7 @@ public: } static CampaignRegions fromJson(const JsonNode & node); + static JsonNode toJson(CampaignRegions cr); static CampaignRegions getLegacy(int campId); }; diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 72c896d47..8b329a98a 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -325,7 +325,14 @@ private: public: static Type getDwellingFromLevel(int level, int upgradeIndex) { - return getDwellings()[upgradeIndex][level]; + try + { + return getDwellings().at(upgradeIndex).at(level); + } + catch (const std::out_of_range & e) + { + return Type::NONE; + } } static int getLevelFromDwelling(BuildingIDBase dwelling) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index fe7e3264d..a633a6d03 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -566,7 +566,7 @@ void CGameState::placeStartingHeroes() continue; HeroTypeID heroTypeId = pickNextHeroType(playerColor); - if(playerSettingPair.second.hero == HeroTypeID::NONE) + if(playerSettingPair.second.hero == HeroTypeID::NONE || playerSettingPair.second.hero == HeroTypeID::RANDOM) playerSettingPair.second.hero = heroTypeId; placeStartingHero(playerColor, HeroTypeID(heroTypeId), playerInfo.posOfMainTown); diff --git a/lib/json/JsonRandom.cpp b/lib/json/JsonRandom.cpp index 7b1f9dabd..90a2ac34d 100644 --- a/lib/json/JsonRandom.cpp +++ b/lib/json/JsonRandom.cpp @@ -35,6 +35,21 @@ VCMI_LIB_NAMESPACE_BEGIN +std::string JsonRandomizationException::cleanupJson(const JsonNode & value) +{ + std::string result = value.toCompactString(); + for (size_t i = 0; i < result.size(); ++i) + if (result[i] == '\n') + result[i] = ' '; + + return result; +} + +JsonRandomizationException::JsonRandomizationException(const std::string & message, const JsonNode & input) + : std::runtime_error(message + " Input was: " + cleanupJson(input)) +{} + + si32 JsonRandom::loadVariable(const std::string & variableGroup, const std::string & value, const Variables & variables, si32 defaultValue) { if (value.empty() || value[0] != '@') @@ -483,7 +498,10 @@ VCMI_LIB_NAMESPACE_BEGIN if (!potentialPicks.empty()) pickedCreature = *RandomGeneratorUtil::nextItem(potentialPicks, rng); else - logMod->warn("Failed to select suitable random creature!"); + throw JsonRandomizationException("No potential creatures to pick!", value); + + if (!pickedCreature.hasValue()) + throw JsonRandomizationException("Invalid creature picked!", value); stack.setType(pickedCreature.toCreature()); stack.count = loadValue(value, rng, variables); diff --git a/lib/json/JsonRandom.h b/lib/json/JsonRandom.h index 5098dc306..c174286f8 100644 --- a/lib/json/JsonRandom.h +++ b/lib/json/JsonRandom.h @@ -27,6 +27,13 @@ struct Bonus; struct Component; class CStackBasicDescriptor; +class JsonRandomizationException : public std::runtime_error +{ + std::string cleanupJson(const JsonNode & value); +public: + JsonRandomizationException(const std::string & message, const JsonNode & input); +}; + class JsonRandom : public GameCallbackHolder { public: diff --git a/lib/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index ed0c92142..837e94f21 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -11,6 +11,7 @@ #include "CRewardableConstructor.h" #include "../json/JsonUtils.h" +#include "../json/JsonRandom.h" #include "../mapObjects/CRewardableObject.h" #include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" @@ -49,7 +50,14 @@ Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCal { Rewardable::Configuration result; result.variables.preset = presetVariables; - objectInfo.configureObject(result, rand, cb); + + try { + objectInfo.configureObject(result, rand, cb); + } + catch (const JsonRandomizationException & e) + { + throw std::runtime_error("Failed to generate configuration for object '" + getJsonKey() + "'! Reason: " + e.what()); + } for(auto & rewardInfo : result.info) { diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index f2783662d..eddaf2d95 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -46,7 +46,7 @@ CArmedInstance::CArmedInstance(IGameCallback *cb) CArmedInstance::CArmedInstance(IGameCallback *cb, bool isHypothetic): CGObjectInstance(cb), CBonusSystemNode(isHypothetic), - nonEvilAlignmentMix(this, BonusType::NONEVIL_ALIGNMENT_MIX), // Take Angelic Alliance troop-mixing freedom of non-evil units into account. + nonEvilAlignmentMix(this, Selector::type()(BonusType::NONEVIL_ALIGNMENT_MIX)), // Take Angelic Alliance troop-mixing freedom of non-evil units into account. battle(nullptr) { } @@ -86,7 +86,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() size_t factionsInArmy = factions.size(); //town garrison seems to take both sets into account - if (nonEvilAlignmentMix.getHasBonus()) + if (nonEvilAlignmentMix.hasBonus()) { size_t mixableFactions = 0; diff --git a/lib/mapObjects/CArmedInstance.h b/lib/mapObjects/CArmedInstance.h index 8c50cba16..cbd943ea0 100644 --- a/lib/mapObjects/CArmedInstance.h +++ b/lib/mapObjects/CArmedInstance.h @@ -11,8 +11,8 @@ #include "CGObjectInstance.h" #include "../CCreatureSet.h" -#include "../bonuses/CBonusProxy.h" #include "../bonuses/CBonusSystemNode.h" +#include "../bonuses/BonusCache.h" VCMI_LIB_NAMESPACE_BEGIN @@ -23,8 +23,7 @@ class JsonSerializeFormat; class DLL_LINKAGE CArmedInstance: public CGObjectInstance, public CBonusSystemNode, public CCreatureSet, public IConstBonusProvider { private: - CCheckProxy nonEvilAlignmentMix; - static CSelector nonEvilAlignmentMixSelector; + BonusValueCache nonEvilAlignmentMix; public: BattleInfo *battle; //set to the current battle, if engaged diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 81ef94c21..472834210 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -73,31 +73,6 @@ void CGHeroPlaceholder::serializeJsonOptions(JsonSerializeFormat & handler) handler.serializeInt("powerRank", powerRank.value()); } -static int lowestSpeed(const CGHeroInstance * chi) -{ - static const CSelector selectorSTACKS_SPEED = Selector::type()(BonusType::STACKS_SPEED); - static const std::string keySTACKS_SPEED = "type_" + std::to_string(static_cast(BonusType::STACKS_SPEED)); - - if(!chi->stacksCount()) - { - if(chi->commander && chi->commander->alive) - { - return chi->commander->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED); - } - - logGlobal->error("Hero %d (%s) has no army!", chi->id.getNum(), chi->getNameTranslated()); - return 20; - } - - auto i = chi->Slots().begin(); - //TODO? should speed modifiers (eg from artifacts) affect hero movement? - - int ret = (i++)->second->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED); - for(; i != chi->Slots().end(); i++) - ret = std::min(ret, i->second->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED)); - return ret; -} - ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const { int64_t ret = GameConstants::BASE_MOVEMENT_COST; @@ -107,13 +82,10 @@ ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const Terrain { ret = from.getRoad()->movementCost; } - else if(ti->nativeTerrain != from.getTerrainID() &&//the terrain is not native - ti->nativeTerrain != ETerrainId::ANY_TERRAIN && //no special creature bonus - !ti->hasBonusOfType(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(from.getTerrainID()))) //no special movement bonus + else if(!ti->hasNoTerrainPenalty(from.getTerrainID())) //no special movement bonus { - ret = VLC->terrainTypeHandler->getById(from.getTerrainID())->moveCost; - ret -= ti->valOfBonuses(BonusType::ROUGH_TERRAIN_DISCOUNT); + ret -= ti->getRoughTerrainDiscountValue(); if(ret < GameConstants::BASE_MOVEMENT_COST) ret = GameConstants::BASE_MOVEMENT_COST; } @@ -257,30 +229,41 @@ void CGHeroInstance::setMovementPoints(int points) int CGHeroInstance::movementPointsLimit(bool onLand) const { - return valOfBonuses(BonusType::MOVEMENT, onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea); + auto ti = getTurnInfo(0); + return onLand ? ti->getMovePointsLimitLand() : ti->getMovePointsLimitWater(); } int CGHeroInstance::getLowestCreatureSpeed() const { - return lowestCreatureSpeed; + if(stacksCount() != 0) + { + int minimalSpeed = std::numeric_limits::max(); + //TODO? should speed modifiers (eg from artifacts) affect hero movement? + for(const auto & slot : Slots()) + minimalSpeed = std::min(minimalSpeed, slot.second->getInitiative()); + + return minimalSpeed; + } + else + { + if(commander && commander->alive) + return commander->getInitiative(); + } + + return 10; } -void CGHeroInstance::updateArmyMovementBonus(bool onLand, const TurnInfo * ti) const +std::unique_ptr CGHeroInstance::getTurnInfo(int days) const { - auto realLowestSpeed = lowestSpeed(this); - if(lowestCreatureSpeed != realLowestSpeed) - { - lowestCreatureSpeed = realLowestSpeed; - //Let updaters run again - treeHasChanged(); - ti->updateHeroBonuses(BonusType::MOVEMENT); - } + return std::make_unique(turnInfoCache.get(), this, days); } int CGHeroInstance::movementPointsLimitCached(bool onLand, const TurnInfo * ti) const { - updateArmyMovementBonus(onLand, ti); - return ti->valOfBonuses(BonusType::MOVEMENT, onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea); + if (onLand) + return ti->getMovePointsLimitLand(); + else + return ti->getMovePointsLimitWater(); } CGHeroInstance::CGHeroInstance(IGameCallback * cb) @@ -293,7 +276,10 @@ CGHeroInstance::CGHeroInstance(IGameCallback * cb) level(1), exp(UNINITIALIZED_EXPERIENCE), gender(EHeroGender::DEFAULT), - lowestCreatureSpeed(0) + primarySkills(this), + magicSchoolMastery(this), + turnInfoCache(std::make_unique(this)), + manaPerKnowledgeCached(this, Selector::type()(BonusType::MANA_PER_KNOWLEDGE_PERCENTAGE)) { setNodeType(HERO); ID = Obj::HERO; @@ -704,40 +690,20 @@ void CGHeroInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier) setStackCount(SlotID(0), identifier.getNum()); } -std::array CGHeroInstance::getPrimarySkills() const +int CGHeroInstance::getPrimSkillLevel(PrimarySkill id) const { - std::array result; - - auto allSkills = getBonusBearer()->getBonusesOfType(BonusType::PRIMARY_SKILL); - for (auto skill : PrimarySkill::ALL_SKILLS()) - { - int ret = allSkills->valOfBonuses(Selector::subtype()(BonusSubtypeID(skill))); - int minSkillValue = VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, skill.getNum()); - result[skill] = std::max(ret, minSkillValue); //otherwise, some artifacts may cause negative skill value effect - } - - return result; + return primarySkills.getSkills()[id]; } double CGHeroInstance::getFightingStrength() const { - const auto & primarySkills = getPrimarySkills(); - return getFightingStrengthImpl(primarySkills); -} - -double CGHeroInstance::getFightingStrengthImpl(const std::array & primarySkills) const -{ - return sqrt((1.0 + 0.05*primarySkills[PrimarySkill::ATTACK]) * (1.0 + 0.05*primarySkills[PrimarySkill::DEFENSE])); + const auto & skillValues = primarySkills.getSkills(); + return sqrt((1.0 + 0.05*skillValues[PrimarySkill::ATTACK]) * (1.0 + 0.05*skillValues[PrimarySkill::DEFENSE])); } double CGHeroInstance::getMagicStrength() const { - const auto & primarySkills = getPrimarySkills(); - return getMagicStrengthImpl(primarySkills); -} - -double CGHeroInstance::getMagicStrengthImpl(const std::array & primarySkills) const -{ + const auto & skillValues = primarySkills.getSkills(); if (!hasSpellbook()) return 1; bool atLeastOneCombatSpell = false; @@ -751,13 +717,12 @@ double CGHeroInstance::getMagicStrengthImpl(const std::array & primarySk } if (!atLeastOneCombatSpell) return 1; - return sqrt((1.0 + 0.05*primarySkills[PrimarySkill::KNOWLEDGE] * mana / manaLimit()) * (1.0 + 0.05*primarySkills[PrimarySkill::SPELL_POWER] * mana / manaLimit())); + return sqrt((1.0 + 0.05*skillValues[PrimarySkill::KNOWLEDGE] * mana / manaLimit()) * (1.0 + 0.05*skillValues[PrimarySkill::SPELL_POWER] * mana / manaLimit())); } double CGHeroInstance::getHeroStrength() const { - const auto & primarySkills = getPrimarySkills(); - return getFightingStrengthImpl(primarySkills) * getMagicStrengthImpl(primarySkills); + return getFightingStrength() * getMagicStrength(); } uint64_t CGHeroInstance::getValueForDiplomacy() const @@ -809,7 +774,7 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, SpellSc spell->forEachSchool([&, this](const SpellSchool & cnf, bool & stop) { - int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(cnf)); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) + int32_t thisSchool = magicSchoolMastery.getMastery(cnf); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) if(thisSchool > skill) { skill = thisSchool; @@ -818,7 +783,7 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, SpellSc } }); - vstd::amax(skill, valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(SpellSchool::ANY))); //any school bonus + vstd::amax(skill, magicSchoolMastery.getMastery(SpellSchool::ANY)); //any school bonus vstd::amax(skill, valOfBonuses(BonusType::SPELL, BonusSubtypeID(spell->getId()))); //given by artifact or other effect vstd::amax(skill, 0); //in case we don't know any school @@ -1207,8 +1172,7 @@ std::string CGHeroInstance::nodeName() const si32 CGHeroInstance::manaLimit() const { - return si32(getPrimSkillLevel(PrimarySkill::KNOWLEDGE) - * (valOfBonuses(BonusType::MANA_PER_KNOWLEDGE_PERCENTAGE))) / 100; + return getPrimSkillLevel(PrimarySkill::KNOWLEDGE) * manaPerKnowledgeCached.getValue() / 100; } HeroTypeID CGHeroInstance::getPortraitSource() const @@ -1381,14 +1345,7 @@ CBonusSystemNode & CGHeroInstance::whereShouldBeAttached(CGameState * gs) int CGHeroInstance::movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark, const TurnInfo * ti) const { - std::unique_ptr turnInfoLocal; - if(!ti) - { - turnInfoLocal = std::make_unique(this); - ti = turnInfoLocal.get(); - } - - if(!ti->hasBonusOfType(BonusType::FREE_SHIP_BOARDING)) + if(!ti->hasFreeShipBoarding()) return 0; // take all MPs by default auto boatLayer = boat ? boat->layer : EPathfindingLayer::SAIL; diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 2a7494415..1f66d678b 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -14,6 +14,7 @@ #include "CArmedInstance.h" #include "IOwnableObject.h" +#include "../bonuses/BonusCache.h" #include "../entities/hero/EHeroGender.h" #include "../CArtHandler.h" // For CArtifactSet @@ -24,8 +25,10 @@ class CGBoat; class CGTownInstance; class CMap; class UpgradeInfo; +class TurnInfo; + struct TerrainTile; -struct TurnInfo; +struct TurnInfoCache; class DLL_LINKAGE CGHeroPlaceholder : public CGObjectInstance { @@ -58,12 +61,13 @@ class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator, friend class CMapFormatJson; private: - std::set spells; //known spells (spell IDs) - mutable int lowestCreatureSpeed; - ui32 movement; //remaining movement points + PrimarySkillsCache primarySkills; + MagicSchoolMasteryCache magicSchoolMastery; + BonusValueCache manaPerKnowledgeCached; + std::unique_ptr turnInfoCache; - double getFightingStrengthImpl(const std::array & primarySkills) const; - double getMagicStrengthImpl(const std::array & primarySkills) const; + std::set spells; //known spells (spell IDs) + ui32 movement; //remaining movement points public: @@ -204,7 +208,7 @@ public: std::vector getLevelUpProposedSecondarySkills(vstd::RNG & rand) const; ui8 getSecSkillLevel(const SecondarySkill & skill) const; //0 - no skill - std::array getPrimarySkills() const; + int getPrimSkillLevel(PrimarySkill id) const; /// Returns true if hero has free secondary skill slot. bool canLearnSkill() const; @@ -222,10 +226,10 @@ public: int movementPointsLimit(bool onLand) const; //cached version is much faster, TurnInfo construction is costly int movementPointsLimitCached(bool onLand, const TurnInfo * ti) const; - //update army movement bonus - void updateArmyMovementBonus(bool onLand, const TurnInfo * ti) const; - int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark = false, const TurnInfo * ti = nullptr) const; + int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark, const TurnInfo * ti) const; + + std::unique_ptr getTurnInfo(int days) const; double getFightingStrength() const; // takes attack / defense skill into account double getMagicStrength() const; // takes knowledge / spell power skill but also current mana, whether the hero owns a spell-book and whether that books contains anything into account diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 819d15723..5bf021a54 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1134,6 +1134,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) eventsHandler.syncSize(events, JsonNode::JsonType::DATA_VECTOR); eventsHandler.serializeStruct(events); } + handler.serializeId("alignmentToPlayer", alignmentToPlayer, PlayerColor::NEUTRAL); } const CFaction * CGTownInstance::getFaction() const diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 7bb3302f1..55d31ed5e 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -803,6 +803,11 @@ std::string CGKeys::getObjectName() const return VLC->generaltexth->tentColors[subID.getNum()] + " " + CGObjectInstance::getObjectName(); } +std::string CGKeys::getObjectDescription(PlayerColor player) const +{ + return visitedTxt(wasMyColorVisited(player)); +} + bool CGKeymasterTent::wasVisited (PlayerColor player) const { return wasMyColorVisited (player); diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 407c46134..d926a8d15 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -199,7 +199,8 @@ public: bool wasMyColorVisited(const PlayerColor & player) const; - std::string getObjectName() const override; //depending on color + std::string getObjectName() const override; + std::string getObjectDescription(PlayerColor player) const; std::string getHoverText(PlayerColor player) const override; template void serialize(Handler &h) diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 4fe5d3055..2aba32c78 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -1289,6 +1289,11 @@ std::string CGObelisk::getHoverText(PlayerColor player) const return getObjectName() + " " + visitedTxt(wasVisited(player)); } +std::string CGObelisk::getObjectDescription(PlayerColor player) const +{ + return visitedTxt(wasVisited(player)); +} + void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch(what) diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 4562ea255..c3427880b 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -406,6 +406,7 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; void initObj(vstd::RNG & rand) override; std::string getHoverText(PlayerColor player) const override; + std::string getObjectDescription(PlayerColor player) const; template void serialize(Handler &h) { diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 3760c3bc1..8d389cf38 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -143,17 +143,6 @@ TerrainTile::TerrainTile(): { } -bool TerrainTile::entrableTerrain(const TerrainTile * from) const -{ - return entrableTerrain(from ? from->isLand() : true, from ? from->isWater() : true); -} - -bool TerrainTile::entrableTerrain(bool allowLand, bool allowSea) const -{ - return getTerrain()->isPassable() - && ((allowSea && isWater()) || (allowLand && isLand())); -} - bool TerrainTile::isClear(const TerrainTile * from) const { return entrableTerrain(from) && !blocked(); @@ -187,72 +176,6 @@ EDiggingStatus TerrainTile::getDiggingStatus(const bool excludeTop) const return EDiggingStatus::CAN_DIG; } -bool TerrainTile::hasFavorableWinds() const -{ - return extTileFlags & 128; -} - -bool TerrainTile::isWater() const -{ - return getTerrain()->isWater(); -} - -bool TerrainTile::isLand() const -{ - return getTerrain()->isLand(); -} - -bool TerrainTile::visitable() const -{ - return !visitableObjects.empty(); -} - -bool TerrainTile::blocked() const -{ - return !blockingObjects.empty(); -} - -bool TerrainTile::hasRiver() const -{ - return getRiverID() != RiverId::NO_RIVER; -} - -bool TerrainTile::hasRoad() const -{ - return getRoadID() != RoadId::NO_ROAD; -} - -const TerrainType * TerrainTile::getTerrain() const -{ - return terrainType.toEntity(VLC); -} - -const RiverType * TerrainTile::getRiver() const -{ - return riverType.toEntity(VLC); -} - -const RoadType * TerrainTile::getRoad() const -{ - return roadType.toEntity(VLC); -} - -TerrainId TerrainTile::getTerrainID() const -{ - return terrainType; -} - -RiverId TerrainTile::getRiverID() const -{ - return riverType; -} - -RoadId TerrainTile::getRoadID() const -{ - return roadType; -} - - CMap::CMap(IGameCallback * cb) : GameCallbackHolder(cb) , checksum(0) @@ -365,7 +288,7 @@ bool CMap::isCoastalTile(const int3 & pos) const return false; } - if(isWaterTile(pos)) + if(getTile(pos).isWater()) return false; for(const auto & dir : dirs) @@ -382,22 +305,6 @@ bool CMap::isCoastalTile(const int3 & pos) const return false; } -TerrainTile & CMap::getTile(const int3 & tile) -{ - assert(isInTheMap(tile)); - return terrain[tile.z][tile.x][tile.y]; -} - -const TerrainTile & CMap::getTile(const int3 & tile) const -{ - assert(isInTheMap(tile)); - return terrain[tile.z][tile.x][tile.y]; -} - -bool CMap::isWaterTile(const int3 &pos) const -{ - return isInTheMap(pos) && getTile(pos).isWater(); -} bool CMap::canMoveBetween(const int3 &src, const int3 &dst) const { const TerrainTile * dstTile = &getTile(dst); diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 5192f7c1a..4fa4c2744 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -86,18 +86,10 @@ public: void initTerrain(); CMapEditManager * getEditManager(); - TerrainTile & getTile(const int3 & tile); - const TerrainTile & getTile(const int3 & tile) const; + inline TerrainTile & getTile(const int3 & tile); + inline const TerrainTile & getTile(const int3 & tile) const; bool isCoastalTile(const int3 & pos) const; - bool isWaterTile(const int3 & pos) const; - inline bool isInTheMap(const int3 & pos) const - { - // Check whether coord < 0 is done implicitly. Negative signed int overflows to unsigned number larger than all signed ints. - return - static_cast(pos.x) < static_cast(width) && - static_cast(pos.y) < static_cast(height) && - static_cast(pos.z) <= (twoLevel ? 1 : 0); - } + inline bool isInTheMap(const int3 & pos) const; bool canMoveBetween(const int3 &src, const int3 &dst) const; bool checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const; @@ -250,4 +242,25 @@ public: } }; +inline bool CMap::isInTheMap(const int3 & pos) const +{ + // Check whether coord < 0 is done implicitly. Negative signed int overflows to unsigned number larger than all signed ints. + return + static_cast(pos.x) < static_cast(width) && + static_cast(pos.y) < static_cast(height) && + static_cast(pos.z) <= (twoLevel ? 1 : 0); +} + +inline TerrainTile & CMap::getTile(const int3 & tile) +{ + assert(isInTheMap(tile)); + return terrain[tile.z][tile.x][tile.y]; +} + +inline const TerrainTile & CMap::getTile(const int3 & tile) const +{ + assert(isInTheMap(tile)); + return terrain[tile.z][tile.x][tile.y]; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index b8da3b405..c34256506 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -12,7 +12,8 @@ #include "../ResourceSet.h" #include "../texts/MetaString.h" -#include "../int3.h" +#include "../VCMI_Lib.h" +#include "../TerrainHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -103,31 +104,32 @@ struct DLL_LINKAGE TerrainTile TerrainTile(); /// Gets true if the terrain is not a rock. If from is water/land, same type is also required. - bool entrableTerrain(const TerrainTile * from = nullptr) const; - bool entrableTerrain(bool allowLand, bool allowSea) const; + inline bool entrableTerrain() const; + inline bool entrableTerrain(const TerrainTile * from) const; + inline bool entrableTerrain(bool allowLand, bool allowSea) const; /// Checks for blocking objects and terraint type (water / land). bool isClear(const TerrainTile * from = nullptr) const; /// Gets the ID of the top visitable object or -1 if there is none. Obj topVisitableId(bool excludeTop = false) const; CGObjectInstance * topVisitableObj(bool excludeTop = false) const; - bool isWater() const; - bool isLand() const; + inline bool isWater() const; + inline bool isLand() const; EDiggingStatus getDiggingStatus(bool excludeTop = true) const; - bool hasFavorableWinds() const; + inline bool hasFavorableWinds() const; - bool visitable() const; - bool blocked() const; + inline bool visitable() const; + inline bool blocked() const; - const TerrainType * getTerrain() const; - const RiverType * getRiver() const; - const RoadType * getRoad() const; + inline const TerrainType * getTerrain() const; + inline const RiverType * getRiver() const; + inline const RoadType * getRoad() const; - TerrainId getTerrainID() const; - RiverId getRiverID() const; - RoadId getRoadID() const; + inline TerrainId getTerrainID() const; + inline RiverId getRiverID() const; + inline RoadId getRoadID() const; - bool hasRiver() const; - bool hasRoad() const; + inline bool hasRiver() const; + inline bool hasRoad() const; TerrainId terrainType; RiverId riverType; @@ -193,4 +195,86 @@ struct DLL_LINKAGE TerrainTile } }; +inline bool TerrainTile::hasFavorableWinds() const +{ + return extTileFlags & 128; +} + +inline bool TerrainTile::isWater() const +{ + return getTerrain()->isWater(); +} + +inline bool TerrainTile::isLand() const +{ + return getTerrain()->isLand(); +} + +inline bool TerrainTile::visitable() const +{ + return !visitableObjects.empty(); +} + +inline bool TerrainTile::blocked() const +{ + return !blockingObjects.empty(); +} + +inline bool TerrainTile::hasRiver() const +{ + return getRiverID() != RiverId::NO_RIVER; +} + +inline bool TerrainTile::hasRoad() const +{ + return getRoadID() != RoadId::NO_ROAD; +} + +inline const TerrainType * TerrainTile::getTerrain() const +{ + return terrainType.toEntity(VLC); +} + +inline const RiverType * TerrainTile::getRiver() const +{ + return riverType.toEntity(VLC); +} + +inline const RoadType * TerrainTile::getRoad() const +{ + return roadType.toEntity(VLC); +} + +inline TerrainId TerrainTile::getTerrainID() const +{ + return terrainType; +} + +inline RiverId TerrainTile::getRiverID() const +{ + return riverType; +} + +inline RoadId TerrainTile::getRoadID() const +{ + return roadType; +} + +inline bool TerrainTile::entrableTerrain() const +{ + return entrableTerrain(true, true); +} + +inline bool TerrainTile::entrableTerrain(const TerrainTile * from) const +{ + const TerrainType * terrainFrom = from->getTerrain(); + return entrableTerrain(terrainFrom->isLand(), terrainFrom->isWater()); +} + +inline bool TerrainTile::entrableTerrain(bool allowLand, bool allowSea) const +{ + const TerrainType * terrain = getTerrain(); + return terrain->isPassable() && ((allowSea && terrain->isWater()) || (allowLand && terrain->isLand())); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 06518e107..99f9c46ed 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -144,7 +144,7 @@ TModID CModHandler::findResourceOrigin(const ResourcePath & name) const return "core"; if(CResourceHandler::get("mapEditor")->existsResource(name)) - return "core"; // Workaround for loading maps via map editor + return "mapEditor"; // Workaround for loading maps via map editor } catch( const std::out_of_range & e) { @@ -189,6 +189,8 @@ std::string CModHandler::getModLanguage(const TModID& modId) const return VLC->generaltexth->getInstalledLanguage(); if(modId == "map") return VLC->generaltexth->getPreferredLanguage(); + if(modId == "mapEditor") + return VLC->generaltexth->getPreferredLanguage(); return getModInfo(modId).getBaseLanguage(); } diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index 1466bb75c..d953b4d81 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -500,7 +500,7 @@ std::vector CIdentifierStorage::getModsWithFailedRequests() const std::vector result; for (const auto & request : failedRequests) - if (!vstd::contains(result, request.localScope)) + if (!vstd::contains(result, request.localScope) && ModScope::isScopeReserved(request.localScope)) result.push_back(request.localScope); return result; diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index deeb3fa5c..c7dfcdcdf 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -514,11 +514,7 @@ CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Her canCastWaterWalk = Hero->canCastThisSpell(waterWalk.toSpell()); } -CPathfinderHelper::~CPathfinderHelper() -{ - for(auto * ti : turnsInfo) - delete ti; -} +CPathfinderHelper::~CPathfinderHelper() = default; void CPathfinderHelper::updateTurnInfo(const int Turn) { @@ -526,10 +522,7 @@ void CPathfinderHelper::updateTurnInfo(const int Turn) { turn = Turn; if(turn >= turnsInfo.size()) - { - auto * ti = new TurnInfo(hero, turn); - turnsInfo.push_back(ti); - } + turnsInfo.push_back(hero->getTurnInfo(turn)); } } @@ -561,12 +554,7 @@ bool CPathfinderHelper::isLayerAvailable(const EPathfindingLayer & layer) const const TurnInfo * CPathfinderHelper::getTurnInfo() const { - return turnsInfo[turn]; -} - -bool CPathfinderHelper::hasBonusOfType(const BonusType type) const -{ - return turnsInfo[turn]->hasBonusOfType(type); + return turnsInfo[turn].get(); } int CPathfinderHelper::getMaxMovePoints(const EPathfindingLayer & layer) const @@ -575,15 +563,16 @@ int CPathfinderHelper::getMaxMovePoints(const EPathfindingLayer & layer) const } void CPathfinderHelper::getNeighbours( - const TerrainTile & srcTile, + const TerrainTile & sourceTile, const int3 & srcCoord, NeighbourTilesVector & vec, const boost::logic::tribool & onLand, const bool limitCoastSailing) const { CMap * map = gs->map; + const TerrainType * sourceTerrain = sourceTile.getTerrain(); - static const int3 dirs[] = { + static constexpr std::array dirs = { int3(-1, +1, +0), int3(0, +1, +0), int3(+1, +1, +0), int3(-1, +0, +0), /* source pos */ int3(+1, +0, +0), int3(-1, -1, +0), int3(0, -1, +0), int3(+1, -1, +0) @@ -596,12 +585,12 @@ void CPathfinderHelper::getNeighbours( continue; const TerrainTile & destTile = map->getTile(destCoord); - const TerrainType* terrain = destTile.getTerrain(); - if(!terrain->isPassable()) + const TerrainType * destTerrain = destTile.getTerrain(); + if(!destTerrain->isPassable()) continue; /// Following condition let us avoid diagonal movement over coast when sailing - if(srcTile.isWater() && limitCoastSailing && terrain->isWater() && dir.x && dir.y) //diagonal move through water + if(sourceTerrain->isWater() && limitCoastSailing && destTerrain->isWater() && dir.x && dir.y) //diagonal move through water { const int3 horizontalNeighbour = srcCoord + int3{dir.x, 0, 0}; const int3 verticalNeighbour = srcCoord + int3{0, dir.y, 0}; @@ -609,7 +598,7 @@ void CPathfinderHelper::getNeighbours( continue; } - if(indeterminate(onLand) || onLand == terrain->isLand()) + if(indeterminate(onLand) || onLand == destTerrain->isLand()) { vec.push_back(destCoord); } @@ -663,54 +652,46 @@ int CPathfinderHelper::getMovementCost( bool isWaterLayer; if(indeterminate(isDstWaterLayer)) - isWaterLayer = ((hero->boat && hero->boat->layer == EPathfindingLayer::WATER) || ti->hasBonusOfType(BonusType::WATER_WALKING)) && dt->isWater(); + isWaterLayer = ((hero->boat && hero->boat->layer == EPathfindingLayer::WATER) || ti->hasWaterWalking()) && dt->isWater(); else isWaterLayer = static_cast(isDstWaterLayer); - bool isAirLayer = (hero->boat && hero->boat->layer == EPathfindingLayer::AIR) || ti->hasBonusOfType(BonusType::FLYING_MOVEMENT); + bool isAirLayer = (hero->boat && hero->boat->layer == EPathfindingLayer::AIR) || ti->hasFlyingMovement(); - int ret = hero->getTileMovementCost(*dt, *ct, ti); + int movementCost = hero->getTileMovementCost(*dt, *ct, ti); if(isSailLayer) { if(ct->hasFavorableWinds()) - ret = static_cast(ret * 2.0 / 3); + movementCost = static_cast(movementCost * 2.0 / 3); } else if(isAirLayer) - vstd::amin(ret, GameConstants::BASE_MOVEMENT_COST + ti->valOfBonuses(BonusType::FLYING_MOVEMENT)); - else if(isWaterLayer && ti->hasBonusOfType(BonusType::WATER_WALKING)) - ret = static_cast(ret * (100.0 + ti->valOfBonuses(BonusType::WATER_WALKING)) / 100.0); + vstd::amin(movementCost, GameConstants::BASE_MOVEMENT_COST + ti->getFlyingMovementValue()); + else if(isWaterLayer && ti->hasWaterWalking()) + movementCost = static_cast(movementCost * (100.0 + ti->getWaterWalkingValue()) / 100.0); if(src.x != dst.x && src.y != dst.y) //it's diagonal move { - int old = ret; - ret = static_cast(ret * M_SQRT2); + int old = movementCost; + movementCost = static_cast(movementCost * M_SQRT2); //diagonal move costs too much but normal move is possible - allow diagonal move for remaining move points // https://heroes.thelazy.net/index.php/Movement#Diagonal_move_exception - if(ret > remainingMovePoints && remainingMovePoints >= old) + if(movementCost > remainingMovePoints && remainingMovePoints >= old) { return remainingMovePoints; } } - const int left = remainingMovePoints - ret; - constexpr auto maxCostOfOneStep = static_cast(175 * M_SQRT2); // diagonal move on Swamp - 247 MP - if(checkLast && left > 0 && left <= maxCostOfOneStep) //it might be the last tile - if no further move possible we take all move points + //it might be the last tile - if no further move possible we take all move points + const int pointsLeft = remainingMovePoints - movementCost; + if(checkLast && pointsLeft > 0) { - NeighbourTilesVector vec; + int minimalNextMoveCost = hero->getTileMovementCost(*dt, *ct, ti); - getNeighbours(*dt, dst, vec, ct->isLand(), true); - for(const auto & elem : vec) - { - int fcost = getMovementCost(dst, elem, nullptr, nullptr, left, false); - if(fcost <= left) - { - return ret; - } - } - ret = remainingMovePoints; + if (pointsLeft < minimalNextMoveCost) + return remainingMovePoints; } - return ret; + return movementCost; } VCMI_LIB_NAMESPACE_END diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h index 9a903ced1..49d2ced24 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGWhirlpool; -struct TurnInfo; +class TurnInfo; struct PathfinderOptions; // Optimized storage - tile can have 0-8 neighbour tiles @@ -78,7 +78,7 @@ public: int turn; PlayerColor owner; const CGHeroInstance * hero; - std::vector turnsInfo; + std::vector> turnsInfo; const PathfinderOptions & options; bool canCastFly; bool canCastWaterWalk; @@ -93,7 +93,6 @@ public: void updateTurnInfo(const int turn = 0); bool isLayerAvailable(const EPathfindingLayer & layer) const; const TurnInfo * getTurnInfo() const; - bool hasBonusOfType(BonusType type) const; int getMaxMovePoints(const EPathfindingLayer & layer) const; TeleporterTilesVector getCastleGates(const PathNodeInfo & source) const; diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index 17258de4e..d8911332a 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -10,40 +10,161 @@ #include "StdInc.h" #include "TurnInfo.h" +#include "../IGameCallback.h" +#include "../IGameSettings.h" #include "../TerrainHandler.h" #include "../VCMI_Lib.h" #include "../bonuses/BonusList.h" +#include "../json/JsonNode.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/MiscObjects.h" VCMI_LIB_NAMESPACE_BEGIN -TurnInfo::BonusCache::BonusCache(const TConstBonusListPtr & bl) +TConstBonusListPtr TurnInfoBonusList::getBonusList(const CGHeroInstance * target, const CSelector & bonusSelector) { - for(const auto & terrain : VLC->terrainTypeHandler->objects) - { - auto selector = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(terrain->getId())); - if (bl->getFirst(selector)) - noTerrainPenalty.insert(terrain->getId()); - } + std::lock_guard guard(bonusListMutex); - freeShipBoarding = static_cast(bl->getFirst(Selector::type()(BonusType::FREE_SHIP_BOARDING))); - flyingMovement = static_cast(bl->getFirst(Selector::type()(BonusType::FLYING_MOVEMENT))); - flyingMovementVal = bl->valOfBonuses(Selector::type()(BonusType::FLYING_MOVEMENT)); - waterWalking = static_cast(bl->getFirst(Selector::type()(BonusType::WATER_WALKING))); - waterWalkingVal = bl->valOfBonuses(Selector::type()(BonusType::WATER_WALKING)); - pathfindingVal = bl->valOfBonuses(Selector::type()(BonusType::ROUGH_TERRAIN_DISCOUNT)); + if (target->getTreeVersion() == bonusListVersion) + return bonusList; + + bonusList = target->getBonuses(bonusSelector); + bonusListVersion = target->getTreeVersion(); + + return bonusList; } -TurnInfo::TurnInfo(const CGHeroInstance * Hero, const int turn): - hero(Hero), - maxMovePointsLand(-1), - maxMovePointsWater(-1), - turn(turn) +int TurnInfo::hasWaterWalking() const { - bonuses = hero->getAllBonuses(Selector::days(turn), Selector::all, "all_days" + std::to_string(turn)); - bonusCache = std::make_unique(bonuses); - nativeTerrain = hero->getNativeTerrain(); + return waterWalkingTest; +} + +int TurnInfo::hasFlyingMovement() const +{ + return flyingMovementTest; +} + +int TurnInfo::hasNoTerrainPenalty(const TerrainId &terrain) const +{ + return noterrainPenalty[terrain.num]; +} + +int TurnInfo::hasFreeShipBoarding() const +{ + return freeShipBoardingTest; +} + +int TurnInfo::getFlyingMovementValue() const +{ + return flyingMovementValue; +} + +int TurnInfo::getWaterWalkingValue() const +{ + return waterWalkingValue; +} + +int TurnInfo::getRoughTerrainDiscountValue() const +{ + return roughTerrainDiscountValue; +} + +int TurnInfo::getMovePointsLimitLand() const +{ + return movePointsLimitLand; +} + +int TurnInfo::getMovePointsLimitWater() const +{ + return movePointsLimitWater; +} + +TurnInfo::TurnInfo(TurnInfoCache * sharedCache, const CGHeroInstance * target, int Turn) + : target(target) + , noterrainPenalty(VLC->terrainTypeHandler->size()) +{ + CSelector daySelector = Selector::days(Turn); + + int lowestSpeed; + if (target->getTreeVersion() == sharedCache->heroLowestSpeedVersion) + { + lowestSpeed = sharedCache->heroLowestSpeedValue; + } + else + { + lowestSpeed = target->getLowestCreatureSpeed(); + sharedCache->heroLowestSpeedValue = lowestSpeed; + sharedCache->heroLowestSpeedVersion = target->getTreeVersion(); + } + + { + static const CSelector selector = Selector::type()(BonusType::WATER_WALKING); + const auto & bonuses = sharedCache->waterWalking.getBonusList(target, selector); + waterWalkingTest = bonuses->getFirst(daySelector) != nullptr; + waterWalkingValue = bonuses->valOfBonuses(daySelector); + } + + { + static const CSelector selector = Selector::type()(BonusType::FLYING_MOVEMENT); + const auto & bonuses = sharedCache->flyingMovement.getBonusList(target, selector); + flyingMovementTest = bonuses->getFirst(daySelector) != nullptr; + flyingMovementValue = bonuses->valOfBonuses(daySelector); + } + + { + static const CSelector selector = Selector::type()(BonusType::FREE_SHIP_BOARDING); + const auto & bonuses = sharedCache->freeShipBoarding.getBonusList(target, selector); + freeShipBoardingTest = bonuses->getFirst(daySelector) != nullptr; + } + + { + static const CSelector selector = Selector::type()(BonusType::ROUGH_TERRAIN_DISCOUNT); + const auto & bonuses = sharedCache->roughTerrainDiscount.getBonusList(target, selector); + roughTerrainDiscountValue = bonuses->getFirst(daySelector) != nullptr; + } + + { + static const CSelector selector = Selector::typeSubtype(BonusType::MOVEMENT, BonusCustomSubtype::heroMovementSea); + const auto & vectorSea = target->cb->getSettings().getValue(EGameSettings::HEROES_MOVEMENT_POINTS_SEA).Vector(); + const auto & bonuses = sharedCache->movementPointsLimitWater.getBonusList(target, selector); + int baseMovementPointsSea; + if (lowestSpeed < vectorSea.size()) + baseMovementPointsSea = vectorSea[lowestSpeed].Integer(); + else + baseMovementPointsSea = vectorSea.back().Integer(); + + movePointsLimitWater = bonuses->valOfBonuses(daySelector, baseMovementPointsSea); + } + + { + static const CSelector selector = Selector::typeSubtype(BonusType::MOVEMENT, BonusCustomSubtype::heroMovementLand); + const auto & vectorLand = target->cb->getSettings().getValue(EGameSettings::HEROES_MOVEMENT_POINTS_LAND).Vector(); + const auto & bonuses = sharedCache->movementPointsLimitLand.getBonusList(target, selector); + int baseMovementPointsLand; + if (lowestSpeed < vectorLand.size()) + baseMovementPointsLand = vectorLand[lowestSpeed].Integer(); + else + baseMovementPointsLand = vectorLand.back().Integer(); + + movePointsLimitLand = bonuses->valOfBonuses(daySelector, baseMovementPointsLand); + } + + { + static const CSelector selector = Selector::type()(BonusType::NO_TERRAIN_PENALTY); + const auto & bonuses = sharedCache->noTerrainPenalty.getBonusList(target, selector); + for (const auto & bonus : *bonuses) + { + TerrainId affectedTerrain = bonus->subtype.as(); + noterrainPenalty.at(affectedTerrain.num) = true; + } + + const auto nativeTerrain = target->getNativeTerrain(); + if (nativeTerrain.hasValue()) + noterrainPenalty.at(nativeTerrain.num) = true; + + if (nativeTerrain == ETerrainId::ANY_TERRAIN) + boost::range::fill(noterrainPenalty, true); + } } bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const @@ -51,19 +172,19 @@ bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const switch(layer.toEnum()) { case EPathfindingLayer::AIR: - if(hero && hero->boat && hero->boat->layer == EPathfindingLayer::AIR) + if(target && target->boat && target->boat->layer == EPathfindingLayer::AIR) break; - if(!hasBonusOfType(BonusType::FLYING_MOVEMENT)) + if(!hasFlyingMovement()) return false; break; case EPathfindingLayer::WATER: - if(hero && hero->boat && hero->boat->layer == EPathfindingLayer::WATER) + if(target && target->boat && target->boat->layer == EPathfindingLayer::WATER) break; - if(!hasBonusOfType(BonusType::WATER_WALKING)) + if(!hasWaterWalking()) return false; break; @@ -72,80 +193,9 @@ bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const return true; } -bool TurnInfo::hasBonusOfType(BonusType type) const -{ - return hasBonusOfType(type, BonusSubtypeID()); -} - -bool TurnInfo::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const -{ - switch(type) - { - case BonusType::FREE_SHIP_BOARDING: - return bonusCache->freeShipBoarding; - case BonusType::FLYING_MOVEMENT: - return bonusCache->flyingMovement; - case BonusType::WATER_WALKING: - return bonusCache->waterWalking; - case BonusType::NO_TERRAIN_PENALTY: - return bonusCache->noTerrainPenalty.count(subtype.as()); - } - - return static_cast( - bonuses->getFirst(Selector::type()(type).And(Selector::subtype()(subtype)))); -} - -int TurnInfo::valOfBonuses(BonusType type) const -{ - return valOfBonuses(type, BonusSubtypeID()); -} - -int TurnInfo::valOfBonuses(BonusType type, BonusSubtypeID subtype) const -{ - switch(type) - { - case BonusType::FLYING_MOVEMENT: - return bonusCache->flyingMovementVal; - case BonusType::WATER_WALKING: - return bonusCache->waterWalkingVal; - case BonusType::ROUGH_TERRAIN_DISCOUNT: - return bonusCache->pathfindingVal; - } - - return bonuses->valOfBonuses(Selector::type()(type).And(Selector::subtype()(subtype))); -} - int TurnInfo::getMaxMovePoints(const EPathfindingLayer & layer) const { - if(maxMovePointsLand == -1) - maxMovePointsLand = hero->movementPointsLimitCached(true, this); - if(maxMovePointsWater == -1) - maxMovePointsWater = hero->movementPointsLimitCached(false, this); - - return layer == EPathfindingLayer::SAIL ? maxMovePointsWater : maxMovePointsLand; -} - -void TurnInfo::updateHeroBonuses(BonusType type) const -{ - switch(type) - { - case BonusType::FREE_SHIP_BOARDING: - bonusCache->freeShipBoarding = static_cast(bonuses->getFirst(Selector::type()(BonusType::FREE_SHIP_BOARDING))); - break; - case BonusType::FLYING_MOVEMENT: - bonusCache->flyingMovement = static_cast(bonuses->getFirst(Selector::type()(BonusType::FLYING_MOVEMENT))); - bonusCache->flyingMovementVal = bonuses->valOfBonuses(Selector::type()(BonusType::FLYING_MOVEMENT)); - break; - case BonusType::WATER_WALKING: - bonusCache->waterWalking = static_cast(bonuses->getFirst(Selector::type()(BonusType::WATER_WALKING))); - bonusCache->waterWalkingVal = bonuses->valOfBonuses(Selector::type()(BonusType::WATER_WALKING)); - break; - case BonusType::ROUGH_TERRAIN_DISCOUNT: - bonusCache->pathfindingVal = bonuses->valOfBonuses(Selector::type()(BonusType::ROUGH_TERRAIN_DISCOUNT)); - break; - default: - bonuses = hero->getAllBonuses(Selector::days(turn), Selector::all, "all_days" + std::to_string(turn)); - } + return layer == EPathfindingLayer::SAIL ? getMovePointsLimitWater() : getMovePointsLimitLand(); } VCMI_LIB_NAMESPACE_END diff --git a/lib/pathfinder/TurnInfo.h b/lib/pathfinder/TurnInfo.h index 02e4597df..70e7a16e1 100644 --- a/lib/pathfinder/TurnInfo.h +++ b/lib/pathfinder/TurnInfo.h @@ -10,43 +10,74 @@ #pragma once #include "../bonuses/Bonus.h" -#include "../GameConstants.h" +#include "../bonuses/BonusSelector.h" +#include "../bonuses/BonusCache.h" VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; -struct DLL_LINKAGE TurnInfo +class TurnInfoBonusList { - /// This is certainly not the best design ever and certainly can be improved - /// Unfortunately for pathfinder that do hundreds of thousands calls onus system add too big overhead - struct BonusCache { - std::set noTerrainPenalty; - bool freeShipBoarding; - bool flyingMovement; - int flyingMovementVal; - bool waterWalking; - int waterWalkingVal; - int pathfindingVal; + TConstBonusListPtr bonusList; + std::mutex bonusListMutex; + std::atomic bonusListVersion = 0; +public: + TConstBonusListPtr getBonusList(const CGHeroInstance * target, const CSelector & bonusSelector); +}; - BonusCache(const TConstBonusListPtr & bonusList); - }; - std::unique_ptr bonusCache; +struct TurnInfoCache +{ + TurnInfoBonusList waterWalking; + TurnInfoBonusList flyingMovement; + TurnInfoBonusList noTerrainPenalty; + TurnInfoBonusList freeShipBoarding; + TurnInfoBonusList roughTerrainDiscount; + TurnInfoBonusList movementPointsLimitLand; + TurnInfoBonusList movementPointsLimitWater; - const CGHeroInstance * hero; - mutable TConstBonusListPtr bonuses; - mutable int maxMovePointsLand; - mutable int maxMovePointsWater; - TerrainId nativeTerrain; - int turn; + const CGHeroInstance * target; - TurnInfo(const CGHeroInstance * Hero, const int Turn = 0); + mutable std::atomic heroLowestSpeedVersion = 0; + mutable std::atomic heroLowestSpeedValue = 0; + + explicit TurnInfoCache(const CGHeroInstance * target): + target(target) + {} +}; + +class DLL_LINKAGE TurnInfo +{ +private: + const CGHeroInstance * target; + + // stores cached values per each terrain + std::vector noterrainPenalty; + + int flyingMovementValue; + int waterWalkingValue; + int roughTerrainDiscountValue; + int movePointsLimitLand; + int movePointsLimitWater; + + bool waterWalkingTest; + bool flyingMovementTest; + bool freeShipBoardingTest; + +public: + int hasWaterWalking() const; + int hasFlyingMovement() const; + int hasNoTerrainPenalty(const TerrainId & terrain) const; + int hasFreeShipBoarding() const; + + int getFlyingMovementValue() const; + int getWaterWalkingValue() const; + int getRoughTerrainDiscountValue() const; + int getMovePointsLimitLand() const; + int getMovePointsLimitWater() const; + + TurnInfo(TurnInfoCache * sharedCache, const CGHeroInstance * target, int Turn); bool isLayerAvailable(const EPathfindingLayer & layer) const; - bool hasBonusOfType(const BonusType type) const; - bool hasBonusOfType(const BonusType type, const BonusSubtypeID subtype) const; - int valOfBonuses(const BonusType type) const; - int valOfBonuses(const BonusType type, const BonusSubtypeID subtype) const; - void updateHeroBonuses(BonusType type) const; int getMaxMovePoints(const EPathfindingLayer & layer) const; }; diff --git a/mapeditor/generatorprogress.cpp b/mapeditor/generatorprogress.cpp index 659f655b8..9219d5936 100644 --- a/mapeditor/generatorprogress.cpp +++ b/mapeditor/generatorprogress.cpp @@ -19,11 +19,11 @@ GeneratorProgress::GeneratorProgress(Load::Progress & source, QWidget *parent) : source(source) { ui->setupUi(this); + + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); setAttribute(Qt::WA_DeleteOnClose); - setWindowFlags(Qt::Window); - show(); } diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 3201e46d8..6c8f0962a 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -20,6 +20,7 @@ #include "../lib/mapObjects/ObjectTemplate.h" #include "../lib/mapping/CMap.h" #include "../lib/constants/StringConstants.h" +#include "../lib/texts/CGeneralTextHandler.h" #include "townbuildingswidget.h" #include "towneventswidget.h" @@ -35,15 +36,6 @@ #include "PickObjectDelegate.h" #include "../mapcontroller.h" -static QList> CharacterIdentifiers -{ - {QObject::tr("Compliant"), QVariant::fromValue(int(CGCreature::Character::COMPLIANT))}, - {QObject::tr("Friendly"), QVariant::fromValue(int(CGCreature::Character::FRIENDLY))}, - {QObject::tr("Aggressive"), QVariant::fromValue(int(CGCreature::Character::AGGRESSIVE))}, - {QObject::tr("Hostile"), QVariant::fromValue(int(CGCreature::Character::HOSTILE))}, - {QObject::tr("Savage"), QVariant::fromValue(int(CGCreature::Character::SAVAGE))}, -}; - //===============IMPLEMENT OBJECT INITIALIZATION FUNCTIONS================ Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : defaultPlayer(pl) { @@ -245,19 +237,19 @@ void Inspector::updateProperties(CArmedInstance * o) if(!o) return; auto * delegate = new ArmyDelegate(*o); - addProperty("Army", PropertyEditorPlaceholder(), delegate, false); + addProperty(QObject::tr("Army"), PropertyEditorPlaceholder(), delegate, false); } void Inspector::updateProperties(CGDwelling * o) { if(!o) return; - addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); + addProperty(QObject::tr("Owner"), o->tempOwner, new OwnerDelegate(controller), false); if (o->ID == Obj::RANDOM_DWELLING || o->ID == Obj::RANDOM_DWELLING_LVL) { auto * delegate = new PickObjectDelegate(controller, PickObjectDelegate::typedFilter); - addProperty("Same as town", PropertyEditorPlaceholder(), delegate, false); + addProperty(QObject::tr("Same as town"), PropertyEditorPlaceholder(), delegate, false); } } @@ -265,29 +257,29 @@ void Inspector::updateProperties(FlaggableMapObject * o) { if(!o) return; - addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); + addProperty(QObject::tr("Owner"), o->tempOwner, new OwnerDelegate(controller), false); } void Inspector::updateProperties(CGGarrison * o) { if(!o) return; - addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); - addProperty("Removable units", o->removableUnits, false); + addProperty(QObject::tr("Owner"), o->tempOwner, new OwnerDelegate(controller), false); + addProperty(QObject::tr("Removable units"), o->removableUnits, false); } void Inspector::updateProperties(CGShipyard * o) { if(!o) return; - addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); + addProperty(QObject::tr("Owner"), o->tempOwner, new OwnerDelegate(controller), false); } void Inspector::updateProperties(CGHeroPlaceholder * o) { if(!o) return; - addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); + addProperty(QObject::tr("Owner"), o->tempOwner, new OwnerDelegate(controller), false); bool type = false; if(o->heroType.has_value()) @@ -297,11 +289,11 @@ void Inspector::updateProperties(CGHeroPlaceholder * o) { auto * delegate = new InspectorDelegate; - delegate->options = {{"POWER RANK", QVariant::fromValue(false)}, {"HERO TYPE", QVariant::fromValue(true)}}; - addProperty("Placeholder type", delegate->options[type].first, delegate, false); + delegate->options = {{QObject::tr("POWER RANK"), QVariant::fromValue(false)}, {QObject::tr("HERO TYPE"), QVariant::fromValue(true)}}; + addProperty(QObject::tr("Placeholder type"), delegate->options[type].first, delegate, false); } - addProperty("Power rank", o->powerRank.has_value() ? o->powerRank.value() : 0, type); + addProperty(QObject::tr("Power rank"), o->powerRank.has_value() ? o->powerRank.value() : 0, type); { auto * delegate = new InspectorDelegate; @@ -309,7 +301,7 @@ void Inspector::updateProperties(CGHeroPlaceholder * o) { delegate->options.push_back({QObject::tr(VLC->heroh->objects[i]->getNameTranslated().c_str()), QVariant::fromValue(VLC->heroh->objects[i]->getId().getNum())}); } - addProperty("Hero type", o->heroType.has_value() ? VLC->heroh->getById(o->heroType.value())->getNameTranslated() : "", delegate, !type); + addProperty(QObject::tr("Hero type"), o->heroType.has_value() ? VLC->heroh->getById(o->heroType.value())->getNameTranslated() : "", delegate, !type); } } @@ -318,23 +310,23 @@ void Inspector::updateProperties(CGHeroInstance * o) if(!o) return; auto isPrison = o->ID == Obj::PRISON; - addProperty("Owner", o->tempOwner, new OwnerDelegate(controller, isPrison), isPrison); //field is not editable for prison - addProperty("Experience", o->exp, false); - addProperty("Hero class", o->getHeroClassID().hasValue() ? o->getHeroClass()->getNameTranslated() : "", true); + addProperty(QObject::tr("Owner"), o->tempOwner, new OwnerDelegate(controller, isPrison), isPrison); //field is not editable for prison + addProperty(QObject::tr("Experience"), o->exp, false); + addProperty(QObject::tr("Hero class"), o->getHeroClassID().hasValue() ? o->getHeroClass()->getNameTranslated() : "", true); { //Gender auto * delegate = new InspectorDelegate; - delegate->options = {{"MALE", QVariant::fromValue(int(EHeroGender::MALE))}, {"FEMALE", QVariant::fromValue(int(EHeroGender::FEMALE))}}; - addProperty("Gender", (o->gender == EHeroGender::FEMALE ? "FEMALE" : "MALE"), delegate , false); + delegate->options = {{QObject::tr("MALE"), QVariant::fromValue(int(EHeroGender::MALE))}, {QObject::tr("FEMALE"), QVariant::fromValue(int(EHeroGender::FEMALE))}}; + addProperty(QObject::tr("Gender"), (o->gender == EHeroGender::FEMALE ? QObject::tr("FEMALE") : QObject::tr("MALE")).toStdString(), delegate , false); } - addProperty("Name", o->getNameTranslated(), false); - addProperty("Biography", o->getBiographyTranslated(), new MessageDelegate, false); - addProperty("Portrait", PropertyEditorPlaceholder(), new PortraitDelegate(*o), false); + addProperty(QObject::tr("Name"), o->getNameTranslated(), false); + addProperty(QObject::tr("Biography"), o->getBiographyTranslated(), new MessageDelegate, false); + addProperty(QObject::tr("Portrait"), PropertyEditorPlaceholder(), new PortraitDelegate(*o), false); auto * delegate = new HeroSkillsDelegate(*o); - addProperty("Skills", PropertyEditorPlaceholder(), delegate, false); - addProperty("Spells", PropertyEditorPlaceholder(), new HeroSpellDelegate(*o), false); - addProperty("Artifacts", PropertyEditorPlaceholder(), new HeroArtifactsDelegate(*o), false); + addProperty(QObject::tr("Skills"), PropertyEditorPlaceholder(), delegate, false); + addProperty(QObject::tr("Spells"), PropertyEditorPlaceholder(), new HeroSpellDelegate(*o), false); + addProperty(QObject::tr("Artifacts"), PropertyEditorPlaceholder(), new HeroArtifactsDelegate(*o), false); if(o->getHeroTypeID().hasValue() || o->ID == Obj::PRISON) { //Hero type @@ -349,7 +341,7 @@ void Inspector::updateProperties(CGHeroInstance * o) } } } - addProperty("Hero type", o->getHeroTypeID().hasValue() ? o->getHeroType()->getNameTranslated() : "", delegate, false); + addProperty(QObject::tr("Hero type"), o->getHeroTypeID().hasValue() ? o->getHeroType()->getNameTranslated() : "", delegate, false); } { const int maxRadius = 60; @@ -358,7 +350,7 @@ void Inspector::updateProperties(CGHeroInstance * o) for(int i = 0; i <= maxRadius; ++i) patrolDelegate->options.push_back({ QObject::tr("%n tile(s)", "", i), QVariant::fromValue(i)}); auto patrolRadiusText = o->patrol.patrolling ? QObject::tr("%n tile(s)", "", o->patrol.patrolRadius) : QObject::tr("No patrol"); - addProperty("Patrol radius", patrolRadiusText, patrolDelegate, false); + addProperty(QObject::tr("Patrol radius"), patrolRadiusText, patrolDelegate, false); } } @@ -366,19 +358,21 @@ void Inspector::updateProperties(CGTownInstance * o) { if(!o) return; - addProperty("Town name", o->getNameTranslated(), false); + addProperty(QObject::tr("Town name"), o->getNameTranslated(), false); auto * delegate = new TownBuildingsDelegate(*o); - addProperty("Buildings", PropertyEditorPlaceholder(), delegate, false); - addProperty("Spells", PropertyEditorPlaceholder(), new TownSpellsDelegate(*o), false); - addProperty("Events", PropertyEditorPlaceholder(), new TownEventsDelegate(*o, controller), false); + addProperty(QObject::tr("Buildings"), PropertyEditorPlaceholder(), delegate, false); + addProperty(QObject::tr("Spells"), PropertyEditorPlaceholder(), new TownSpellsDelegate(*o), false); + addProperty(QObject::tr("Events"), PropertyEditorPlaceholder(), new TownEventsDelegate(*o, controller), false); + if(o->ID == Obj::RANDOM_TOWN) + addProperty(QObject::tr("Same as player"), o->alignmentToPlayer, new OwnerDelegate(controller), false); } void Inspector::updateProperties(CGArtifact * o) { if(!o) return; - addProperty("Message", o->message, false); + addProperty(QObject::tr("Message"), o->message, false); CArtifactInstance * instance = o->storedArtifact; if(instance) @@ -392,7 +386,7 @@ void Inspector::updateProperties(CGArtifact * o) if(controller.map()->allowedSpells.count(spell->id) != 0) delegate->options.push_back({QObject::tr(spell->getNameTranslated().c_str()), QVariant::fromValue(int(spell->getId()))}); } - addProperty("Spell", VLC->spellh->getById(spellId)->getNameTranslated(), delegate, false); + addProperty(QObject::tr("Spell"), VLC->spellh->getById(spellId)->getNameTranslated(), delegate, false); } } } @@ -401,42 +395,42 @@ void Inspector::updateProperties(CGMine * o) { if(!o) return; - addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); - addProperty("Resource", o->producedResource); - addProperty("Productivity", o->producedQuantity); + addProperty(QObject::tr("Owner"), o->tempOwner, new OwnerDelegate(controller), false); + addProperty(QObject::tr("Resource"), o->producedResource); + addProperty(QObject::tr("Productivity"), o->producedQuantity); } void Inspector::updateProperties(CGResource * o) { if(!o) return; - addProperty("Amount", o->amount, false); - addProperty("Message", o->message, false); + addProperty(QObject::tr("Amount"), o->amount, false); + addProperty(QObject::tr("Message"), o->message, false); } void Inspector::updateProperties(CGSignBottle * o) { if(!o) return; - addProperty("Message", o->message, new MessageDelegate, false); + addProperty(QObject::tr("Message"), o->message, new MessageDelegate, false); } void Inspector::updateProperties(CGCreature * o) { if(!o) return; - addProperty("Message", o->message, false); + addProperty(QObject::tr("Message"), o->message, false); { //Character auto * delegate = new InspectorDelegate; - delegate->options = CharacterIdentifiers; + delegate->options = characterIdentifiers; addProperty("Character", (CGCreature::Character)o->character, delegate, false); } - addProperty("Never flees", o->neverFlees, false); - addProperty("Not growing", o->notGrowingTeam, false); - addProperty("Artifact reward", o->gainedArtifact); //TODO: implement in setProperty - addProperty("Army", PropertyEditorPlaceholder(), true); - addProperty("Amount", o->stacks[SlotID(0)]->count, false); - //addProperty("Resources reward", o->resources); //TODO: implement in setProperty + addProperty(QObject::tr("Never flees"), o->neverFlees, false); + addProperty(QObject::tr("Not growing"), o->notGrowingTeam, false); + addProperty(QObject::tr("Artifact reward"), o->gainedArtifact); //TODO: implement in setProperty + addProperty(QObject::tr("Army"), PropertyEditorPlaceholder(), true); + addProperty(QObject::tr("Amount"), o->stacks[SlotID(0)]->count, false); + //addProperty(QObject::tr("Resources reward"), o->resources); //TODO: implement in setProperty } void Inspector::updateProperties(CRewardableObject * o) @@ -444,23 +438,23 @@ void Inspector::updateProperties(CRewardableObject * o) if(!o) return; auto * delegate = new RewardsDelegate(*controller.map(), *o); - addProperty("Reward", PropertyEditorPlaceholder(), delegate, false); + addProperty(QObject::tr("Reward"), PropertyEditorPlaceholder(), delegate, false); } void Inspector::updateProperties(CGPandoraBox * o) { if(!o) return; - addProperty("Message", o->message, new MessageDelegate, false); + addProperty(QObject::tr("Message"), o->message, new MessageDelegate, false); } void Inspector::updateProperties(CGEvent * o) { if(!o) return; - addProperty("Remove after", o->removeAfterVisit, false); - addProperty("Human trigger", o->humanActivate, false); - addProperty("Cpu trigger", o->computerActivate, false); + addProperty(QObject::tr("Remove after"), o->removeAfterVisit, false); + addProperty(QObject::tr("Human trigger"), o->humanActivate, false); + addProperty(QObject::tr("Cpu trigger"), o->computerActivate, false); //ui8 availableFor; //players whom this event is available for } @@ -468,15 +462,15 @@ void Inspector::updateProperties(CGSeerHut * o) { if(!o || !o->quest) return; - addProperty("First visit text", o->quest->firstVisitText, new MessageDelegate, false); - addProperty("Next visit text", o->quest->nextVisitText, new MessageDelegate, false); - addProperty("Completed text", o->quest->completedText, new MessageDelegate, false); - addProperty("Repeat quest", o->quest->repeatedQuest, false); - addProperty("Time limit", o->quest->lastDay, false); + addProperty(QObject::tr("First visit text"), o->quest->firstVisitText, new MessageDelegate, false); + addProperty(QObject::tr("Next visit text"), o->quest->nextVisitText, new MessageDelegate, false); + addProperty(QObject::tr("Completed text"), o->quest->completedText, new MessageDelegate, false); + addProperty(QObject::tr("Repeat quest"), o->quest->repeatedQuest, false); + addProperty(QObject::tr("Time limit"), o->quest->lastDay, false); { //Quest auto * delegate = new QuestDelegate(controller, *o->quest); - addProperty("Quest", PropertyEditorPlaceholder(), delegate, false); + addProperty(QObject::tr("Quest"), PropertyEditorPlaceholder(), delegate, false); } } @@ -484,8 +478,8 @@ void Inspector::updateProperties(CGQuestGuard * o) { if(!o || !o->quest) return; - addProperty("Reward", PropertyEditorPlaceholder(), nullptr, true); - addProperty("Repeat quest", o->quest->repeatedQuest, true); + addProperty(QObject::tr("Reward"), PropertyEditorPlaceholder(), nullptr, true); + addProperty(QObject::tr("Repeat quest"), o->quest->repeatedQuest, true); } void Inspector::updateProperties() @@ -494,18 +488,18 @@ void Inspector::updateProperties() return; table->setRowCount(0); //cleanup table - addProperty("Identifier", obj); - addProperty("ID", obj->ID.getNum()); - addProperty("SubID", obj->subID); - addProperty("InstanceName", obj->instanceName); + addProperty(QObject::tr("Identifier"), obj); + addProperty(QObject::tr("ID"), obj->ID.getNum()); + addProperty(QObject::tr("SubID"), obj->subID); + addProperty(QObject::tr("InstanceName"), obj->instanceName); if(obj->ID != Obj::HERO_PLACEHOLDER && !dynamic_cast(obj)) { auto factory = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); - addProperty("IsStatic", factory->isStaticObject()); + addProperty(QObject::tr("IsStatic"), factory->isStaticObject()); } - addProperty("Owner", obj->tempOwner, new OwnerDelegate(controller), true); + addProperty(QObject::tr("Owner"), obj->tempOwner, new OwnerDelegate(controller), true); UPDATE_OBJ_PROPERTIES(CArmedInstance); UPDATE_OBJ_PROPERTIES(CGResource); @@ -552,7 +546,7 @@ void Inspector::setProperty(const QString & key, const QVariant & value) if(!obj) return; - if(key == "Owner") + if(key == QObject::tr("Owner")) obj->tempOwner = PlayerColor(value.toInt()); SET_PROPERTIES(CArmedInstance); @@ -594,7 +588,7 @@ void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVarian { if(!o) return; - if(key == "Message") + if(key == QObject::tr("Message")) o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); } @@ -603,13 +597,13 @@ void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & v { if(!o) return; - if(key == "Remove after") + if(key == QObject::tr("Remove after")) o->removeAfterVisit = value.toBool(); - if(key == "Human trigger") + if(key == QObject::tr("Human trigger")) o->humanActivate = value.toBool(); - if(key == "Cpu trigger") + if(key == QObject::tr("Cpu trigger")) o->computerActivate = value.toBool(); } @@ -617,16 +611,19 @@ void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVari { if(!o) return; - if(key == "Town name") + if(key == QObject::tr("Town name")) o->setNameTextId(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); + + if(key == QObject::tr("Same as player")) + o->alignmentToPlayer = PlayerColor(value.toInt()); } void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value) { if(!o) return; - if(key == "Message") + if(key == QObject::tr("Message")) o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); } @@ -635,7 +632,7 @@ void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & va { if(!o) return; - if(key == "Productivity") + if(key == QObject::tr("Productivity")) o->producedQuantity = value.toString().toInt(); } @@ -643,11 +640,11 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant { if(!o) return; - if(key == "Message") + if(key == QObject::tr("Message")) o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); - if(o->storedArtifact && key == "Spell") + if(o->storedArtifact && key == QObject::tr("Spell")) { o->storedArtifact = ArtifactUtils::createScroll(SpellID(value.toInt())); } @@ -657,7 +654,7 @@ void Inspector::setProperty(CGDwelling * o, const QString & key, const QVariant { if(!o) return; - if(key == "Same as town") + if(key == QObject::tr("Same as town")) { if (!o->randomizationInfo.has_value()) o->randomizationInfo = CGDwellingRandomizationInfo(); @@ -672,7 +669,7 @@ void Inspector::setProperty(CGGarrison * o, const QString & key, const QVariant { if(!o) return; - if(key == "Removable units") + if(key == QObject::tr("Removable units")) o->removableUnits = value.toBool(); } @@ -680,7 +677,7 @@ void Inspector::setProperty(CGHeroPlaceholder * o, const QString & key, const QV { if(!o) return; - if(key == "Placeholder type") + if(key == QObject::tr("Placeholder type")) { if(value.toBool()) { @@ -698,10 +695,10 @@ void Inspector::setProperty(CGHeroPlaceholder * o, const QString & key, const QV updateProperties(); } - if(key == "Power rank") + if(key == QObject::tr("Power rank")) o->powerRank = value.toInt(); - if(key == "Hero type") + if(key == QObject::tr("Hero type")) { o->heroType = HeroTypeID(value.toInt()); } @@ -711,21 +708,21 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari { if(!o) return; - if(key == "Gender") + if(key == QObject::tr("Gender")) o->gender = EHeroGender(value.toInt()); - if(key == "Name") + if(key == QObject::tr("Name")) o->nameCustomTextId = mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); - if(key == "Biography") + if(key == QObject::tr("Biography")) o->biographyCustomTextId = mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); - if(key == "Experience") + if(key == QObject::tr("Experience")) o->exp = value.toString().toInt(); - if(key == "Hero type") + if(key == QObject::tr("Hero type")) { for(auto const & t : VLC->heroh->objects) { @@ -737,7 +734,7 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari updateProperties(); //updating other properties after change } - if(key == "Patrol radius") + if(key == QObject::tr("Patrol radius")) { auto radius = value.toInt(); o->patrol.patrolRadius = radius; @@ -754,7 +751,7 @@ void Inspector::setProperty(CGResource * o, const QString & key, const QVariant { if(!o) return; - if(key == "Amount") + if(key == QObject::tr("Amount")) o->amount = value.toString().toInt(); } @@ -762,16 +759,16 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant { if(!o) return; - if(key == "Message") + if(key == QObject::tr("Message")) o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); - if(key == "Character") + if(key == QObject::tr("Character")) o->character = CGCreature::Character(value.toInt()); - if(key == "Never flees") + if(key == QObject::tr("Never flees")) o->neverFlees = value.toBool(); - if(key == "Not growing") + if(key == QObject::tr("Not growing")) o->notGrowingTeam = value.toBool(); - if(key == "Amount") + if(key == QObject::tr("Amount")) o->stacks[SlotID(0)]->count = value.toString().toInt(); } @@ -779,18 +776,18 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & { if(!o) return; - if(key == "First visit text") + if(key == QObject::tr("First visit text")) o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); - if(key == "Next visit text") + if(key == QObject::tr("Next visit text")) o->quest->nextVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); - if(key == "Completed text") + if(key == QObject::tr("Completed text")) o->quest->completedText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); - if(key == "Repeat quest") + if(key == QObject::tr("Repeat quest")) o->quest->repeatedQuest = value.toBool(); - if(key == "Time limit") + if(key == QObject::tr("Time limit")) o->quest->lastDay = value.toString().toInt(); } @@ -874,8 +871,10 @@ QTableWidgetItem * Inspector::addProperty(const PlayerColor & value) if(value == PlayerColor::NEUTRAL) str = QObject::tr("neutral"); + MetaString playerStr; + playerStr.appendName(value); if(value.isValidPlayer()) - str = QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[value]); + str = QString::fromStdString(playerStr.toString()); auto * item = new QTableWidgetItem(str); item->setFlags(Qt::NoItemFlags); @@ -885,7 +884,9 @@ QTableWidgetItem * Inspector::addProperty(const PlayerColor & value) QTableWidgetItem * Inspector::addProperty(const GameResID & value) { - auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[value.toEnum()])); + MetaString str; + str.appendName(value); + auto * item = new QTableWidgetItem(QString::fromStdString(str.toString())); item->setFlags(Qt::NoItemFlags); item->setData(Qt::UserRole, QVariant::fromValue(value.getNum())); return item; @@ -897,7 +898,7 @@ QTableWidgetItem * Inspector::addProperty(CGCreature::Character value) item->setFlags(Qt::NoItemFlags); item->setData(Qt::UserRole, QVariant::fromValue(int(value))); - for(auto & i : CharacterIdentifiers) + for(auto & i : characterIdentifiers) { if(i.second.toInt() == value) { @@ -913,6 +914,13 @@ QTableWidgetItem * Inspector::addProperty(CGCreature::Character value) Inspector::Inspector(MapController & c, CGObjectInstance * o, QTableWidget * t): obj(o), table(t), controller(c) { + characterIdentifiers = { + { QObject::tr("Compliant"), QVariant::fromValue(int(CGCreature::Character::COMPLIANT)) }, + { QObject::tr("Friendly"), QVariant::fromValue(int(CGCreature::Character::FRIENDLY)) }, + { QObject::tr("Aggressive"), QVariant::fromValue(int(CGCreature::Character::AGGRESSIVE)) }, + { QObject::tr("Hostile"), QVariant::fromValue(int(CGCreature::Character::HOSTILE)) }, + { QObject::tr("Savage"), QVariant::fromValue(int(CGCreature::Character::SAVAGE)) }, + }; } /* @@ -964,5 +972,9 @@ OwnerDelegate::OwnerDelegate(MapController & controller, bool addNeutral) options.push_back({QObject::tr("neutral"), QVariant::fromValue(PlayerColor::NEUTRAL.getNum()) }); for(int p = 0; p < controller.map()->players.size(); ++p) if(controller.map()->players[p].canAnyonePlay()) - options.push_back({QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[p]), QVariant::fromValue(PlayerColor(p).getNum()) }); + { + MetaString str; + str.appendName(PlayerColor(p)); + options.push_back({QString::fromStdString(str.toString()), QVariant::fromValue(PlayerColor(p).getNum()) }); + } } diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 1f03d5601..a7939dd56 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -63,6 +63,8 @@ private: class Inspector { + QList> characterIdentifiers; + protected: struct PropertyEditorPlaceholder {}; diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index 1a14d7fa1..ff84f8ea7 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -43,7 +43,9 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par ui->lResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1); for(int i = 0; i < GameConstants::RESOURCE_QUANTITY - 1; ++i) { - auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); + MetaString str; + str.appendName(GameResID(i)); + auto * item = new QTableWidgetItem(QString::fromStdString(str.toString())); item->setData(Qt::UserRole, QVariant::fromValue(i)); ui->lResources->setItem(i, 0, item); auto * spinBox = new QSpinBox; @@ -126,7 +128,9 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par //fill players for(auto color = PlayerColor(0); color < PlayerColor::PLAYER_LIMIT; ++color) { - auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[color.getNum()])); + MetaString str; + str.appendName(color); + auto * item = new QListWidgetItem(QString::fromStdString(str.toString())); item->setData(Qt::UserRole, QVariant::fromValue(color.getNum())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 614846d2b..0e3e5bb4f 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -55,9 +55,11 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : ui->lResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1); for(int i = 0; i < GameConstants::RESOURCE_QUANTITY - 1; ++i) { + MetaString str; + str.appendName(GameResID(i)); for(auto * w : {ui->rResources, ui->lResources}) { - auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); + auto * item = new QTableWidgetItem(QString::fromStdString(str.toString())); item->setData(Qt::UserRole, QVariant::fromValue(i)); w->setItem(i, 0, item); auto * spinBox = new QSpinBox; @@ -163,7 +165,9 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : //fill players for(auto color = PlayerColor(0); color < PlayerColor::PLAYER_LIMIT; ++color) { - auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[color.getNum()])); + MetaString str; + str.appendName(color); + auto * item = new QListWidgetItem(QString::fromStdString(str.toString())); item->setData(Qt::UserRole, QVariant::fromValue(color.getNum())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); diff --git a/mapeditor/inspector/towneventdialog.cpp b/mapeditor/inspector/towneventdialog.cpp index 475c42edc..6ab833be3 100644 --- a/mapeditor/inspector/towneventdialog.cpp +++ b/mapeditor/inspector/towneventdialog.cpp @@ -67,8 +67,10 @@ void TownEventDialog::initPlayers() auto playerList = params.value("players").toList(); for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) { + MetaString str; + str.appendName(PlayerColor(i)); bool isAffected = playerList.contains(toQString(PlayerColor(i))); - auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i])); + auto * item = new QListWidgetItem(QString::fromStdString(str.toString())); item->setData(MapEditorRoles::PlayerIDRole, QVariant::fromValue(i)); item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked); ui->playersAffected->addItem(item); @@ -81,13 +83,15 @@ void TownEventDialog::initResources() auto resourcesMap = params.value("resources").toMap(); for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) { - auto name = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]); + MetaString str; + str.appendName(GameResID(i)); + auto name = QString::fromStdString(str.toString()); auto * item = new QTableWidgetItem(); item->setFlags(item->flags() & ~Qt::ItemIsEditable); item->setText(name); ui->resourcesTable->setItem(i, 0, item); - int val = resourcesMap.value(name).toInt(); + int val = resourcesMap.value(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])).toInt(); auto * edit = new QSpinBox(ui->resourcesTable); edit->setMaximum(i == GameResID::GOLD ? MAXIMUM_GOLD_CHANGE : MAXIMUM_RESOURCE_CHANGE); edit->setMinimum(i == GameResID::GOLD ? -MAXIMUM_GOLD_CHANGE : -MAXIMUM_RESOURCE_CHANGE); diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 345f645dc..8210b77ca 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -24,7 +24,9 @@ #include "../lib/logging/CBasicLogConfigurator.h" #include "../lib/CConfigHandler.h" #include "../lib/filesystem/Filesystem.h" +#include "../lib/filesystem/CMemoryBuffer.h" #include "../lib/GameConstants.h" +#include "../lib/campaign/CampaignHandler.h" #include "../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../lib/mapObjects/ObjectTemplate.h" @@ -32,6 +34,7 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapEditManager.h" #include "../lib/mapping/MapFormat.h" +#include "../lib/mapping/MapFormatJson.h" #include "../lib/modding/ModIncompatibility.h" #include "../lib/RoadHandler.h" #include "../lib/RiverHandler.h" @@ -398,6 +401,27 @@ std::unique_ptr MainWindow::openMapInternal(const QString & filenameSelect throw std::runtime_error("Corrupted map"); } +std::shared_ptr MainWindow::openCampaignInternal(const QString & filenameSelect) +{ + QFileInfo fi(filenameSelect); + std::string fname = fi.fileName().toStdString(); + std::string fdir = fi.dir().path().toStdString(); + + ResourcePath resId("MAPEDITOR/" + fname, EResType::CAMPAIGN); + + //addFilesystem takes care about memory deallocation if case of failure, no memory leak here + auto * mapEditorFilesystem = new CFilesystemLoader("MAPEDITOR/", fdir, 0); + CResourceHandler::removeFilesystem("local", "mapEditor"); + CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem); + + if(!CResourceHandler::get("mapEditor")->existsResource(resId)) + throw std::runtime_error("Cannot open campaign from this folder"); + if(auto campaign = CampaignHandler::getCampaign(resId.getName())) + return campaign; + else + throw std::runtime_error("Corrupted campaign"); +} + bool MainWindow::openMap(const QString & filenameSelect) { try @@ -407,12 +431,12 @@ bool MainWindow::openMap(const QString & filenameSelect) catch(const ModIncompatibility & e) { assert(e.whatExcessive().empty()); - QMessageBox::warning(this, "Mods are required", QString::fromStdString(e.whatMissing())); + QMessageBox::warning(this, tr("Mods are required"), QString::fromStdString(e.whatMissing())); return false; } catch(const std::exception & e) { - QMessageBox::critical(this, "Failed to open map", tr(e.what())); + QMessageBox::critical(this, tr("Failed to open map"), tr(e.what())); return false; } @@ -459,8 +483,6 @@ void MainWindow::on_actionOpenRecent_triggered() RecentFileDialog(const QStringList& recentFiles, QWidget *parent) : QDialog(parent), layout(new QVBoxLayout(this)), listWidget(new QListWidget(this)) { - - setWindowTitle(tr("Recently Opened Files")); setMinimumWidth(600); connect(listWidget, &QListWidget::itemActivated, this, [this](QListWidgetItem *item) @@ -496,6 +518,7 @@ void MainWindow::on_actionOpenRecent_triggered() }; RecentFileDialog d(recentFiles, this); + d.setWindowTitle(tr("Recently Opened Files")); if(d.exec() == QDialog::Accepted && getAnswerAboutUnsavedChanges()) { openMap(d.getSelectedFilePath()); @@ -548,10 +571,11 @@ void MainWindow::saveMap() if(!issues.empty()) { + auto mapValidationTitle = tr("Map validation"); if(critical) - QMessageBox::warning(this, "Map validation", "Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found"); + QMessageBox::warning(this, mapValidationTitle, tr("Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found")); else - QMessageBox::information(this, "Map validation", "Map has some errors. Open Validator from the Map menu to see issues found"); + QMessageBox::information(this, mapValidationTitle, tr("Map has some errors. Open Validator from the Map menu to see issues found")); } Translations::cleanupRemovedItems(*controller.map()); @@ -575,7 +599,7 @@ void MainWindow::saveMap() } catch(const std::exception & e) { - QMessageBox::critical(this, "Failed to save map", e.what()); + QMessageBox::critical(this, tr("Failed to save map"), e.what()); return; } @@ -942,7 +966,7 @@ void MainWindow::loadObjectsTree() } catch(const std::exception &) { - QMessageBox::critical(this, "Mods loading problem", "Critical error during Mods loading. Disable invalid mods and restart."); + QMessageBox::critical(this, tr("Mods loading problem"), tr("Critical error during Mods loading. Disable invalid mods and restart.")); } } @@ -1373,6 +1397,53 @@ void MainWindow::on_actionh3m_converter_triggered() } } +void MainWindow::on_actionh3c_converter_triggered() +{ + auto campaignFile = QFileDialog::getOpenFileName(this, tr("Select campaign to convert"), + QString::fromStdString(VCMIDirs::get().userDataPath().make_preferred().string()), + tr("HoMM3 campaigns (*.h3c)")); + if(campaignFile.isEmpty()) + return; + + auto campaignFileDest = QFileDialog::getSaveFileName(this, tr("Select destination file"), + QString::fromStdString(VCMIDirs::get().userDataPath().make_preferred().string()), + tr("VCMI campaigns (*.vcmp)")); + if(campaignFileDest.isEmpty()) + return; + + QFileInfo fileInfo(campaignFileDest); + if(fileInfo.suffix().toLower() != "vcmp") + campaignFileDest += ".vcmp"; + auto campaign = openCampaignInternal(campaignFile); + + auto jsonCampaign = CampaignHandler::writeHeaderToJson(*campaign); + + std::shared_ptr io(new CDefaultIOApi()); + auto saver = std::make_shared(io, campaignFileDest.toStdString()); + for(auto & scenario : campaign->allScenarios()) + { + CMapService mapService; + auto map = campaign->getMap(scenario, nullptr); + controller.repairMap(map.get()); + CMemoryBuffer serializeBuffer; + { + CMapSaverJson jsonSaver(&serializeBuffer); + jsonSaver.saveMap(map); + } + + auto mapName = boost::algorithm::to_lower_copy(campaign->scenario(scenario).mapName); + mapName = boost::replace_all_copy(mapName, ".h3m", std::string("")) + ".vmap"; + + auto stream = saver->addFile(mapName); + stream->write(reinterpret_cast(serializeBuffer.getBuffer().data()), serializeBuffer.getSize()); + + jsonCampaign["scenarios"].Vector().push_back(CampaignHandler::writeScenarioToJson(campaign->scenario(scenario))); + jsonCampaign["scenarios"].Vector().back()["map"].String() = mapName; + } + + auto jsonCampaignStr = jsonCampaign.toString(); + saver->addFile("header.json")->write(reinterpret_cast(jsonCampaignStr.data()), jsonCampaignStr.length()); +} void MainWindow::on_actionLock_triggered() { diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index ba4fa4db9..e7c90801f 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -11,6 +11,7 @@ class ObjectBrowserProxyModel; VCMI_LIB_NAMESPACE_BEGIN class CMap; +class CampaignState; class CGObjectInstance; VCMI_LIB_NAMESPACE_END @@ -35,6 +36,7 @@ class MainWindow : public QMainWindow #endif std::unique_ptr openMapInternal(const QString &); + std::shared_ptr openCampaignInternal(const QString &); public: explicit MainWindow(QWidget *parent = nullptr); @@ -118,6 +120,8 @@ private slots: void on_actionh3m_converter_triggered(); + void on_actionh3c_converter_triggered(); + void on_actionLock_triggered(); void on_actionUnlock_triggered(); diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 5a3e6b94c..31431bd8d 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -71,6 +71,7 @@ + @@ -1352,6 +1353,14 @@ h3m converter + + + h3c converter + + + h3c converter + + Lock diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp index 6f7da223b..3535ab390 100644 --- a/mapeditor/mapsettings/mapsettings.cpp +++ b/mapeditor/mapsettings/mapsettings.cpp @@ -25,7 +25,9 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : controller(ctrl) { ui->setupUi(this); - + + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint); + assert(controller.map()); controller.settingsDialog = this; show(); diff --git a/mapeditor/mapsettings/timedevent.cpp b/mapeditor/mapsettings/timedevent.cpp index 81998765f..7d627615a 100644 --- a/mapeditor/mapsettings/timedevent.cpp +++ b/mapeditor/mapsettings/timedevent.cpp @@ -37,7 +37,9 @@ TimedEvent::TimedEvent(MapController & c, QListWidgetItem * t, QWidget *parent) for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) { bool isAffected = playerList.contains(toQString(PlayerColor(i))); - auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i])); + MetaString str; + str.appendName(PlayerColor(i)); + auto * item = new QListWidgetItem(QString::fromStdString(str.toString())); item->setData(Qt::UserRole, QVariant::fromValue(i)); item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked); ui->playersAffected->addItem(item); @@ -46,8 +48,10 @@ TimedEvent::TimedEvent(MapController & c, QListWidgetItem * t, QWidget *parent) ui->resources->setRowCount(GameConstants::RESOURCE_QUANTITY); for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) { - auto name = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]); - int val = params.value("resources").toMap().value(name).toInt(); + MetaString str; + str.appendName(GameResID(i)); + auto name = QString::fromStdString(str.toString()); + int val = params.value("resources").toMap().value(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])).toInt(); ui->resources->setItem(i, 0, new QTableWidgetItem(name)); auto nval = new QTableWidgetItem(QString::number(val)); nval->setFlags(nval->flags() | Qt::ItemIsEditable); diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp index 8a3ba6490..f98956a22 100644 --- a/mapeditor/mapsettings/translations.cpp +++ b/mapeditor/mapsettings/translations.cpp @@ -62,6 +62,8 @@ Translations::Translations(CMapHeader & mh, QWidget *parent) : setAttribute(Qt::WA_DeleteOnClose, true); ui->setupUi(this); + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint); + //fill languages list std::set indexFoundLang; int foundLang = -1; diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp index 51b23cf7e..487a04d05 100644 --- a/mapeditor/mapsettings/victoryconditions.cpp +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -407,7 +407,9 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) { for(int resType = 0; resType < GameConstants::RESOURCE_QUANTITY; ++resType) { - auto resName = QString::fromStdString(GameConstants::RESOURCE_NAMES[resType]); + MetaString str; + str.appendName(GameResID(resType)); + auto resName = QString::fromStdString(str.toString()); victoryTypeWidget->addItem(resName, QVariant::fromValue(resType)); } } @@ -427,7 +429,7 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) victorySelectWidget = new QComboBox; ui->victoryParamsLayout->addWidget(victorySelectWidget); - victorySelectWidget->addItem("Any town", QVariant::fromValue(-1)); + victorySelectWidget->addItem(tr("Any town"), QVariant::fromValue(-1)); for(int i : getObjectIndexes(*controller->map())) victorySelectWidget->addItem(getTownName(*controller->map(), i).c_str(), QVariant::fromValue(i)); diff --git a/mapeditor/playerparams.cpp b/mapeditor/playerparams.cpp index d4c7872d1..5d8eb2f44 100644 --- a/mapeditor/playerparams.cpp +++ b/mapeditor/playerparams.cpp @@ -29,7 +29,9 @@ PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent) { if(i == playerId || !controller.map()->players[i].canAnyonePlay()) { - ui->playerColorCombo->addItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i]), QVariant(i)); + MetaString str; + str.appendName(PlayerColor(i)); + ui->playerColorCombo->addItem(QString::fromStdString(str.toString()), QVariant(i)); if(i == playerId) ui->playerColorCombo->setCurrentIndex(index); ++index; diff --git a/mapeditor/playersettings.cpp b/mapeditor/playersettings.cpp index ef4a382c2..1efcb436a 100644 --- a/mapeditor/playersettings.cpp +++ b/mapeditor/playersettings.cpp @@ -23,6 +23,9 @@ PlayerSettings::PlayerSettings(MapController & ctrl, QWidget *parent) : { ui->setupUi(this); controller.settingsDialog = this; + + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint); + show(); int players = 0; diff --git a/mapeditor/translation/chinese.ts b/mapeditor/translation/chinese.ts index 6c4676775..3f2a37627 100644 --- a/mapeditor/translation/chinese.ts +++ b/mapeditor/translation/chinese.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? 未保存的改动会丢失,你确定要这么做吗? + + + Mods are required + + + + + Failed to open map + + Open map @@ -657,10 +667,30 @@ 所有支持的地图类型(*.vmap *.h3m);;VCMI地图(*.vmap);;英雄无敌3地图(*.h3m) - + Recently Opened Files 最近打开文件 + + + Map validation + + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + + + + + Map has some errors. Open Validator from the Map menu to see issues found + + + + + Failed to save map + + Save map @@ -756,6 +786,16 @@ Other 其他 + + + Mods loading problem + + + + + Critical error during Mods loading. Disable invalid mods and restart. + + View surface @@ -997,7 +1037,7 @@ 无队伍 - + Player ID: %1 玩家ID: %1 @@ -1062,52 +1102,293 @@ 高级 - + Compliant 屈服的 - + Friendly 友善的 - + Aggressive 好斗的 - + Hostile 有敌意的 - + Savage 野蛮的 - - + + + No patrol 无巡逻 + + + POWER RANK + + + + + HERO TYPE + + + + + Hero class + + + + + Portrait + 头像 + + + + Skills + 技能 + + + + Artifacts + 宝物 + - - + + %n tile(s) %n格 - - + + Buildings + 建筑 + + + + Events + 事件 + + + + Resource + + + + + Artifact reward + + + + + Quest + + + + + Identifier + + + + + ID + + + + + SubID + + + + + InstanceName + + + + + IsStatic + + + + + neutral 中立 - + + Army + + + + + Owner + + + + + Same as town + + + + + Removable units + + + + + Placeholder type + + + + + Power rank + + + + + Hero type + + + + + Experience + + + + + MALE + + + + + FEMALE + + + + + Gender + + + + + Name + + + + + Biography + + + + + Spells + 魔法 + + + + Patrol radius + + + + + Town name + + + + + Message + 消息 + + + + Spell + 魔法 + + + + Productivity + + + + + Amount + + + + + Character + + + + + Never flees + + + + + Not growing + + + + + Reward + 奖励 + + + + Remove after + + + + + Human trigger + + + + + Cpu trigger + + + + + First visit text + + + + + Next visit text + + + + + Completed text + + + + + Repeat quest + + + + + Time limit + + + + UNFLAGGABLE 没有旗帜 @@ -1533,8 +1814,8 @@ %1 天 - - + + Reward %1 奖励 %1 @@ -1756,12 +2037,12 @@ 确定 - + Creature level %1 / Creature level %1 Upgrade %1级生物 / 升级后的%1级生物 - + Day %1 - %2 %1 - %2 日 @@ -2089,6 +2370,11 @@ Kill monster 击杀怪物 + + + Any town + + WindowNewMap @@ -2291,6 +2577,11 @@ RMG failure 随机地图生成失败 + + + [default] + + main diff --git a/mapeditor/translation/czech.ts b/mapeditor/translation/czech.ts index 515dae06a..7a4e8f4d2 100644 --- a/mapeditor/translation/czech.ts +++ b/mapeditor/translation/czech.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? Neuložené změny budou ztraceny, jste si jisti? + + + Mods are required + + + + + Failed to open map + + Open map @@ -657,10 +667,30 @@ Všechny podporované mapy (*.vmap *.h3m);; Mapy VCMI(*.vmap);;Mapy HoMM3(*.h3m) - + Recently Opened Files Naposledny otevřené soubory + + + Map validation + + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + + + + + Map has some errors. Open Validator from the Map menu to see issues found + + + + + Failed to save map + + Save map @@ -756,6 +786,16 @@ Other Ostatní + + + Mods loading problem + + + + + Critical error during Mods loading. Disable invalid mods and restart. + + View surface @@ -997,7 +1037,7 @@ Bez týmu - + Player ID: %1 ID hráče: %1 @@ -1062,39 +1102,70 @@ Expert - + Compliant Ochotná - + Friendly Přátelská - + Aggressive Agresivní - + Hostile Nepřátelská - + Savage Brutální - - + + + No patrol Bez hlídky + + + POWER RANK + + + + + HERO TYPE + + + + + Hero class + + + + + Portrait + Portrét + + + + Skills + Dovednosti + + + + Artifacts + Artefakty + - - + + %n tile(s) %n pole @@ -1103,13 +1174,223 @@ - - + + Buildings + Budovy + + + + Events + Události + + + + Resource + + + + + Artifact reward + + + + + Quest + + + + + Identifier + + + + + ID + + + + + SubID + + + + + InstanceName + + + + + IsStatic + + + + + neutral neutrální - + + Army + + + + + Owner + + + + + Same as town + + + + + Removable units + + + + + Placeholder type + + + + + Power rank + + + + + Hero type + + + + + Experience + + + + + MALE + + + + + FEMALE + + + + + Gender + + + + + Name + + + + + Biography + + + + + Spells + Kouzla + + + + Patrol radius + + + + + Town name + + + + + Message + Zpráva + + + + Spell + Kouzlo + + + + Productivity + + + + + Amount + + + + + Character + + + + + Never flees + + + + + Not growing + + + + + Reward + Odměna + + + + Remove after + + + + + Human trigger + + + + + Cpu trigger + + + + + First visit text + + + + + Next visit text + + + + + Completed text + + + + + Repeat quest + + + + + Time limit + + + + UNFLAGGABLE NEOZNAČITELNÝ @@ -1535,8 +1816,8 @@ Den %1 - - + + Reward %1 Odměna %1 @@ -1758,12 +2039,12 @@ OK - + Creature level %1 / Creature level %1 Upgrade Úroveň jednotky %1 / Úroveň jednotky%1 vylepšení - + Day %1 - %2 Den %1 - %2 @@ -2091,6 +2372,11 @@ Kill monster Zabít příšeru + + + Any town + + WindowNewMap @@ -2293,6 +2579,11 @@ RMG failure Chyba RMG + + + [default] + + main diff --git a/mapeditor/translation/english.ts b/mapeditor/translation/english.ts index 34c19c69d..a3f8bfa49 100644 --- a/mapeditor/translation/english.ts +++ b/mapeditor/translation/english.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? + + + Mods are required + + + + + Failed to open map + + Open map @@ -657,10 +667,30 @@ - + Recently Opened Files + + + Map validation + + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + + + + + Map has some errors. Open Validator from the Map menu to see issues found + + + + + Failed to save map + + Save map @@ -756,6 +786,16 @@ Other + + + Mods loading problem + + + + + Critical error during Mods loading. Disable invalid mods and restart. + + View surface @@ -997,7 +1037,7 @@ - + Player ID: %1 @@ -1062,39 +1102,70 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + + No patrol + + + POWER RANK + + + + + HERO TYPE + + + + + Hero class + + + + + Portrait + + + + + Skills + + + + + Artifacts + + - - + + %n tile(s) @@ -1102,13 +1173,223 @@ - - + + Buildings + + + + + Events + + + + + Resource + + + + + Artifact reward + + + + + Quest + + + + + Identifier + + + + + ID + + + + + SubID + + + + + InstanceName + + + + + IsStatic + + + + + neutral - + + Army + + + + + Owner + + + + + Same as town + + + + + Removable units + + + + + Placeholder type + + + + + Power rank + + + + + Hero type + + + + + Experience + + + + + MALE + + + + + FEMALE + + + + + Gender + + + + + Name + + + + + Biography + + + + + Spells + + + + + Patrol radius + + + + + Town name + + + + + Message + + + + + Spell + + + + + Productivity + + + + + Amount + + + + + Character + + + + + Never flees + + + + + Not growing + + + + + Reward + + + + + Remove after + + + + + Human trigger + + + + + Cpu trigger + + + + + First visit text + + + + + Next visit text + + + + + Completed text + + + + + Repeat quest + + + + + Time limit + + + + UNFLAGGABLE @@ -1534,8 +1815,8 @@ - - + + Reward %1 @@ -1757,12 +2038,12 @@ - + Creature level %1 / Creature level %1 Upgrade - + Day %1 - %2 @@ -2090,6 +2371,11 @@ Kill monster + + + Any town + + WindowNewMap @@ -2292,6 +2578,11 @@ RMG failure + + + [default] + + main diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index 60f5aae48..8e4e9f946 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? Les modifications non sauvegardées seront perdues. Êtes-vous sûr ? + + + Mods are required + + + + + Failed to open map + + Open map @@ -657,10 +667,30 @@ Toutes les cartes prises en charge (*.vmap *.h3m);;Cartes VCMI (*.vmap);;Cartes HoMM3 (*.h3m) - + Recently Opened Files + + + Map validation + + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + + + + + Map has some errors. Open Validator from the Map menu to see issues found + + + + + Failed to save map + + Save map @@ -756,6 +786,16 @@ Other + + + Mods loading problem + + + + + Critical error during Mods loading. Disable invalid mods and restart. + + View surface @@ -997,7 +1037,7 @@ - + Player ID: %1 Identifiant du joueur : %1 @@ -1062,39 +1102,70 @@ Expert - + Compliant Compérhensif - + Friendly Amical - + Aggressive Aggressif - + Hostile Hostile - + Savage Sauvage - - + + + No patrol + + + POWER RANK + + + + + HERO TYPE + + + + + Hero class + + + + + Portrait + Portrait + + + + Skills + + + + + Artifacts + + - - + + %n tile(s) @@ -1102,13 +1173,223 @@ - - + + Buildings + Bâtiments + + + + Events + Événements + + + + Resource + + + + + Artifact reward + + + + + Quest + + + + + Identifier + + + + + ID + + + + + SubID + + + + + InstanceName + + + + + IsStatic + + + + + neutral neutre - + + Army + + + + + Owner + + + + + Same as town + + + + + Removable units + + + + + Placeholder type + + + + + Power rank + + + + + Hero type + + + + + Experience + + + + + MALE + + + + + FEMALE + + + + + Gender + + + + + Name + + + + + Biography + + + + + Spells + Sorts + + + + Patrol radius + + + + + Town name + + + + + Message + Message + + + + Spell + Sort + + + + Productivity + + + + + Amount + + + + + Character + + + + + Never flees + + + + + Not growing + + + + + Reward + Récompense + + + + Remove after + + + + + Human trigger + + + + + Cpu trigger + + + + + First visit text + + + + + Next visit text + + + + + Completed text + + + + + Repeat quest + + + + + Time limit + + + + UNFLAGGABLE INCLASSABLE @@ -1534,8 +1815,8 @@ Jour %1 - - + + Reward %1 Récompense %1 @@ -1757,12 +2038,12 @@ OK - + Creature level %1 / Creature level %1 Upgrade Créature niveau %1 / Créature niveau %1 Augmenté - + Day %1 - %2 Jour %1 - %2 @@ -2090,6 +2371,11 @@ Kill monster Tuer un monster + + + Any town + + WindowNewMap @@ -2292,6 +2578,11 @@ RMG failure Echec de RMG + + + [default] + + main diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index 509daef6f..247a9ef37 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? Ungespeicherte Änderungen gehen verloren, sind sie sicher? + + + Mods are required + Mods sind erforderlich + + + + Failed to open map + Karte konnte nicht geöffnet werden + Open map @@ -657,10 +667,30 @@ Alle unterstützten Karten (*.vmap *.h3m);;VCMI-Karten (*.vmap);;HoMM3-Karten (*.h3m) - + Recently Opened Files Kürzlich geöffnete Dateien + + + Map validation + Validierung der Karte + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + Die Karte hat kritische Probleme und wird höchstwahrscheinlich nicht spielbar sein. Öffnen Sie den Validator aus dem Kartenmenü, um die gefundenen Probleme zu sehen + + + + Map has some errors. Open Validator from the Map menu to see issues found + Karte hat einige Fehler. Öffnen Sie den Validator aus dem Kartenmenü, um die gefundenen Probleme zu sehen + + + + Failed to save map + Karte konnte nicht gespeichert werden + Save map @@ -756,6 +786,16 @@ Other Anderes + + + Mods loading problem + Problem beim Laden von Mods + + + + Critical error during Mods loading. Disable invalid mods and restart. + Kritischer Fehler beim Laden von Mods. Deaktivieren Sie ungültige Mods und starten Sie neu. + View surface @@ -997,7 +1037,7 @@ Kein Team - + Player ID: %1 Spieler-ID: %1 @@ -1062,39 +1102,70 @@ Experte - + Compliant Konform - + Friendly Freundlich - + Aggressive Aggressiv - + Hostile Feindlich - + Savage Wild - - + + + No patrol Keine Streife + + + POWER RANK + STÄRKE RANG + + + + HERO TYPE + HELDENTYP + + + + Hero class + Heldenklasse + + + + Portrait + Porträt + + + + Skills + Fertigkeiten + + + + Artifacts + Artefakte + - - + + %n tile(s) %n Kachel @@ -1102,13 +1173,223 @@ - - + + Buildings + Gebäude + + + + Events + Ereignisse + + + + Resource + Ressource + + + + Artifact reward + Artefakt-Belohnung + + + + Quest + Aufgabe + + + + Identifier + Identifier + + + + ID + ID + + + + SubID + SubID + + + + InstanceName + InstanzName + + + + IsStatic + IstStatisch + + + + neutral neutral - + + Army + Armee + + + + Owner + Besitzer + + + + Same as town + Wie die Stadt + + + + Removable units + Entfernbare Einheiten + + + + Placeholder type + Platzhalter-Typ + + + + Power rank + Stärke Rang + + + + Hero type + Helden-Typ + + + + Experience + Erfahrung + + + + MALE + MÄNNLICH + + + + FEMALE + WEIBLICH + + + + Gender + Geschlecht + + + + Name + Name + + + + Biography + Biografie + + + + Spells + Zaubersprüche + + + + Patrol radius + Patrouillenradius + + + + Town name + Stadtname + + + + Message + Nachricht + + + + Spell + Zauberspruch + + + + Productivity + Produktivität + + + + Amount + Menge + + + + Character + Charakter + + + + Never flees + Flieht niemals + + + + Not growing + Wächst nicht + + + + Reward + Belohnung + + + + Remove after + Nach Entfernen + + + + Human trigger + Menschlich-Auslöser + + + + Cpu trigger + CPU-Auslöser + + + + First visit text + Erster Besuch Text + + + + Next visit text + Nächster Besuch Text + + + + Completed text + Abgeschlossener Text + + + + Repeat quest + Wiederhole Quest + + + + Time limit + Zeitlimit + + + UNFLAGGABLE UNFLAGGBAR @@ -1534,8 +1815,8 @@ Tag %1 - - + + Reward %1 Belohnung %1 @@ -1757,12 +2038,12 @@ OK - + Creature level %1 / Creature level %1 Upgrade Kreaturlevel %1 / Kreaturlevel %1 Aufgerüstet - + Day %1 - %2 Tag %1 - %2 @@ -2090,6 +2371,11 @@ Kill monster Monster töten + + + Any town + Jede Stadt + WindowNewMap @@ -2292,6 +2578,11 @@ RMG failure RMG-Fehler + + + [default] + [Standard] + main diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index 9db04426d..6ebc6b9ae 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? Niezapisane zmiany zostaną utracone, jesteś pewny? + + + Mods are required + Wymagane są mody + + + + Failed to open map + Nie udało się otworzyć mapy + Open map @@ -657,10 +667,30 @@ Wszystkie wspierane mapy (*.vmap *.h3m);;Mapy VCMI(*.vmap);;Mapy HoMM3(*.h3m) - + Recently Opened Files Ostatnio otwierane pliki + + + Map validation + Walidacja mapy + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + Mapa ma krytyczne problemy i prawdopodobnie nie będzie grywalna. Otwórz Walidator z menu Mapy, aby zobaczyć znalezione problemy. + + + + Map has some errors. Open Validator from the Map menu to see issues found + Mapa zawiera błędy. Otwórz Walidator z menu Mapy, aby zobaczyć znalezione problemy. + + + + Failed to save map + Nie udało się zapisać mapy + Save map @@ -756,6 +786,16 @@ Other Inne + + + Mods loading problem + Problem z ładowaniem modów + + + + Critical error during Mods loading. Disable invalid mods and restart. + Krytyczny błąd podczas ładowania modów. Wyłącz nieprawidłowe mody i uruchom ponownie. + View surface @@ -997,7 +1037,7 @@ Bez sojuszu - + Player ID: %1 ID gracza: %1 @@ -1062,39 +1102,70 @@ Ekspert - + Compliant Przyjazny - + Friendly Przychylny - + Aggressive Agresywny - + Hostile Wrogi - + Savage Nienawistny - - + + + No patrol Brak patrolu + + + POWER RANK + RANKING SIŁY + + + + HERO TYPE + TYP BOHATERA + + + + Hero class + Klasa bohatera + + + + Portrait + Portret + + + + Skills + Umiejętności + + + + Artifacts + Artefakty + - - + + %n tile(s) %n pole @@ -1103,13 +1174,223 @@ - - + + Buildings + Budynki + + + + Events + Zdarzenia + + + + Resource + Zasób + + + + Artifact reward + Nagroda: Artefakt + + + + Quest + Misja + + + + Identifier + Identyfikator + + + + ID + ID + + + + SubID + SubID + + + + InstanceName + Nazwa Instancji + + + + IsStatic + Statyczny + + + + neutral neutralny - + + Army + Armia + + + + Owner + Właściciel + + + + Same as town + Taki sam jak miasto + + + + Removable units + Jednostki do usunięcia + + + + Placeholder type + Typ zastępczy + + + + Power rank + Ranking siły + + + + Hero type + Typ bohatera + + + + Experience + Doświadczenie + + + + MALE + MĘŻCZYZNA + + + + FEMALE + KOBIETA + + + + Gender + Płeć + + + + Name + Nazwa + + + + Biography + Biografia + + + + Spells + Zaklęcia + + + + Patrol radius + Promień patrolu + + + + Town name + Nazwa miasta + + + + Message + Wiadomość + + + + Spell + Zaklęcie + + + + Productivity + Produktywność + + + + Amount + Ilość + + + + Character + Charakter + + + + Never flees + Nigdy nie ucieka + + + + Not growing + Nie rośnie + + + + Reward + Nagroda + + + + Remove after + Usuń po + + + + Human trigger + Wyzwalacz gracza + + + + Cpu trigger + Wyzwalacz SI + + + + First visit text + Tekst pierwszej wizyty + + + + Next visit text + Tekst następnej wizyty + + + + Completed text + Tekst po zakończeniu + + + + Repeat quest + Powtarzaj misję + + + + Time limit + Limit czasu + + + UNFLAGGABLE NIEFLAGOWALNY @@ -1535,8 +1816,8 @@ Dzień %1 - - + + Reward %1 Nagroda %1 @@ -1758,12 +2039,12 @@ OK - + Creature level %1 / Creature level %1 Upgrade Stworzenie poziomu %1 / Ulepszone stworzenie poziomu %1 - + Day %1 - %2 Dzień %1 - %2 @@ -2091,6 +2372,11 @@ Kill monster Zabij potwora + + + Any town + Dowolne miasto + WindowNewMap @@ -2293,6 +2579,11 @@ RMG failure Niepowodzenie generatora map losowych + + + [default] + [domyślny] + main diff --git a/mapeditor/translation/portuguese.ts b/mapeditor/translation/portuguese.ts index 0b049373f..7ef993eca 100644 --- a/mapeditor/translation/portuguese.ts +++ b/mapeditor/translation/portuguese.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? As alterações não salvas serão perdidas. Tem certeza? + + + Mods are required + + + + + Failed to open map + + Open map @@ -657,10 +667,30 @@ Todos os mapas suportados (*.vmap *.h3m);;Mapas do VCMI (*.vmap);;Mapas do HoMM3 (*.h3m) - + Recently Opened Files + + + Map validation + + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + + + + + Map has some errors. Open Validator from the Map menu to see issues found + + + + + Failed to save map + + Save map @@ -756,6 +786,16 @@ Other + + + Mods loading problem + + + + + Critical error during Mods loading. Disable invalid mods and restart. + + View surface @@ -997,7 +1037,7 @@ - + Player ID: %1 ID do Jogador: %1 @@ -1062,39 +1102,70 @@ Experiente - + Compliant Complacente - + Friendly Amigável - + Aggressive Agressivo - + Hostile Hostil - + Savage Selvagem - - + + + No patrol Sem patrulha + + + POWER RANK + + + + + HERO TYPE + + + + + Hero class + + + + + Portrait + Retrato + + + + Skills + Habilidades + + + + Artifacts + Artefatos + - - + + %n tile(s) %n bloco @@ -1102,13 +1173,223 @@ - - + + Buildings + Estruturas + + + + Events + Eventos + + + + Resource + + + + + Artifact reward + + + + + Quest + + + + + Identifier + + + + + ID + + + + + SubID + + + + + InstanceName + + + + + IsStatic + + + + + neutral neutro - + + Army + + + + + Owner + + + + + Same as town + + + + + Removable units + + + + + Placeholder type + + + + + Power rank + + + + + Hero type + + + + + Experience + + + + + MALE + + + + + FEMALE + + + + + Gender + + + + + Name + + + + + Biography + + + + + Spells + Feitiços + + + + Patrol radius + + + + + Town name + + + + + Message + Mensagem + + + + Spell + Feitiço + + + + Productivity + + + + + Amount + + + + + Character + + + + + Never flees + + + + + Not growing + + + + + Reward + Recompensa + + + + Remove after + + + + + Human trigger + + + + + Cpu trigger + + + + + First visit text + + + + + Next visit text + + + + + Completed text + + + + + Repeat quest + + + + + Time limit + + + + UNFLAGGABLE NÃO TEM BANDEIRA @@ -1534,8 +1815,8 @@ Dia %1 - - + + Reward %1 Recompensa %1 @@ -1757,12 +2038,12 @@ OK - + Creature level %1 / Creature level %1 Upgrade Nível da criatura %1 / Nível da criatura %1 - Atualização - + Day %1 - %2 Dia %1 - %2 @@ -2090,6 +2371,11 @@ Kill monster Matar monstro + + + Any town + + WindowNewMap @@ -2292,6 +2578,11 @@ RMG failure Falha do GMA + + + [default] + + main diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts index d079f6d1c..1bea21ec3 100644 --- a/mapeditor/translation/russian.ts +++ b/mapeditor/translation/russian.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? Несохранённые изменения будут потеряны. Вы уверены? + + + Mods are required + + + + + Failed to open map + + Open map @@ -657,10 +667,30 @@ Все поддерживаемые карты (*.vmap *.h3m);;Карты VCMI (*.vmap);;Карты Героев III (*.h3m) - + Recently Opened Files Недавно открытые файлы + + + Map validation + + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + + + + + Map has some errors. Open Validator from the Map menu to see issues found + + + + + Failed to save map + + Save map @@ -756,6 +786,16 @@ Other Прочее + + + Mods loading problem + + + + + Critical error during Mods loading. Disable invalid mods and restart. + + View surface @@ -997,7 +1037,7 @@ Без команды - + Player ID: %1 Игрок: %1 @@ -1062,39 +1102,70 @@ Эксперт - + Compliant Сговорчивый - + Friendly Дружелюбный - + Aggressive Агрессивный - + Hostile Враждебный - + Savage Дикий - - + + + No patrol Без патруля + + + POWER RANK + + + + + HERO TYPE + + + + + Hero class + + + + + Portrait + Портрет + + + + Skills + Навыки + + + + Artifacts + Артефакты + - - + + %n tile(s) @@ -1103,13 +1174,223 @@ - - + + Buildings + Постройки + + + + Events + События + + + + Resource + + + + + Artifact reward + + + + + Quest + + + + + Identifier + + + + + ID + + + + + SubID + + + + + InstanceName + + + + + IsStatic + + + + + neutral нейтральный - + + Army + + + + + Owner + + + + + Same as town + + + + + Removable units + + + + + Placeholder type + + + + + Power rank + + + + + Hero type + + + + + Experience + + + + + MALE + + + + + FEMALE + + + + + Gender + + + + + Name + + + + + Biography + + + + + Spells + Заклинания + + + + Patrol radius + + + + + Town name + + + + + Message + Сообщение + + + + Spell + Заклинание + + + + Productivity + + + + + Amount + + + + + Character + + + + + Never flees + + + + + Not growing + + + + + Reward + Награда + + + + Remove after + + + + + Human trigger + + + + + Cpu trigger + + + + + First visit text + + + + + Next visit text + + + + + Completed text + + + + + Repeat quest + + + + + Time limit + + + + UNFLAGGABLE НЕФЛАГУЕМЫЙ @@ -1535,8 +1816,8 @@ День %1 - - + + Reward %1 Награда %1 @@ -1758,12 +2039,12 @@ ОК - + Creature level %1 / Creature level %1 Upgrade Уровень существа %1 / Уровень существа %1 Улучшение - + Day %1 - %2 День %1 - %2 @@ -2091,6 +2372,11 @@ Kill monster Убить монстра + + + Any town + + WindowNewMap @@ -2293,6 +2579,11 @@ RMG failure Ошибка генерации случайной карты + + + [default] + + main diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index 78e43a61b..69c6a4761 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? Los cambios no guardados se perderán. Está usted seguro ? + + + Mods are required + + + + + Failed to open map + + Open map @@ -657,10 +667,30 @@ Todos los mapas soportados (*.vmap *.h3m);;Mapas VCMI (*.vmap);;Mapas HoMM3 (*.h3m) - + Recently Opened Files + + + Map validation + + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + + + + + Map has some errors. Open Validator from the Map menu to see issues found + + + + + Failed to save map + + Save map @@ -756,6 +786,16 @@ Other + + + Mods loading problem + + + + + Critical error during Mods loading. Disable invalid mods and restart. + + View surface @@ -997,7 +1037,7 @@ - + Player ID: %1 ID de jugador: %1 @@ -1062,39 +1102,70 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + + No patrol + + + POWER RANK + + + + + HERO TYPE + + + + + Hero class + + + + + Portrait + + + + + Skills + + + + + Artifacts + Artefactos + - - + + %n tile(s) @@ -1102,13 +1173,223 @@ - - + + Buildings + Edificios + + + + Events + Eventos + + + + Resource + + + + + Artifact reward + + + + + Quest + + + + + Identifier + + + + + ID + + + + + SubID + + + + + InstanceName + + + + + IsStatic + + + + + neutral - + + Army + + + + + Owner + + + + + Same as town + + + + + Removable units + + + + + Placeholder type + + + + + Power rank + + + + + Hero type + + + + + Experience + + + + + MALE + + + + + FEMALE + + + + + Gender + + + + + Name + + + + + Biography + + + + + Spells + Hechizos + + + + Patrol radius + + + + + Town name + + + + + Message + Mensaje + + + + Spell + + + + + Productivity + + + + + Amount + + + + + Character + + + + + Never flees + + + + + Not growing + + + + + Reward + + + + + Remove after + + + + + Human trigger + + + + + Cpu trigger + + + + + First visit text + + + + + Next visit text + + + + + Completed text + + + + + Repeat quest + + + + + Time limit + + + + UNFLAGGABLE @@ -1534,8 +1815,8 @@ - - + + Reward %1 @@ -1757,12 +2038,12 @@ - + Creature level %1 / Creature level %1 Upgrade - + Day %1 - %2 @@ -2090,6 +2371,11 @@ Kill monster + + + Any town + + WindowNewMap @@ -2292,6 +2578,11 @@ RMG failure + + + [default] + + main diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts index ae33427a7..09bb32ac0 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? + + + Mods are required + + + + + Failed to open map + + Open map @@ -657,10 +667,30 @@ Всі підтримувані мапи (*.vmap *.h3m);;Мапи VCMI (*.vmap);;Мапи HoMM3 (*.h3m) - + Recently Opened Files + + + Map validation + + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + + + + + Map has some errors. Open Validator from the Map menu to see issues found + + + + + Failed to save map + + Save map @@ -756,6 +786,16 @@ Other + + + Mods loading problem + + + + + Critical error during Mods loading. Disable invalid mods and restart. + + View surface @@ -997,7 +1037,7 @@ - + Player ID: %1 Гравець %1 @@ -1062,39 +1102,70 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + + No patrol + + + POWER RANK + + + + + HERO TYPE + + + + + Hero class + + + + + Portrait + + + + + Skills + Вміння + + + + Artifacts + Артефакти + - - + + %n tile(s) @@ -1103,13 +1174,223 @@ - - + + Buildings + Будівлі + + + + Events + Події + + + + Resource + + + + + Artifact reward + + + + + Quest + + + + + Identifier + + + + + ID + + + + + SubID + + + + + InstanceName + + + + + IsStatic + + + + + neutral - + + Army + + + + + Owner + + + + + Same as town + + + + + Removable units + + + + + Placeholder type + + + + + Power rank + + + + + Hero type + + + + + Experience + + + + + MALE + + + + + FEMALE + + + + + Gender + + + + + Name + + + + + Biography + + + + + Spells + Закляття + + + + Patrol radius + + + + + Town name + + + + + Message + Повідомлення + + + + Spell + + + + + Productivity + + + + + Amount + + + + + Character + + + + + Never flees + + + + + Not growing + + + + + Reward + + + + + Remove after + + + + + Human trigger + + + + + Cpu trigger + + + + + First visit text + + + + + Next visit text + + + + + Completed text + + + + + Repeat quest + + + + + Time limit + + + + UNFLAGGABLE @@ -1535,8 +1816,8 @@ - - + + Reward %1 @@ -1758,12 +2039,12 @@ - + Creature level %1 / Creature level %1 Upgrade - + Day %1 - %2 @@ -2091,6 +2372,11 @@ Kill monster + + + Any town + + WindowNewMap @@ -2293,6 +2579,11 @@ RMG failure + + + [default] + + main diff --git a/mapeditor/translation/vietnamese.ts b/mapeditor/translation/vietnamese.ts index 3bdb1dd75..bcc2815d7 100644 --- a/mapeditor/translation/vietnamese.ts +++ b/mapeditor/translation/vietnamese.ts @@ -646,6 +646,16 @@ Unsaved changes will be lost, are you sure? Thay đổi chưa lưu sẽ bị mất, bạn có chắc chắn? + + + Mods are required + + + + + Failed to open map + + Open map @@ -657,10 +667,30 @@ Tất cả bản đồ hỗ trợ (*.vmap *.h3m);;Bản đồ VCMI (*.vmap);;Bản đồ HoMM3 (*.h3m) - + Recently Opened Files + + + Map validation + + + + + Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found + + + + + Map has some errors. Open Validator from the Map menu to see issues found + + + + + Failed to save map + + Save map @@ -756,6 +786,16 @@ Other + + + Mods loading problem + + + + + Critical error during Mods loading. Disable invalid mods and restart. + + View surface @@ -997,7 +1037,7 @@ - + Player ID: %1 ID người chơi: %1 @@ -1062,52 +1102,293 @@ - + Compliant - + Friendly - + Aggressive - + Hostile - + Savage - - + + + No patrol + + + POWER RANK + + + + + HERO TYPE + + + + + Hero class + + + + + Portrait + + + + + Skills + + + + + Artifacts + Vật phẩm + - - + + %n tile(s) - - + + Buildings + Công trình + + + + Events + Sự kiện + + + + Resource + + + + + Artifact reward + + + + + Quest + + + + + Identifier + + + + + ID + + + + + SubID + + + + + InstanceName + + + + + IsStatic + + + + + neutral - + + Army + + + + + Owner + + + + + Same as town + + + + + Removable units + + + + + Placeholder type + + + + + Power rank + + + + + Hero type + + + + + Experience + + + + + MALE + + + + + FEMALE + + + + + Gender + + + + + Name + + + + + Biography + + + + + Spells + Phép + + + + Patrol radius + + + + + Town name + + + + + Message + Thông báo + + + + Spell + + + + + Productivity + + + + + Amount + + + + + Character + + + + + Never flees + + + + + Not growing + + + + + Reward + + + + + Remove after + + + + + Human trigger + + + + + Cpu trigger + + + + + First visit text + + + + + Next visit text + + + + + Completed text + + + + + Repeat quest + + + + + Time limit + + + + UNFLAGGABLE @@ -1533,8 +1814,8 @@ - - + + Reward %1 @@ -1756,12 +2037,12 @@ - + Creature level %1 / Creature level %1 Upgrade - + Day %1 - %2 @@ -2089,6 +2370,11 @@ Kill monster + + + Any town + + WindowNewMap @@ -2291,6 +2577,11 @@ RMG failure Tạo bản đồ ngẫu nhiên thất bại + + + [default] + + main diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index a2d965e49..1c8508914 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -24,7 +24,9 @@ Validator::Validator(const CMap * map, QWidget *parent) : ui(new Ui::Validator) { ui->setupUi(this); - + + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint); + show(); setAttribute(Qt::WA_DeleteOnClose); diff --git a/mapeditor/windownewmap.cpp b/mapeditor/windownewmap.cpp index cf67dc8f4..c4abfb2b9 100644 --- a/mapeditor/windownewmap.cpp +++ b/mapeditor/windownewmap.cpp @@ -33,6 +33,8 @@ WindowNewMap::WindowNewMap(QWidget *parent) : { ui->setupUi(this); + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint); + setAttribute(Qt::WA_DeleteOnClose); setWindowModality(Qt::ApplicationModal); @@ -55,7 +57,9 @@ WindowNewMap::WindowNewMap(QWidget *parent) : ui->cpuTeamsCombo->addItem(!i ? randomString : QString::number(cpuPlayers.at(i))); ui->cpuTeamsCombo->setItemData(i, QVariant(cpuPlayers.at(i))); } - + + on_sizeStandardRadio_toggled(true); + on_checkSeed_toggled(false); bool useLoaded = loadUserSettings(); if (!useLoaded) @@ -107,8 +111,8 @@ bool WindowNewMap::loadUserSettings() handler.serializeStruct("lastSettings", mapGenOptions); templ = const_cast(mapGenOptions.getMapTemplate()); // Remember for later - ui->widthTxt->setText(QString::number(mapGenOptions.getWidth())); - ui->heightTxt->setText(QString::number(mapGenOptions.getHeight())); + ui->widthTxt->setValue(mapGenOptions.getWidth()); + ui->heightTxt->setValue(mapGenOptions.getHeight()); for(const auto & sz : mapSizes) { if(sz.second.first == mapGenOptions.getWidth() && @@ -217,6 +221,17 @@ std::unique_ptr generateEmptyMap(CMapGenOptions & options) return map; } +std::pair getSelectedMapSize(QComboBox* comboBox, const std::map>& mapSizes) { + int selectedIndex = comboBox->currentIndex(); + + auto it = mapSizes.find(selectedIndex); + if (it != mapSizes.end()) { + return it->second; // Return the width and height pair + } + + return { 0, 0 }; +} + void WindowNewMap::on_okButton_clicked() { EWaterContent::EWaterContent water = EWaterContent::RANDOM; @@ -245,6 +260,18 @@ void WindowNewMap::on_okButton_clicked() mapGenOptions.setRoadEnabled(Road::GRAVEL_ROAD, ui->roadGravel->isChecked()); mapGenOptions.setRoadEnabled(Road::COBBLESTONE_ROAD, ui->roadCobblestone->isChecked()); + if(ui->sizeStandardRadio->isChecked()) + { + auto size = getSelectedMapSize(ui->sizeCombo, mapSizes); + mapGenOptions.setWidth(size.first); + mapGenOptions.setHeight(size.second); + } + else + { + mapGenOptions.setWidth(ui->widthTxt->value()); + mapGenOptions.setHeight(ui->heightTxt->value()); + } + saveUserSettings(); std::unique_ptr nmap; @@ -257,9 +284,11 @@ void WindowNewMap::on_okButton_clicked() return; } + hide(); + int seed = std::time(nullptr); - if(ui->checkSeed->isChecked() && !ui->lineSeed->text().isEmpty()) - seed = ui->lineSeed->text().toInt(); + if(ui->checkSeed->isChecked() && ui->lineSeed->value() != 0) + seed = ui->lineSeed->value(); CMapGenerator generator(mapGenOptions, nullptr, seed); auto progressBarWnd = new GeneratorProgress(generator, this); @@ -290,8 +319,10 @@ void WindowNewMap::on_okButton_clicked() void WindowNewMap::on_sizeCombo_activated(int index) { - ui->widthTxt->setText(QString::number(mapSizes.at(index).first)); - ui->heightTxt->setText(QString::number(mapSizes.at(index).second)); + auto size = getSelectedMapSize(ui->sizeCombo, mapSizes); + mapGenOptions.setWidth(size.first); + mapGenOptions.setHeight(size.second); + updateTemplateList(); } @@ -312,8 +343,6 @@ void WindowNewMap::on_humanCombo_activated(int index) ui->humanCombo->setCurrentIndex(humans); } - mapGenOptions.setHumanOrCpuPlayerCount(humans); - int teams = mapGenOptions.getTeamCount(); if(teams > humans - 1) { @@ -335,6 +364,8 @@ void WindowNewMap::on_humanCombo_activated(int index) ui->cpuTeamsCombo->setCurrentIndex(cpuTeams + 1); //skip one element because first is random } + mapGenOptions.setHumanOrCpuPlayerCount(humans); + updateTemplateList(); } @@ -350,9 +381,7 @@ void WindowNewMap::on_cpuCombo_activated(int index) cpu = PlayerColor::PLAYER_LIMIT_I - humans; ui->cpuCombo->setCurrentIndex(cpu + 1); //skip one element because first is random } - - mapGenOptions.setCompOnlyPlayerCount(cpu); - + int cpuTeams = mapGenOptions.getCompOnlyTeamCount(); //comp only players - 1 if(cpuTeams > cpu - 1) { @@ -360,6 +389,8 @@ void WindowNewMap::on_cpuCombo_activated(int index) ui->cpuTeamsCombo->setCurrentIndex(cpuTeams + 1); //skip one element because first is random } + mapGenOptions.setCompOnlyPlayerCount(cpu); + updateTemplateList(); } @@ -367,7 +398,7 @@ void WindowNewMap::on_cpuCombo_activated(int index) void WindowNewMap::on_randomMapCheck_stateChanged(int arg1) { randomMap = ui->randomMapCheck->isChecked(); - ui->templateCombo->setEnabled(randomMap); + ui->randomOptions->setEnabled(randomMap); updateTemplateList(); } @@ -385,23 +416,21 @@ void WindowNewMap::on_templateCombo_activated(int index) } -void WindowNewMap::on_widthTxt_textChanged(const QString &arg1) +void WindowNewMap::on_widthTxt_valueChanged(int value) { - int sz = arg1.toInt(); - if(sz > 1) + if(value > 1) { - mapGenOptions.setWidth(arg1.toInt()); + mapGenOptions.setWidth(value); updateTemplateList(); } } -void WindowNewMap::on_heightTxt_textChanged(const QString &arg1) +void WindowNewMap::on_heightTxt_valueChanged(int value) { - int sz = arg1.toInt(); - if(sz > 1) + if(value > 1) { - mapGenOptions.setHeight(arg1.toInt()); + mapGenOptions.setHeight(value); updateTemplateList(); } } @@ -419,7 +448,7 @@ void WindowNewMap::updateTemplateList() if(templates.empty()) return; - ui->templateCombo->addItem("[default]", 0); + ui->templateCombo->addItem(tr("[default]"), 0); for(auto * templ : templates) { @@ -464,3 +493,23 @@ void WindowNewMap::on_cpuTeamsCombo_activated(int index) updateTemplateList(); } + + +void WindowNewMap::on_sizeStandardRadio_toggled(bool checked) +{ + if (checked) { + ui->sizeGroup1->setEnabled(true); + ui->sizeGroup2->setEnabled(false); + } + updateTemplateList(); +} + + +void WindowNewMap::on_sizeCustomRadio_toggled(bool checked) +{ + if (checked) { + ui->sizeGroup1->setEnabled(false); + ui->sizeGroup2->setEnabled(true); + } + updateTemplateList(); +} \ No newline at end of file diff --git a/mapeditor/windownewmap.h b/mapeditor/windownewmap.h index b4402691a..a3695a0f8 100644 --- a/mapeditor/windownewmap.h +++ b/mapeditor/windownewmap.h @@ -67,13 +67,13 @@ class WindowNewMap : public QDialog const std::map> mapSizes { - {0, {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_SMALL}}, - {1, {CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_MIDDLE}}, - {2, {CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_LARGE}}, - {3, {CMapHeader::MAP_SIZE_XLARGE, CMapHeader::MAP_SIZE_XLARGE}}, - {4, {CMapHeader::MAP_SIZE_HUGE, CMapHeader::MAP_SIZE_HUGE}}, - {5, {CMapHeader::MAP_SIZE_XHUGE, CMapHeader::MAP_SIZE_XHUGE}}, - {6, {CMapHeader::MAP_SIZE_GIANT, CMapHeader::MAP_SIZE_GIANT}}, + {0, {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_SMALL}}, + {1, {CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_MIDDLE}}, + {2, {CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_LARGE}}, + {3, {CMapHeader::MAP_SIZE_XLARGE, CMapHeader::MAP_SIZE_XLARGE}}, + {4, {CMapHeader::MAP_SIZE_HUGE, CMapHeader::MAP_SIZE_HUGE}}, + {5, {CMapHeader::MAP_SIZE_XHUGE, CMapHeader::MAP_SIZE_XHUGE}}, + {6, {CMapHeader::MAP_SIZE_GIANT, CMapHeader::MAP_SIZE_GIANT}}, }; public: @@ -97,9 +97,9 @@ private slots: void on_templateCombo_activated(int index); - void on_widthTxt_textChanged(const QString &arg1); + void on_widthTxt_valueChanged(int value); - void on_heightTxt_textChanged(const QString &arg1); + void on_heightTxt_valueChanged(int value); void on_checkSeed_toggled(bool checked); @@ -107,6 +107,10 @@ private slots: void on_cpuTeamsCombo_activated(int index); + void on_sizeStandardRadio_toggled(bool checked); + + void on_sizeCustomRadio_toggled(bool checked); + private: void updateTemplateList(); diff --git a/mapeditor/windownewmap.ui b/mapeditor/windownewmap.ui index e4121de3c..6462aa850 100644 --- a/mapeditor/windownewmap.ui +++ b/mapeditor/windownewmap.ui @@ -6,8 +6,8 @@ 0 0 - 444 - 506 + 460 + 573 @@ -38,175 +38,267 @@ 10 - 20 - 291 - 91 + 10 + 441 + 121 Map size - + - 0 - 20 - 281 - 73 + 10 + 40 + 211 + 40 - - - - - Two level map - - - - - + + + + + false + + + false + + + + + 10 + 10 + 191 + 24 + + + + + + + + 120 + 0 + + + + + 200 + 16777215 + + + + + S (36x36) + + + + + M (72x72) + + + + + L (108x108) + + + + + XL (144x144) + + + + + H (180x180) + + + + + XH (216x216) + + + + + G (252x252) + + + + + + + + + + + 220 + 40 + 211 + 40 + + + + + + + false + + + false + + + + + 10 + 10 + 194 + 24 + + + + + + + + 48 + 0 + + + + + 96 + 16777215 + + + + Width + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + 999 + + + 0 + + + + + + + + 48 + 0 + + + + Height + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + 999 + + + 0 + + + + + + + + + + 10 + 20 + 211 + 22 + + + + + - 64 - 16777215 - - - - Qt::InputMethodHint::ImhDigitsOnly - - - 36 - - - 3 - - - - - - - Height - - - - - - - - 64 - 16777215 - - - - Qt::InputMethodHint::ImhDigitsOnly - - - 36 - - - 3 - - - - - - - - 48 - 0 - - - - - 96 + 16777215 16777215 - Width + Standard size + + true + + + buttonGroup + - - - - - - Qt::Orientation::Horizontal - - - QSizePolicy::Policy::Fixed - - - - 8 - 20 - - - - - - - - - 96 - 0 - - - - - 144 - 16777215 - - - - - S (36x36) - - - - - M (72x72) - - - - - L (108x108) - - - - - XL (144x144) - - - - - H (180x180) - - - - - XH (216x216) - - - - - G (252x252) - - - - - - + + + + 220 + 20 + 211 + 22 + + + + + + + Custom size + + + buttonGroup + + + + + + + + + 10 + 90 + 277 + 20 + + + + + 96 + 0 + + + + Underground + + - + + + false + 10 - 140 - 431 + 170 + 441 361 @@ -223,8 +315,8 @@ 10 - 20 - 411 + 110 + 421 91 @@ -236,72 +328,16 @@ 10 20 - 391 - 72 + 401 + 61 - - - - - Random - - - - - 0 - - - - - 1 - - - - - 2 - - - - - 3 - - - - - 4 - - - - - 5 - - - - - 6 - - - - - 7 - - - - - - - - Computer only - - - - 96 + 90 0 @@ -362,7 +398,7 @@ - 96 + 90 0 @@ -373,38 +409,7 @@ - Human/Computer - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - 0 - - - - 0 - - - - - - - - Human teams + Humans @@ -427,6 +432,99 @@ + + + + + 90 + 0 + + + + Human teams + + + + + + + + 90 + 0 + + + + + 16777215 + 16777215 + + + + 0 + + + + 0 + + + + + + + + Computers + + + + + + + + Random + + + + + 0 + + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + @@ -434,8 +532,8 @@ 10 - 170 - 411 + 250 + 421 51 @@ -445,13 +543,13 @@ - 0 + 10 20 - 411 + 401 26 - + @@ -460,6 +558,12 @@ 0 + + + 90 + 0 + + 16777215 @@ -482,6 +586,12 @@ 0 + + + 90 + 0 + + 120 @@ -501,6 +611,12 @@ 0 + + + 90 + 0 + + 120 @@ -520,6 +636,12 @@ 0 + + + 90 + 0 + + 120 @@ -531,31 +653,6 @@ - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - @@ -563,8 +660,8 @@ 10 - 120 - 411 + 200 + 421 51 @@ -586,13 +683,13 @@ - 0 + 10 20 - 411 + 401 26 - + @@ -601,6 +698,12 @@ 0 + + + 90 + 0 + + 144 @@ -623,6 +726,12 @@ 0 + + + 90 + 0 + + 144 @@ -642,6 +751,12 @@ 0 + + + 90 + 0 + + 144 @@ -661,6 +776,12 @@ 0 + + + 90 + 0 + + 144 @@ -672,19 +793,6 @@ - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - @@ -692,8 +800,8 @@ 10 - 230 - 411 + 300 + 421 51 @@ -715,68 +823,63 @@ - 0 + 10 20 - 411 + 401 26 - + + + + 90 + 0 + + Dirt - - - Qt::Orientation::Horizontal - - + + - 40 - 20 + 90 + 0 - - - - Gravel - - - Qt::Orientation::Horizontal - - + + - 40 - 20 + 90 + 0 - - - - Cobblestone - + Qt::Orientation::Horizontal + + QSizePolicy::Policy::Fixed + - 40 + 100 20 @@ -785,89 +888,108 @@ - + 10 - 280 - 411 - 34 + 20 + 421 + 91 - - - - - - 0 - 0 - - - - - 120 - 16777215 - - - - Template - - - - - - - false - - - - - - -1 - - - - - - - - - 80 - 320 - 283 - 33 - + + Template - - - - - Custom seed - - - - - - - false - - - Qt::InputMethodHint::ImhDigitsOnly - - - 0 - - - - + + + + 10 + 20 + 401 + 61 + + + + + + + + 100 + 0 + + + + + 120 + 16777215 + + + + Template + + + + + + + true + + + + 0 + 0 + + + + + + + -1 + + + + + + + + 90 + 0 + + + + + 120 + 16777215 + + + + Custom seed + + + + + + + + 39 + 22 + + + + 999 + + + + + 10 - 120 - 291 + 140 + 441 20 @@ -878,13 +1000,13 @@ - 310 - 20 - 111 - 101 + 250 + 540 + 201 + 26 - + @@ -893,12 +1015,6 @@ 0 - - - 0 - 36 - - 16777215 @@ -906,7 +1022,7 @@ - Ok + OK @@ -934,4 +1050,7 @@ + + + diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index b7b58a60b..0de9ea2e1 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -849,8 +849,8 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme auto pathfinderHelper = std::make_unique(gs, h, PathfinderOptions(this)); auto ti = pathfinderHelper->getTurnInfo(); - const bool canFly = pathfinderHelper->hasBonusOfType(BonusType::FLYING_MOVEMENT) || (h->boat && h->boat->layer == EPathfindingLayer::AIR); - const bool canWalkOnSea = pathfinderHelper->hasBonusOfType(BonusType::WATER_WALKING) || (h->boat && h->boat->layer == EPathfindingLayer::WATER); + const bool canFly = ti->hasFlyingMovement() || (h->boat && h->boat->layer == EPathfindingLayer::AIR); + const bool canWalkOnSea = ti->hasWaterWalking() || (h->boat && h->boat->layer == EPathfindingLayer::WATER); const int cost = pathfinderHelper->getMovementCost(h->visitablePos(), hmpos, nullptr, nullptr, h->movementPointsRemaining()); const bool movingOntoObstacle = t.blocked() && !t.visitable(); @@ -4118,12 +4118,10 @@ void CGameHandler::changeFogOfWar(const std::unordered_set &tiles, PlayerC for (auto tile : observedTiles) vstd::erase_if_present (fow.tiles, tile); - - if (fow.tiles.empty()) - return; } - sendAndApply(fow); + if (!fow.tiles.empty()) + sendAndApply(fow); } const CGHeroInstance * CGameHandler::getVisitingHero(const CGObjectInstance *obj) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 767cfc065..cdfed2975 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -257,7 +257,7 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c } //attack - int totalAttacks = stack->totalAttacks.getMeleeValue(); + int totalAttacks = stack->getTotalAttacks(false); //TODO: move to CUnitState const auto * attackingHero = battle.battleGetFightingHero(ba.side); @@ -378,7 +378,7 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co } //allow more than one additional attack - int totalRangedAttacks = stack->totalAttacks.getRangedValue(); + int totalRangedAttacks = stack->getTotalAttacks(true); //TODO: move to CUnitState const auto * attackingHero = battle.battleGetFightingHero(ba.side); diff --git a/server/processors/NewTurnProcessor.cpp b/server/processors/NewTurnProcessor.cpp index abd130793..41dc0bdc4 100644 --- a/server/processors/NewTurnProcessor.cpp +++ b/server/processors/NewTurnProcessor.cpp @@ -584,7 +584,7 @@ std::vector NewTurnProcessor::updateHeroesMovementPoints() { for (CGHeroInstance *h : elem.second.getHeroes()) { - auto ti = std::make_unique(h, 1); + auto ti = h->getTurnInfo(1); // NOTE: this code executed when bonuses of previous day not yet updated (this happen in NewTurn::applyGs). See issue 2356 int32_t newMovementPoints = h->movementPointsLimitCached(gameHandler->gameState()->map->getTile(h->visitablePos()).isLand(), ti.get()); diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 89b034128..84787af96 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -618,7 +618,9 @@ void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); delete [] hlp_tab; - gameHandler->sendAndApply(fc); + + if (!fc.tiles.empty()) + gameHandler->sendAndApply(fc); } void PlayerMessageProcessor::cheatPuzzleReveal(PlayerColor player) diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp index 3c7237d37..9e7a68c7b 100644 --- a/test/battle/CBattleInfoCallbackTest.cpp +++ b/test/battle/CBattleInfoCallbackTest.cpp @@ -71,6 +71,11 @@ public: addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, BonusSourceID())); } + bool isHypnotized() const override + { + return hasBonusOfType(BonusType::HYPNOTIZED); + } + void redirectBonusesToFake() { ON_CALL(*this, getAllBonuses(_, _, _)).WillByDefault(Invoke(&bonusFake, &BonusBearerMock::getAllBonuses)); @@ -262,7 +267,7 @@ TEST_F(AttackableHexesTest, DragonDragonBottomRightHead_BottomRightBreathFromHea UnitFake & attacker = addDragon(35, BattleSide::ATTACKER); UnitFake & defender = addDragon(attacker.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER); UnitFake & next = addRegularMelee(defender.getPosition().cloneInDirection(BattleHex::BOTTOM_RIGHT), BattleSide::DEFENDER); - + auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); EXPECT_TRUE(vstd::contains(attacked, &next)); diff --git a/test/battle/CUnitStateTest.cpp b/test/battle/CUnitStateTest.cpp index b4eb111c6..6b155355f 100644 --- a/test/battle/CUnitStateTest.cpp +++ b/test/battle/CUnitStateTest.cpp @@ -228,6 +228,19 @@ TEST_F(UnitStateTest, additionalMeleeAttack) EXPECT_EQ(subject.getTotalAttacks(true), 1); } +TEST_F(UnitStateTest, hypnotized) +{ + setDefaultExpectations(); + + { + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::SPELL_EFFECT, 41, BonusSourceID()); + + bonusMock.addNewBonus(bonus); + } + + EXPECT_TRUE(subject.isHypnotized()); +} + TEST_F(UnitStateTest, additionalRangedAttack) { setDefaultExpectations(); @@ -252,10 +265,16 @@ TEST_F(UnitStateTest, getMinDamage) bonusMock.addNewBonus(bonus); bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, BonusSourceID(), BonusCustomSubtype::creatureDamageMin); + bonus->effectRange = BonusLimitEffect::ONLY_DISTANCE_FIGHT; bonusMock.addNewBonus(bonus); + + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -10, BonusSourceID(), BonusCustomSubtype::creatureDamageMin); + bonus->effectRange = BonusLimitEffect::ONLY_MELEE_FIGHT; + bonusMock.addNewBonus(bonus); + } - EXPECT_EQ(subject.getMinDamage(false), 10); + EXPECT_EQ(subject.getMinDamage(false), 20); EXPECT_EQ(subject.getMinDamage(true), 10); } @@ -268,10 +287,15 @@ TEST_F(UnitStateTest, getMaxDamage) bonusMock.addNewBonus(bonus); bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, BonusSourceID(), BonusCustomSubtype::creatureDamageMax); + bonus->effectRange = BonusLimitEffect::ONLY_DISTANCE_FIGHT; + bonusMock.addNewBonus(bonus); + + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -10, BonusSourceID(), BonusCustomSubtype::creatureDamageMax); + bonus->effectRange = BonusLimitEffect::ONLY_MELEE_FIGHT; bonusMock.addNewBonus(bonus); } - EXPECT_EQ(subject.getMaxDamage(false), 10); + EXPECT_EQ(subject.getMaxDamage(false), 20); EXPECT_EQ(subject.getMaxDamage(true), 10); } diff --git a/test/mock/mock_BonusBearer.cpp b/test/mock/mock_BonusBearer.cpp index 72696cbc3..2496fb74b 100644 --- a/test/mock/mock_BonusBearer.cpp +++ b/test/mock/mock_BonusBearer.cpp @@ -40,8 +40,7 @@ TConstBonusListPtr BonusBearerMock::getAllBonuses(const CSelector & selector, co int64_t BonusBearerMock::getTreeVersion() const { - int64_t ret = treeVersion; - return ret << 32; + return treeVersion; } diff --git a/test/mock/mock_battle_Unit.h b/test/mock/mock_battle_Unit.h index 7560bfdc8..e7efca987 100644 --- a/test/mock/mock_battle_Unit.h +++ b/test/mock/mock_battle_Unit.h @@ -57,10 +57,12 @@ public: MOCK_CONST_METHOD0(isFrozen, bool()); MOCK_CONST_METHOD1(isValidTarget, bool(bool)); + MOCK_CONST_METHOD0(isHypnotized, bool()); MOCK_CONST_METHOD0(isClone, bool()); MOCK_CONST_METHOD0(hasClone, bool()); MOCK_CONST_METHOD0(canCast, bool()); MOCK_CONST_METHOD0(isCaster, bool()); + MOCK_CONST_METHOD0(canShootBlocked, bool()); MOCK_CONST_METHOD0(canShoot, bool()); MOCK_CONST_METHOD0(isShooter, bool());